K8s Service原理介绍

K8s Service原理介绍

1.相关概念解读

1.1 Service

Kubernetes中一个应用服务会有一个或多个实例(Pod),每个实例(Pod)的IP地址由网络插件动态随机分配(Pod重启后IP地址会改变)。为屏蔽这些后端实例的动态变化和对多实例的负载均衡,引入了Service这个资源对象,如下所示:
apiVersion: v1
kind: Service
metadata:
name: nginx-svc
labels:
app: nginx
spec:
type: ClusterIP
ports:
– port: 80
targetPort: 80
selector:
app: nginx
12345678910111213
根据创建Service的type类型不同,可分成4种模式:

ClusterIP:

默认方式。根据是否生成ClusterIP又可分为普通Service和Headless Service两类:

普通Service:

通过为Kubernetes的Service分配一个集群内部可访问的固定虚拟IP(Cluster IP),实现集群内的访问。为最常见的方式。

Headless Service:

该服务不会分配Cluster IP,也不通过kube-proxy做反向代理和负载均衡。而是通过DNS提供稳定的网络ID来访问,DNS会将headless service的后端直接解析为podIP列表。主要供StatefulSet使用。

NodePort:

除了使用Cluster IP之外,还通过将service的port映射到集群内每个节点的相同一个端口,实现通过nodeIP:nodePort从集群外访问服务。

LoadBalancer:

和nodePort类似,不过除了使用一个Cluster IP和nodePort之外,还会向所使用的公有云申请一个负载均衡器(负载均衡器后端映射到各节点的nodePort),实现从集群外通过LB访问服务。

ExternalName:

是 Service 的特例。此模式主要面向运行在集群外部的服务,通过它可以将外部服务映射进k8s集群,且具备k8s内服务的一些特征(如具备namespace等属性),来为集群内部提供服务。此模式要求kube-dns的版本为1.7或以上。这种模式和前三种模式(除headless service)最大的不同是重定向依赖的是dns层次,而不是通过kube-proxy。

比如,在service定义中指定externalName的值”my.database.example.com”:

此时k8s集群内的DNS服务会给集群内的服务名 <service-name>.<namespace>.svc.cluster.local 创建一个CNAME记录,其值为指定的”my.database.example.com”。
当查询k8s集群内的服务my-service.prod.svc.cluster.local时,集群的 DNS 服务将返回映射的CNAME记录”foo.bar.example.com”。
备注:

前3种模式,定义服务的时候通过selector指定服务对应的pods,根据pods的地址创建出endpoints作为服务后端;Endpoints Controller会watch Service以及pod的变化,维护对应的Endpoint信息。kube-proxy根据Service和Endpoint来维护本地的路由规则。当Endpoint发生变化,即Service以及关联的pod发生变化,kube-proxy都会在每个节点上更新iptables,实现一层负载均衡。

而ExternalName模式则不指定selector,相应的也就没有port和endpoints。
ExternalName和ClusterIP中的Headles Service同属于Headless Service的两种情况。Headless Service主要是指不分配Service IP,且不通过kube-proxy做反向代理和负载均衡的服务。

针对以上各发布方式,会涉及一些相应的Port和IP的概念。

1.2 Port

Service中主要涉及三种Port:

* `port`

这里的port表示service暴露在clusterIP上的端口,clusterIP:Port 是提供给集群内部访问kubernetes服务的入口。

targetPort

containerPort,targetPort是pod上的端口,从port和nodePort上到来的数据最终经过kube-proxy流入到后端pod的targetPort上进入容器。

nodePort

nodeIP:nodePort 是提供给从集群外部访问kubernetes服务的入口。

总的来说,port和nodePort都是service的端口,前者暴露给从集群内访问服务,后者暴露给从集群外访问服务。从这两个端口到来的数据都需要经过反向代理kube-proxy流入后端具体pod的targetPort,从而进入到pod上的容器内。

1.3 IP

使用Service服务还会涉及到几种IP:

ClusterIP

Pod IP 地址是实际存在于某个网卡(可以是虚拟设备)上的,但clusterIP就不一样了,没有网络设备承载这个地址。它是一个虚拟地址,由kube-proxy使用iptables规则重新定向到其本地端口,再均衡到后端Pod。当kube-proxy发现一个新的service后,它会在本地节点打开一个任意端口,创建相应的iptables规则,重定向服务的clusterIP和port到这个新建的端口,开始接受到达这个服务的连接。

Pod IP

Pod的IP,每个Pod启动时,会自动创建一个镜像为gcr.io/google_containers/pause的容器,Pod内部其他容器的网络模式使用container模式,并指定为pause容器的ID,即:network_mode: “container:pause容器ID”,使得Pod内所有容器共享pause容器的网络,与外部的通信经由此容器代理,pause容器的IP也可以称为Pod IP。

节点IP

