概述

Service是Kubernetes中最核心的概念,正是因为对此概念的支持,Kubernetes在某种角度下可以被看成是一种微服务平台。Kubernetes中的pod并不稳定,比如由ReplicaSet、Deployment、DaemonSet等副本控制器创建的pod,其副本数量、pod名称、pod所运行的节点、pod的IP地址等,会随着集群规模、节点状态、用户缩放等因素动态变化。Service是一组逻辑pod的抽象,为一组pod提供统一入口,用户只需与service打交道,service提供DNS解析名称,负责追踪pod动态变化并更新转发表,通过负载均衡算法最终将流量转发到后端的pod。

原理

本节定义一个service示例并说明其工作原理。假设已经通过Deployment副本控制器创建了3个pod,每个pod包含"app=Myapp"标签,每个pod暴露端口9376。只所以假设已经有3个pod实例是为了方便说明service工作原理,推荐的做法是先创建service后创建pod。以下是service声明:

kind: Service
apiVersion: v1
metadata:
  name: my-service
spec:
  selector:
    app: MyApp
  ports:
  - protocol: TCP
    port: 80
    targetPort: 9376

保存到文件中并运行如下命令创建实例:

kubectl create -f Myapp.yaml

工作过程如下:

  1. 为实例分配置集群虚拟IP。如果在声明时明确指定集群虚拟IP,则分配指定IP,如未指定则自动分配。
  2. 根据实例名称、分配的集群虚拟IP、端口号创建DNS条目。
  3. 根据标签选择器聚合符合条件的节点,并创建相应endpoint,endpoint包含所有符合条件pod的ip地址与端口号。
  4. kube-proxy运行在集群中每一个节点上,并持续监控集群中service、endpoint变更,根据监控结果设置转发规则,将一个集群虚拟IP、端口与一个或者多个pod的IP、端口映射起来。
  5. 当在集群内部通过服务名称访问创建的service时,首先由DNS将服务名称转换成集群虚拟IP与端口号,kube-proxy根据转发规则对service的流量计算负载均衡、转发到位于后端的pod。

无标签选择器service

当与service对应的后端位于集群外部时,因为集群中没有相关的pod实例,因此这种情况下就不需要标签选择器。有标签选择器时系统自动查询pod并创建相应的endpoint,无标签选择器时需要用户手动创建endpoint,定义如下service:

kind: Service
apiVersion: v1
metadata:
  name: my-service
spec:
  ports:
  - protocol: TCP
    port: 80
    targetPort: 9376

为其手动创建endpoint:

kind: Endpoints
apiVersion: v1
metadata:
  name: my-service
subsets:
  - addresses:
      - ip: 1.2.3.4
    ports:
      - port: 9376

除需要手动创建endpoint外,无标签选择器与有标签选择器的servcie工作过程完全相同。

集群虚拟IP与kube-proxy

什么是虚拟IP?一般情况下,一个IP地址都会被分配给一个二层网络设备,网络设备可以是物理的、也可以是虚拟的,但总有设备对IP地址对应。而kubernetes中的集群IP,只是三层网络上的一个地址,没有设备与其对应,因此集群IP又是虚拟IP。

kube-proxy是kubernetes核心组件,运行在集群中每一个节点上,负责监控集群中service、endpoint变更,维护各个节点上的转发规则,是实现servcie功能的核心部件。在1.8及以后的版本中,kube-proxy有以下三种工作模式,但不同版本kubernetes能支持的工作模式不同,注意查证。

用户空间模式

Services overview diagram for userspace proxy

                  图1

上图要点:

  1. kube-proxy通过访问apiServer持续监控集群中service变更
  2. 当发现有新service时,kube-proxy随机开启local网络的端口号进行监听。同时向节点的iptables中添加转发条目,将service的流量转发到自己监听的端口上。
  3. kube-proxy通过访问apiServer持续监控集群中endpoint变更,并将service及其可用pod列表保存起来。
  4. 当在节点中访问服务时,流量首先到达iptables,iptables根据设置好的规则将流量转发给kube-proxy中相应的端口,kube-proxy再根据其维护的servcie与可用pod的对应关系,通过负载均衡计算以后转发给后端pod。

iptables模式 

                  图2

上图要点:与图1相比,kube-proxy的角色发生了变化。它在监控集群中的service与endpoint时,不会在local网络上打开端口并设置iptables先将流量转发给自己,由自己分发给pod。而是设置iptable将流量直接转发给pod,转发给pod的工作由kube-proxy转移到iptables中,也就是转移到内核空间。

