滴普技术荟 - 云原生基座 OpenKube 开放容器实践(九):K8S 的 ServiceIP 实现原理
当我们在 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 的命令:
[root@worker2 ~]# 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.deepexi.com/bbs/developer-bbs
版权声明: 本文为 InfoQ 作者【滴普科技2048实验室】的原创文章。
原文链接:【http://xie.infoq.cn/article/c3fec830637ad832b6cdab5db】。文章转载请联系作者。
评论