Node-IP,service对象在Cluster IP range池中分配到的IP只能在内部访问,如果服务作为一个应用程序内部的层次,还是很合适的。如果这个service作为前端服务,准备为集群外的客户提供业务,我们就需要给这个服务提供公共IP了。指定service的spec.type=NodePort,这个类型的service,系统会给它在集群的各个代理节点上分配一个节点级别的端口,能访问到代理节点的客户端都能访问这个端口,从而访问到服务。

2.kube-proxy简介

当service有了port和nodePort之后,就可以对内/外提供服务。那么其具体是通过什么原理来实现的呢?奥妙就在kube-proxy在本地node上创建的iptables规则。
每个Node上都运行着一个kube-proxy进程,kube-proxy是service的具体实现载体,所以,说到service,就不得不提到kube-proxy。
kube-proxy是kubernetes中设置转发规则的组件。kube-proxy通过查询和监听API server中service和endpoint的变化,为每个service都建立了一个服务代理对象,并自动同步。服务代理对象是proxy程序内部的一种数据结构,它包括一个用于监听此服务请求的SocketServer,SocketServer的端口是随机选择的一个本地空闲端口。如果存在多个pod实例,kube-proxy同时也会负责负载均衡。而具体的负载均衡策略取决于Round Robin负载均衡算法及service的session会话保持这两个特性。会话保持策略使用的是ClientIP(将同一个ClientIP的请求转发同一个Endpoint上)。kube-proxy 可以直接运行在物理机上,也可以以 static-pod 或者 daemonset 的方式运行。

kube-proxy 当前支持以下3种实现模式:

userspace:

最早的负载均衡方案,它在用户空间监听一个端口,Service的请求先从用户空间进入内核iptables转发到这个端口,然后再回到用户空间,由kube-proxy完成后端endpoints的选择和代理,这样流量会有从用户空间进出内核的过程,效率低,有明显的性能瓶颈。

iptables:

目前默认的方案,完全以内核 iptables 的 nat 方式实现 service 负载均衡。该方式在大规模情况下存在一些性能问题:首先,iptables 没有增量更新功能,更新一条规则需要整体 flush,更新时间长,这段时间之内流量会有不同程度的影响;另外,iptables 规则串行匹配,没有预料到 Kubernetes 这种在一个机器上会有很多规则的情况,流量需要经过所有规则的匹配之后再进行转发,对时间和内存都是极大的消耗,尤其在大规模情况下对性能的影响十分明显。

ipvs:

为解决 iptables 模式的性能问题,v1.11 新增了 ipvs 模式(v1.8 开始支持测试版,并在 v1.11 GA),采用增量式更新,不会强制进行全量更新,可以保证 service 更新期间连接保持不断开;也不会进行串行的匹配,会通过一定的规则进行哈希 map 映射,很快地映射到对应的规则,不会出现大规模情况下性能线性下降的状况。

service原理

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

kind: ServiceapiVersion: v1metadata: name: my-servicespec: selector: app: MyApp ports: – protocol: TCP port: 80 targetPort: 9376

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

kubectl create -f Myapp.yaml

工作过程如下:

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

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

kind: ServiceapiVersion: v1metadata: name: my-servicespec: ports: – protocol: TCP port: 80 targetPort: 9376

为其手动创建endpoint:

kind: EndpointsapiVersion: v1metadata: name: my-servicesubsets: – 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能支持的工作模式不同,注意查证。

用户空间模式

上图要点:

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

iptables模式

上图要点:

与图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: ServiceapiVersion: v1metadata: name: my-servicespec: selector: app: MyApp ports: – name: http protocol: TCP port: 80 targetPort: 9376 – name: https protocol: TCP port: 443 targetPort: 9377

注意端口号名称有限制,必需只能包含小写字母、数字、中划线,且不能以中划线结尾。如123-abc、web合名称为 {SVCNAME}_SERVICE_{PORTNAME}_PORT。
例如”redis-master”服务端口号为6379,集群虚拟IP地址为10.0.0.11,则其相关环境变量如下:

REDIS_MASTER_SERVICE_HOST=10.0.0.11REDIS_MASTER_SERVICE_PORT=6379REDIS_MASTER_PORT=tcp://10.0.0.11:6379REDIS_MASTER_PORT_6379_TCP=tcp://10.0.0.11:6379REDIS_MASTER_PORT_6379_TCP_PROTO=tcpREDIS_MASTER_PORT_6379_TCP_PORT=6379REDIS_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: ServiceapiVersion: v1metadata: name: my-servicespec: selector: app: MyApp ports: – protocol: TCP port: 80 targetPort: 9376 clusterIP: 10.0.171.239 loadBalancerIP: 78.11.24.19 type: LoadBalancerstatus: loadBalancer: ingress: – ip: 146.148.47.155

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

ExternalName类型

典型配置如下:

kind: ServiceapiVersion: v1metadata: name: my-service namespace: prodspec: type: ExternalName externalName: my.database.example.com

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

kind: ServiceapiVersion: v1metadata: name: my-servicespec: 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。