iptables模式安全,可靠、效率高,但因为受内核的限制不够灵活。用户空间模式没有iptables模式安全,可靠、效率高,但因为它工作在用户空间,因此比较灵活,比如当某个pod没有应答时,它可以自动重试其它可用pod。iptables模式对于无应答的pod不会重试其它pod,而且问题pod一直存在,当pod的readness诊断失败后,pod才会被系统从可用列表中删除。

ipvs模式

iptables模式与ipvs模式本质相同,实现细节不同。前者首先定义规则,表示规则的数据保存在内核中,即通常说的“四表五链”。然后内核根据"四表五链"中的数据创建相应函数并挂载到内核中合适的点上。在ipvs模式中,kube-proxy根据其对servcie、endpoint的监控结果,调用内核netlink接口创建ipvs rule,不同于iptables的"四表五链",ipvs rule的数据组织更加紧凑、高效。因此相对于iptabels模式,ipvs模式更节省资源,对servcie、endpoint的变更同步速度更快。另外ipvs支持更多种类的负载均衡算法:

  • rr: round-robin
  • lc: least connection
  • dh: destination hashing
  • sh: source hashing
  • sed: shortest expected delay
  • nq: never queue

多端口service

很多service需要向外暴露不只一个端口,kubernetes支持在一个服务中声明多个端口,但必需为每个端口指定名称,避免在生成endpoint时产生歧义,示例如下:

kind: Service
apiVersion: v1
metadata:
  name: my-service
spec:
  selector:
    app: MyApp
  ports:
  - name: http
    protocol: TCP
    port: 80
    targetPort: 9376
  - name: https
    protocol: TCP
    port: 443
    targetPort: 9377

注意端口号名称有限制,必需只能包含小写字母、数字、中划线,且不能以中划线结尾。如123-abc、web合法, 123_abc、-web非法。

服务发现

所谓服务发现,就是在pod知道服务名称的前提下,如何得到其访问IP地址与端口号。Kubernetes支持两种类型的服务发现:环境变量与DNS。

环境变量方式

环境变量方式要求service先于使用者pod创建。在创建pod时,kubernetes将系统中所有激活服务的访问IP地址、端口号以环境变量的形式自动注册到pod所创建的容器中。不同服务的环境变量用名称区分,例如:
{SVCNAME}_SERVICE_HOST and {SVCNAME}_SERVICE_PORT
如果服务有多个端口则端口的环境变量名称为 {SVCNAME}_SERVICE_{PORTNAME}_PORT。
例如"redis-master"服务端口号为6379,集群虚拟IP地址为10.0.0.11,则其相关环境变量如下:

REDIS_MASTER_SERVICE_HOST=10.0.0.11
REDIS_MASTER_SERVICE_PORT=6379
REDIS_MASTER_PORT=tcp://10.0.0.11:6379
REDIS_MASTER_PORT_6379_TCP=tcp://10.0.0.11:6379
REDIS_MASTER_PORT_6379_TCP_PROTO=tcp
REDIS_MASTER_PORT_6379_TCP_PORT=6379
REDIS_MASTER_PORT_6379_TCP_ADDR=10.0.0.11

DNS方式

DNS是kubernetes的可选装插件,如果在集群中已经安装插件并打开此项功能,则自动为集群中的service添加名称解析条目,而且集群中所有pod默认可以使用服务名称寻址到服务。

假如在名称空间"my-ns"下有名为"my-service"的服务,则DNS插件自动生成"my-service.my-ns"条目。位于"my-ns"名称空间下的pod可直接使用"my-service"名称寻址,位于不同名称空间下的pod寻址时则用"my-service.my-ns"。

DNS插件支持对命名端口的查询,假如"my-service"服务中包含命名端口"http",协议为tcp,则寻址"_http._tcp.my-service.my-ns"则会返回http对应的端口号,这就是对命名端口的支持。

很明显DNS方式比环境变量方式合理、灵活的多。

无头服务

在定义service时,如果.spec.clusterIP被指定为固定值则为服务分配指定的IP,如果.spec.clusterIP字段没有出现在配置中,则自动分配集群虚拟IP。但如果.spect.clusterIP的值被指定为"None",此时创建的服务就被称为无头服务,其行为与普通服务有很大区别。首先不为服务分配集群虚拟IP,自然也就不能在DNS插件中添加服务相关条目。运行在各节点上的kube-proxy不为其添加转发规则,自然也就无法利用kube-proxy的转发、负载均衡功能。

虽然不向DNS插件添加服务相关条目,但可能添加其它条目,取决于service是合包含标签选择器。

包含标签选择器

