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
工作过程如下:
当与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?一般情况下,一个IP地址都会被分配给一个二层网络设备,网络设备可以是物理的、也可以是虚拟的,但总有设备对IP地址对应。而kubernetes中的集群IP,只是三层网络上的一个地址,没有设备与其对应,因此集群IP又是虚拟IP。
kube-proxy是kubernetes核心组件,运行在集群中每一个节点上,负责监控集群中service、endpoint变更,维护各个节点上的转发规则,是实现servcie功能的核心部件。在1.8及以后的版本中,kube-proxy有以下三种工作模式,但不同版本kubernetes能支持的工作模式不同,注意查证。
图1
上图要点:
图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才会被系统从可用列表中删除。
iptables模式与ipvs模式本质相同,实现细节不同。前者首先定义规则,表示规则的数据保存在内核中,即通常说的“四表五链”。然后内核根据"四表五链"中的数据创建相应函数并挂载到内核中合适的点上。在ipvs模式中,kube-proxy根据其对servcie、endpoint的监控结果,调用内核netlink接口创建ipvs rule,不同于iptables的"四表五链",ipvs rule的数据组织更加紧凑、高效。因此相对于iptabels模式,ipvs模式更节省资源,对servcie、endpoint的变更同步速度更快。另外ipvs支持更多种类的负载均衡算法:
很多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是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.
本文以上示例都以默认服务类型为前提,实际上kubernetes暴露服务IP的类型有四种,分别如下:
接下来介绍除ClusterIP类型以外的其它三种类型
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”。集群外部不能使用服务名。
此种类型在公有云服务供应商平台上会用到,典型配置如下:
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。这种方式的实现机制与配置方式与具体的供应商有关。
典型配置如下:
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/#