Hi!请登陆

云原生基座OpenKube开放容器实践九:K8S的ServiceIP实现原理

2021-2-8 39 2/8

当我们在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好,但在服务量不大的时候这种差距并不明显。

相关推荐