此种情况下,系统仍然根据标签选择器创建endpoint,并根据endpoint向DNS插件中添加条目。比如命名空间为"my-ns",服务名称为"my-headless",endpoing指向的pod名称为pod1、pod2,则向DNS插件中添加的条目类似于"pod1.my-headless.my-ns"与"pod1.my-headless.my-ns",此时DNS中的条目直接指向pod。在StatefulSet类型资源中,使用无头服务为其中的pod提供名称解析服务,只所以可行,其实是因为StatefulSet能保证其管理的pod有序,名称地址等特征保持不变。

不包含标签选择器

CNAME records for ExternalName-type services.
A records for any Endpoints that share a name with the service, for all other types.

Service类型

本文以上示例都以默认服务类型为前提,实际上kubernetes暴露服务IP的类型有四种,分别如下:

  • ClusterIP:默认类型,为服务分配集群虚拟IP,此时集群内部的pod可以通过服务名称寻址到服务的集群虚拟IP地址,集群外无效。
  • NodePort:在每个节点上为服务分配静态端口号,注意此端口号占用的是节点网络,此时如果在集群外部访问任何一个节点的IP地址加指定的端口号,kube-proxy会将流量转发到服务的集群虚拟IP,再由虚拟IP寻址到POD。
  • LoadBalancer:通过云服务供应商提供的load balancer向外部暴露服务,由指定的load balancer负责对NodePort与ClusterIP服务的路由。
  • ExternalName:比较特殊,只是简单的将服务名称映射成指定名称,如foo.bar.example.com,kube-dns1.7有以后版本支持此特性。

接下来介绍除ClusterIP类型以外的其它三种类型

NodePort类型

NodePort类型服务会占用节点网络与端口号,其目的是为外部网络访问集群内部服务提供一种手段。首先将.spec.type值设置为NodePort,在创建服务时系统在各个节点上自动分配相同的port,范围由--service-node-port-range,默认30000-32767。服务占用的NodePort号可通过.spec.ports[*].nodePort查询。当访问IP地址加NodePort端口号时,节点将流量转发到集群虚拟IP加端口号,最终访问到后端pod。

如果存在多个节点网络,默认为所以节点网络打开NodePort。如果想限定使用的节点网络,可以为kube-proxy设置--nodeport-addresses,其值为地址块,如--nodeport-addresses=127.0.0.0/8,则只会为匹配地址块的节点网络地址打开端口。

设置.spec.ports[*].nodePort为特定值,指明使用指定的端口号而非随机分配,其值必需位于--service-node-port-range规定的范围内。此时由用户自行解决端口号冲突问题。

集群内部pod仍可通过“服务名=>>集群虚拟IP:端口号=>>kube-proxy转发到pod”的形式访问服务。
集群外部访问服务路径为“节点IP:NodePort=>>集群虚拟IP:端口号=>>kube-proxy转发到pod”。集群外部不能使用服务名。

LoadBalancer类型

此种类型在公有云服务供应商平台上会用到,典型配置如下:

kind: Service
apiVersion: v1
metadata:
  name: my-service
spec:
  selector:
    app: MyApp
  ports:
  - protocol: TCP
    port: 80
    targetPort: 9376
  clusterIP: 10.0.171.239
  loadBalancerIP: 78.11.24.19
  type: LoadBalancer
status:
  loadBalancer:
    ingress:
    - ip: 146.148.47.155

.spec.status.loadBalancer指定服务商提供的负载均衡器地址,.spec.type设置成LoadBalancer,.spec.loadBalancerIP指定占用的由服务商负载均衡器提供的IP地址,当访问此IP地址时,流量被直接转发到后端pod。这种方式的实现机制与配置方式与具体的供应商有关。

ExternalName类型

典型配置如下:

kind: Service
apiVersion: v1
metadata:
  name: my-service
  namespace: prod
spec:
  type: ExternalName
  externalName: my.database.example.com

当在集群访问my-service时,kube-dns插件返回的结果会是my.database.example.com,而my.database.example.com应该是在集群外其它DNS服务器中可用的域名,使用者需要通过外部DNS将此域名再转换成IP地址。另外一种形式,直接将服务名称转换成集群外可用的IP地址:

kind: Service
apiVersion: v1
metadata:
  name: my-service
spec:
  selector:
    app: MyApp
  ports:
  - name: http
    protocol: TCP
    port: 80
    targetPort: 9376
  externalIPs:
  - 80.11.12.10

kube-dns插件自动将服务名转换成80.11.12.10这个IP地址。

总结

kube-proxy运行在集群中的每个节点上,并为每个服务设置转发条目,即使在这个节点上从来不会访问这个服务,效率很低,这种方式简单通用,但不适合于大规模集群服务。对于大规模集群,应该使用供应商或者自定义Loadbalancer。

参考:https://kubernetes.io/docs/concepts/services-networking/service/#

Logo

开源、云原生的融合云平台

更多推荐