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
根据创建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服务会给集群内的服务名
创建一个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的概念。
targetPort
containerPort,targetPort是pod上的端口,从port和nodePort上到来的数据最终经过kube-proxy流入到后端pod的targetPort上进入容器。
nodePort
nodeIP:nodePort 是提供给从集群外部访问kubernetes服务的入口。
总的来说,port和nodePort都是service的端口,前者暴露给从集群内访问服务,后者暴露给从集群外访问服务。从这两个端口到来的数据都需要经过反向代理kube-proxy
流入后端具体pod的targetPort,从而进入到pod上的容器内。
使用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,系统会给它在集群的各个代理节点上分配一个节点级别的端口,能访问到代理节点的客户端都能访问这个端口,从而访问到服务。
当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 映射,很快地映射到对应的规则,不会出现大规模情况下性能线性下降的状况。
后文主要对目前使用较多的iptables模式进行分析。
传统iptables的数据包转发流程如下所示:
由图可知,流量到达防火墙后进入路由表前会先进入PREROUTING链,所以首先对PREROUTING阶段进行分析。
以k8s集群中的一个heapster服务为例:
如图所示,这个heapster服务的模式是NodePort。
PREROUTIN链只存在于nat表和mangle表中,又由于从代码可知kube-proxy主要操作nat表和filter表,不涉及mangle表,因此,首先看nat表的PREROUTING链:
可见经过cali-PREROUTING后,流量全部进入到了KUBE-SERVICES链。
KUBE-SERVICES链如下:
目的地址是clusterIP的有两条链,对应两种流量:
KUBE-MARK-MASQ
链,再转发到KUBE-SVC-BJM46V3U5RZHCFRZ
KUBE-SVC-BJM46V3U5RZHCFRZ
链另外,在KUBE-SERVICES链最后还有一条链,对应访问NodePort的流量:
KUBE-NODEPORTS all -- anywhere anywhere /* kubernetes service nodeports; NOTE: this must be the last rule in this chain */ ADDRTYPE match dst-type LOCAL
<1> 首先跟踪KUBE-MARK-MASQ
链:
KUBE-MARK-MASQ
链给途径的流量打了个0x4000
标记。
<2> 再来看KUBE-SVC-BJM46V3U5RZHCFRZ
链:
途径KUBE-SVC-BJM46V3U5RZHCFRZ
链的流量会以各50%的概率(statistic mode random probability 0.50000000000)转发到两个endpoint后端链KUBE-SEP-P7XP4GXFNM4TCRK6
和KUBE-SEP-HY3BTV7JVLTVQYRP
中,概率是通过probability后的1.0/float64(n-i)
计算出来的,譬如有两个的场景,那么将会是一个0.5和1也就是第一个是50%概率第二个是100%概率,如果是三个的话类似,33%、50%、100%。
<3> 最后看KUBE-NODEPORTS
链:
也是先通过KUBE-MARK-MASQ
链打了个标记0x4000
,然后转到KUBE-SVC-BJM46V3U5RZHCFRZ
链。
可见经过第三步后,第二步中的三种流量都跳转到了KUBE-SVC-BJM46V3U5RZHCFRZ
,其中从集群外部来的流量以及通过nodePort访问的流量添加上了0x4000
标记。
KUBE-SEP-P7XP4GXFNM4TCRK6
的流量进行跟踪有两条链,对应两种流量:
KUBE-MARK-MASQ
链,然后会打上0x4000
标记,然后再做DNAT。DNAT
,将目的ip和port转换为对应pod的ip和port。原始源地址 | 原始目标地址 | 是否打0x4000 标记(做SNAT) |
是否做DNAT |
---|---|---|---|
非PodIP | clusterIP | 是 | 是 |
服务自身PodIP | clusterIP | 是(当转发到该pod自身时) | 是 |
PodIP | clusterIP | 否 | 是 |
* | NodePort | 是 | 是 |
在主机上通过clusterIP访问heapster服务,各IP如下:
10.142.232.150
;10.233.60.145
192.168.38.213
192.168.68.78
实验过程
从10.142.232.150这个主机上访问heapster的 clusterIP:port
(10.233.60.145:80),在heapster的两个pod实例所在机器分别抓包:
可知到走的是cali008939a0a2f网卡,用tcpdump抓这块网卡的数据包。
实验结果
访问流量:
10.142.232.150抓包结果:
10.142.232.151抓包结果:
可以看到:
实验表明,当从集群中主机上通过clusterIP访问服务时,都会对数据包做SNAT(转换为该节点ip)和DNAT(转换为podIP),与表中第一种流量一致。
实验环境
使用测试应用prometheus,从10.142.232.150主机的prometheus pod(192.168.38.214)中通过clusterIP(10.233.6.92)访问prometheus服务,各IP如下:
192.168.38.214
;10.233.6.92
192.168.38.214
192.168.68.78
实验过程
从10.142.232.151主机的prometheus pod(192.168.38.214)中通过clusterIP(10.233.6.92)访问自身服务,在prometheus的两个pod实例所在机器分别用tcpdump抓包:
实验结果
访问流量:
10.142.232.150抓包结果:
10.142.232.151抓包结果:
可以看到:
实验表明,当从服务自身pod中访问服务的clusterIP时:
实验结果与表中第三种流量一致。
实验环境
从10.142.232.152主机的network-detect pod中通过clusterIP访问heapster服务,各IP如下:
192.168.114.194
;10.233.60.145
192.168.38.213
192.168.68.78
实验过程
从192.168.114.194这个pod中访问heapster的clusterIP地址10.233.60.145,在heapster的两个pod实例所在机器分别用tcpdump抓源地址是192.168.114.194的数据包。
实验结果
访问流量:
10.142.232.150抓包结果:
10.142.232.151抓包结果:
可以看到:
实验表明,当源地址为podIP,且不是要访问的服务本身的pod时,仅会对数据包做DNAT(转换为podIP),与表中第三种流量一致。
####4. 验证第四种流量
实验环境
通过NodeIP访问heapster服务,各IP如下:
192.168.114.194
;10.233.60.145
192.168.38.213
192.168.68.78
实验过程
实验1:
从192.168.114.194这个pod中访问heapster的 nodeIP:nodePort
(10.142.232.152:30082),在heapster的两个pod实例所在机器分别用tcpdump抓目的地址是本机heapster podIP的数据包。
实验2:
从某一节点访问heapster的 nodeIP:nodePort
10.142.232.152:30082,在heapster的两个pod实例所在机器分别用tcpdump抓源目的地址是本机的heapster podIP的数据包。
实验结果
10.142.232.150抓包结果:
10.142.232.151抓包结果:
10.142.232.150抓包结果:
10.142.232.151抓包结果:
可以看到实验1和实验2结果一致:
实验表明,无论源地址是什么,只要目的地址是通过nodePort访问,都会对数据包做SNAT(转换为该节点ip)和DNAT(转换为podIP),与表中第四种流量一致。
heapster的两个podIP分别是192.168.68.66和192.168.114.202,根据路由表,分别发送到10.142.232.151和10.142.232.152:
以其中一条发送到10.142.232.151的流量为例,进行流量跟踪。假如当前流量所在机器是10.142.232.150,由于要发送的151不是本机,所以流量会走filter的FORWARD链:
FORWARD链不对流量做处理,所以流量随后会继续去到POSTROUTING链
在POSTROUTING阶段,对之前打了0x4000标记的流量做了MASQUERADE,将源地址替换为主机网卡地址。
Kubernetes使用iptables来为service做路由和负载均衡,其核心逻辑代码在kubernetes/pkg/proxy/iptables/proxier.go
中的syncProxyRules
函数内。
1. iptables链定义
这里定义了kube-proxy创建的自定义iptables链的名称。
2. 导流到KUBE-SERVICES链
上述代码做了如下两件事情:
KUBE-SERVICES
的自定义链。KUBE-SERVICES
中 iptables -I OUTPUT -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
KUBE-SERVICES
中iptables -t nat -I PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
KUBE-SERVICES
中iptables -t nat -I OUTPUT -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
3. 导流到KUBE-POSTROUTING链
上述代码做了如下两件事情:
KUBE-POSTROUTING
的自定义链KUBE-POSTROUTING
中(要做SNAT)iptables -t nat -I POSTROUTING -m comment --comment "kubernetes postrouting rules" -j KUBE-POSTROUTING
4. 调用iptables-save获取filter和nat中已有链
上述代码做的事情是:
iptables-save
命令解析当前node中iptables的filter表和nat表中已经存在的chain,kubernetes会将这些chain存在两个map中(existingFilterChains
和existingNATChains
),然后再创建四个protobuf中的buffer(分别是filterChains
、filterRules
、natChains
和natRules
),后续kubernetes会往这四个buffer中写入大量iptables规则,最后再调用iptables-restore写回到当前node的iptables中。5. 打标记
上述代码做了如下事情:
如果当前node的iptables的filter表和nat表中已经存在名为KUBE-SERVICES
、KUBE-NODEPORTS
、KUBE-POSTROUTING
和KUBE-MARK-MASQ
的自定义链,那就原封不动将它们按照原来的形式(: [:])写入到filterChains和natChains中;如果没有,则以“: [0:0]”的格式写入上述4个chain(即将:KUBE-SERVICES – [0:0]
、:KUBE-NODEPORTS – [0:0]
、:KUBE-POSTROUTING – [0:0]
和:KUBE-MARK-MASQ – [0:0]
写入到filterChains和natChains中,这相当于在filter表和nat表中创建了上述4个自定义链);
对nat表的自定义链KUBE-POSTROUTING
写入如下规则,重定向到自定义链KUBE-MASQUERADE:
-A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -m mark –mark 0x4000/0x4000 -j MASQUERADE
KUBE-MARK-MASQ
写入如下规则:-A KUBE-MARK-MASQ -j MARK --set-xmark 0x4000/0x4000
这里2和3做的事情的实际含义是kubernetes会让***所有kubernetes集群内部产生的数据包***流经nat表的自定义链KUBE-MARK-MASQ
,然后在这里kubernetes会对这些数据包打一个标记(0x4000/0x4000),接着在nat的自定义链KUBE-POSTROUTING
中根据上述标记匹配所有的kubernetes集群内部的数据包,匹配的目的是kubernetes会对这些包做***SNAT***操作。
KUBE-SVC-XXXXXXXXXXXXXXXX
的自定义链(这里的XXXXXXXXXXXXXXXX是一个16位字符串,kubernetes使用SHA256 算法对“服务名+协议名”生成哈希值,然后通过base32对该哈希值编码,最后取编码值的前16位,kubernetes通过这种方式保证每个服务对应的“KUBE-SVC-XXXXXXXXXXXXXXXX”都不一样)。然后对每个服务,根据服务是否有cluster ip、是否有external ip、是否启用了外部负载均衡服务在nat表的自定义链KUBE-SERVICES
中加入类似如下这样的规则:-A KUBE-SERVICES -d 172.30.32.92/32 -p tcp -m comment --comment "kongxl/test2:8778-tcp cluster IP" -m tcp --dport 8778 -j KUBE-SVC-XAKTM6QUKQ53BZHS
KUBE-NODEPORTS
中加入如下两条规则:KUBE-NODEPORTS
的来自于服务“ym/echo-app-nodeport”的数据包都会跳转到自定义链KUBE-MARK-MASQ
中,即kubernetes会对来自上述服务的这些数据包打一个标记(0x4000/0x4000)-A KUBE-NODEPORTS -p tcp -m comment --comment "ym/echo-app-nodeport:" -m tcp --dport 30001 -j KUBE-MARK-MASQ
KUBE-NODEPORTS
的来自于服务“ym/echo-app-nodeport”的数据包都会跳转到自定义链KUBE-SVC-LQ6G5YLNLUHHZYH5
中-A KUBE-NODEPORTS -p tcp -m comment --comment "ym/echo-app-nodeport:" -m tcp --dport 30001 -j KUBE-SVC-LQ6G5YLNLUHHZYH5
KUBE-SERVICES
中加入如下规则:-A KUBE-SERVICES -d 172.30.32.92/32 -p tcp -m comment --comment "kongxl/test2:8080-tcp has no endpoints" -m tcp --dport 8080 -j REJECT
在遍历每一个服务的过程中,对每一个服务,如果这个服务有对应的endpoints,那么在nat表中创建名为KUBE-SEP-XXXXXXXXXXXXXXXX
的自定义链。然后对每个endpoint,如果该服务配置了session affinity
,则在nat表的该service对应的自定义链KUBE-SVC-XXXXXXXXXXXXXXXX
中加入类似如下这样的规则:
default/docker-registry:5000-tcp
的数据包都会跳转到自定义链KUBE-SEP-LPCU5ERTNL2YBWXG
中,且会在一段时间内保持session affinity
,保持时间为180秒(这里kubernetes用-m recent –rcheck –seconds 180 –reap
实现了会话保持)-A KUBE-SVC-ECTPRXTXBM34L34Q -m comment --comment "default/docker-registry:5000-tcp" -m recent --rcheck --seconds 180 --reap --name KUBE-SEP-LPCU5ERTNL2YBWXG --mask 255.255.255.255 --rsource -j KUBE-SEP-LPCU5ERTNL2YBWXG
在遍历每一个服务的过程中,对每一个服务,如果这个服务有对应的endpoints,且没有配置session affinity
,则在nat表的该service对应的自定义链KUBE-SVC-XXXXXXXXXXXXXXXX
中加入类似如下这样的规则(如果该服务对应的endpoints大于等于2,则还会加入负载均衡规则):
所有流经自定义链KUBE-SVC-VX5XTMYNLWGXYEL4的来自于服务“ym/echo-app”的数据//包既可能会跳转到自定义链KUBE-SEP-27OZWHQEIJ47W5ZW
,也可能会跳转到自定义链KUBE-SEP-AA6LE4U3XA6T2EZB
,这里kubernetes用-m statistic --mode random //--probability 0.50000000000
实现了对该服务访问的负载均衡
-A KUBE-SVC-VX5XTMYNLWGXYEL4 -m comment --comment "ym/echo-app:" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-27OZWHQEIJ47W5ZW
-A KUBE-SVC-VX5XTMYNLWGXYEL4 -m comment --comment "ym/echo-app:" -j KUBE-SEP-AA6LE4U3XA6T2EZB
最后,在遍历每一个服务的过程中,对每一个服务的endpoints,在nat表的该endpoint对应的自定义链KUBE-SEP-XXXXXXXXXXXXXXXX
中加入如下规则,实现到该服务最终目的地的***DNAT***:
服务“ym/echo-app”有两个endpoints,之前kubernetes已经对该服务做了负载均衡,所以这里一共会产生4条跳转规则
-A KUBE-SEP-27OZWHQEIJ47W5ZW -s 10.1.0.8/32 -m comment –comment "ym/echo-app:" -j KUBE-MARK-MASQ
-A KUBE-SEP-27OZWHQEIJ47W5ZW -p tcp -m comment –comment "ym/echo-app:" -m tcp -j DNAT –to-destination 10.1.0.8:8080
-A KUBE-SEP-AA6LE4U3XA6T2EZB -s 10.1.1.4/32 -m comment –comment "ym/echo-app:" -j KUBE-MARK-MASQ
-A KUBE-SEP-AA6LE4U3XA6T2EZB -p tcp -m comment –comment "ym/echo-app:" -m tcp -j DNAT –to-destination 10.1.1.4:8080
7. 删除失效链
上述代码做的事情是:
删掉当前节点中已经不存在的服务所对应的KUBE-SVC-XXXXXXXXXXXXXXXX
链和KUBE-SEP- XXXXXXXXXXXXXXXX
链;
向nat表的自定义链KUBE-SERVICES
中写入如下这样规则:
将目的地址是本地的数据包跳转到自定义链KUBE-NODEPORTS中
-A KUBE-SERVICES -m comment --comment "kubernetes service nodeports; NOTE: th"
8. 写回iptables
上述代码做的事情是:合并已经被写入了大量规则的四个protobuf中的buffer(分别是filterChains、filterRules、natChains和natRules),然后调用iptables-restore
写回到当前node的iptables中。
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!