当我们在K8S上部署一个工作负载时,通常会设置多个副本(下面开始简称:Pod)来实现高可用,因为Pod的IP经常会变化,所以我们会给服务创建一个K8S的Service,如果Service的类型为ClusterIP的话,K8S会给这个Service分配一个ServiceIP,如果类型为NodePort,K8S会打开每一个节点的某个端口,然后我们调用这个服务就会使用K8S分配的ServiceIP或打开的端口,流量总是能转到后端的多个Pod上,这个ServiceIP只能在Pod和K8S的节点中访问,今天我们就来介绍一下这个ServiceIP的实现原理。
刚开始玩K8S的时候,我以为K8S是不是在哪里创建了一张虚拟网卡然后把ServiceIP设置在那个网卡上,翻遍了K8S的节点和POD都没找到有这个IP的网卡。后来才知道,原来K8S利用linux的iptables来对数据包的目的地址进行改写来达到转发的目的,而所谓的ServiceIP只是转发记录里的一个虚拟地址。
K8S会在集群的每一个节点运行一个叫kube-proxy的Pod,这个Pod负责监听api-server中的Service/EndPoint/Node类型的资源变化事件,然后操作本机的iptables或ipvs来创建ServiceIP,所以K8S集群的节点之外的其它节点是不认识这个ServiceIP的。ServiceIP由控制器从Service网段分配(默认为10.96.0.0/16),在集群安装时可以通过修改service-cidr来指定网段。
现在k8s实现ServiceIP主要通过iptables或ipvs的方式,在这我们主要介绍iptables的方式,让我们先简单介绍一下iptables。
即使是使用了ipvs,也是要通过iptables来实现SNAT的,ipvs目前只负责DNAT,然后数据包回来还是要借助本机的conntrack系统来把包回给最初的发送方。
iptables
iptables是一款使用很广泛的linux防火墙工具,当前主流的linux发行版基本上都默认集成了iptables,它在用户态提供一些简单的命令和用户进行交互,使用户可以轻松地设置一些对数据包的过滤或修改的规则,然后把规则设置到内核的netfilter子系统的hook函数上,达到对数据包进行高效地过滤与转发的目的。iptables提供了丰富的模块来完成数据包的匹配和修改,同时也提供了相关的接口用来扩展新的模块。
先来了解一下iptables的命令:
[[email protected] ~]# iptables --help
iptables v1.4.21
Usage: iptables -[ACD] chain rule-specification [options]
iptables -I chain [rulenum] rule-specification [options]
iptables -R chain rulenum rule-specification [options]
iptables -D chain rulenum [options]
iptables -[LS] [chain [rulenum]] [options]
iptables -[FZ] [chain] [options]
iptables -[NX] chain
iptables -E old-chain-name new-chain-name
iptables -P chain target [options]
iptables -h (print this help information)
-A 新增一条规则
-I 插入一条规则
-D 删除指定的规则
-N 新建一条用户自定义链
-L 展示指定的链上的规则
-F 清除链上的所有规则或所有链
-X 删除一条用户自定义链
-P 更改链的默认策略
-t 指定当前命令操作所属的表,如果不指定,则默认为filter表
先来简单看几个iptables的命令:
下面的命令会把本机去往192.168.8.163的数据包转到192.168.8.162上:
iptables -A OUTPUT -t nat -d 192.168.8.163 -j DNAT --to-distination 192.168.8.162
下面的命令则会把所有不是来自192.168.8.166的数据包都拦截掉,就是防火墙的效果:
iptables -A INPUT -t filter ! -s 192.168.8.166 -j DROP
下面的命令可以把来自192.168.6.166并访问本机80端口的tcp请求放过:
iptables -A INPUT -t filter -s 192.168.8.166 -p tcp --dport 80 -j ACCEPT
下面的命令会把去往8.8.8.8的流量都进行源地址转换:
iptables -A POSTROUTING -t nat -d 8.8.8.8 -j MASQUERADE
上面的第一条命令中我们把规则建在了叫OUTPUT的链上,这个链只会影响从主机的应用层发出的数据包,而不会影响从主机的容器发送出来的数据包,如果要想让主机的容器也应用这个规则,要在PREROUTING链上建规则:
iptables -A PREROUTING -t nat -d 192.168.8.163 -j DNAT --to-distination 192.168.8.162
上面提到的PREROUTING、INPUT、OUTPUT、POSTROUTING链,这些链代表的其实是linux的netfilter子系统上不同的hook函数处理点,一共有五个点,分别在数据包进入本机,进入传输层,通过本机转发,最终流出本机的过程中“埋伏”着,对经过的数据包进行各种“调戏”,iptables一共内置有五个链,与netfilter的hook点是一一对应的,主要有:
1. PREROUTING:数据包刚到达时会经过这个点,通常用来完成DNAT的功能。
2. INPUT:数据包要进入本机的传输层时会经过这个点,通常用来完成防火墙入站检测的功能。
3. FORWARD:数据包要通过本机转发时会经过这个点,通常用来过滤数据包。
4. OUTPUT:从本机的数据包要出去的时候会经过这个点,通常用来做DNAT和包过滤。
5. POSTROUTING:数据包离开本机前会经过这个点,通常用来做SNAT。
这些链在协议栈的位置如下图所示:
示例:
· 数据包从节点外进入节点的传输层会经过的链:PREROUTING -> INPUT
· 数据包从节点应用发到其它节点: OUTPUT -> POSTROUTING
· 节点上的容器发送的数据包去节点外:PREROUTING -> FORWARD -> POSTROUTING
所以通常我们会在OUTPUT和PREROUTING链上对经过的数据包进行目标地址转换,就是常说的DNAT,因为本机出的包和容器出的包会在这两个链经过;而在POSTROUTING上对数据包进行源地址转换,就是常说的SNAT,因为最终出去的数据包都会经过这个链。
还有上面的命令中出现在-t后是iptable的表(如:nat表、filter表),iptables使用表来组织规则,根据规则的功能,把规则放在不同的table;如果是做SNAT或DNAT,则放在nat表;如果是做包的过滤则放在filter表;在表的内部,每一条规则都被组织成链,被依附在iptables内置的链上,iptables一共内置了5个tables:filter、nat、mangle、raw、security,在本文中我们重点关注nat表。
模拟k8s 服务IP
下面我们先抛开K8S,直接用iptables命令来模拟一个k8s的serviceIP,假设有一个服务叫KUBE-SVC1,服务IP为10.96.10.10,后面指向两个pod为10.244.1.3和10.244.1.4,我们一步步来模拟这个serviceIP:
在上面我们已经演示了要把目标地址从A转到B,现在是要求A转到B1、B2,会稍微复杂一点。
第一步,新建一个自定义链叫KUBE-SERVICES,然后在OUTPUT和PREROUTING链上把流量引过来:
iptables -N KUBE-SERVICES -t nat
iptables -A OUTPUT -t nat -j KUBE-SERVICES
iptables -A PREROUTING -t nat -j KUBE-SERVICES
OUTPUT链的规则影响本机应用层出来的数据包,PREROUTING链的规则影响从容器经过本机出外面的数据包,之所以先建一个KUBE-SERVICES链是为了不用把相同的规则在OUTPUT和PREROUTING上重复建一次。
第二,再新建一个叫KUBE-SVC1的链,把KUBE-SERVICES中的流量去往10.96.10.10的引到KUBE-SVC1
iptables -N KUBE-SVC1 -t nat
iptables -A KUBE-SERVICES -t nat -d 10.96.10.10 -j KUBE-SVC1
第三步,把KUBE-SVC1的流量分发给两个IP:
iptables -A KUBE-SVC1 -t nat -m statistic --mode nth --every 2 --packet 0 -j DNAT --to-destination 10.244.1.3
iptables -A KUBE-SVC1 -t nat -m statistic --mode nth --every 1 --packet 0 -j DNAT --to-destination 10.244.1.4
然后每当新建一个K8S的服务,就是重复执行二三步的步骤,这其实就是k8s的kube-proxy大概会做的事情,不一定完全一样,比如上面的第三步,kube-proxy会再建两条以KUBE-SEP开头的链,我们为了演示简单就省了,而且我们在上面模拟的IP是可以ping得通的,但K8S模拟的serviceIP如果用的是iptables则是ping不通的(IPVS的话应该是可以ping得通的),主要是因为k8s是在二步有一点点不同,它会多匹配一个协议:
iptables -N KUBE-SVC1 -t nat
iptables -A KUBE-SERVICES -d 10.96.10.10 -m tcp -p tcp -j KUBE-SVC1
模拟K8S NodePort
NodePort与ClusterIP的区别是匹配端口,端口也是由控制器分配并保存到Service的元数据上再由kube-proxy监听到变化的事情写到每个节点的iptables的。
下面我们来模拟一下,与上面的第一三步不变,第二步不同,先会新建一个叫KUBE-NODEPORTS的链,然后把KUBE-SERVICES链中,凡是请求本机地址的都转给这个新建的链:
iptables -N KUBE-NODEPORTS -t nat
iptables -A KUBE-SERVICES -t nat -m addrtype --dst-type LOCAL -j KUBE-NODEPORTS
接着像上面的第二步一样新建一个KUBE-SVC1的链,把端口为33888的流量转给这个新的链:
iptables -N KUBE-SVC1 -t nat
iptables -A KUBE-NODEPORTS -t nat -m tcp -p tcp --dport 33888 -j KUBE-SVC1
这时候如果要开多个端口指向一个服务,就是重复上面的两句命令。
经过上面一轮操作,此时的链的示意图如下:
此时如果新增一个服务KUBE-SVC2的话,变化如下(KUBE-NODEPORTS到KUBE-SVC2的链在创建的服务类型为NodePort时才会有):
为了使数据包能够尽量正常地处理与转发,iptables上的规则创建会有一些限制,例如我们不能在POSTROUTING链上创建DNAT的规则,因为在POSTROUTING之前,数据包要进行路由判决,内核会根据当前的目的地选择一个最合适的出口,而POSTROUTING链的规则是在路由判决后发生,在这里再修改数据包的目的地,会造成数据包不可到达的后果,所以当我们用iptables执行如下命令时:
iptables -A POSTROUTING -t nat -d 192.168.8.163 -j DNAT --to-distination 192.168.8.162
iptables:Invalid argument. Run `dmesg` for more information.
//执行dmesg命令会看到iptables提示:DNAT模块只能在PREROUTING或OUTPUT链中使用
x_tables:iptables:DNAT target:used from hooks POSTROUTING,but only usable from PREROUTING/OUTPUT
kube-proxy有几种模式,在iptables或ipvs模式下,kube-proxy已经不参与数据包的转发了,只负责设置iptables/ipvs规则,并且定期恢复误删的规则(所以如果不小心误删了iptables规则,只需要建个Service,就会触发kube-proxy恢复iptables规则),哪怕是在运行过程中kube-proxy挂了也不会影响已经有service的运行,只是新建的Service没效果。
总结
我们先简单介绍了iptables的一些命令,然后用iptables模拟了k8s的kube-proxy在新建一个ClusterIP或NodePort时大概会完成的事情,借此来加深我们对kube-proxy的功能了解和K8S中的ServiceIP有个理性的认识。iptables在服务非常多的时候会创建很多的链,流量在转发时可能需要在这些链上频繁地遍历,造成性能下降,所以除了iptables,现在很多生产的k8s开始用ipvs来解决DNAT的问题,ipvs用hash表来匹配转发规则,性能会比iptables好,但在服务量不大的时候这种差距并不明显。
如若转载,请注明出处:https://www.ozabc.com/keji/363627.html