在 iptables 工作模式下,iptables 中 KUBE-SEP-XXX 链上规则和 Pod 数量成正比,当集群规模增大(10000 个 Pod 以上)时每个 k8s 节点上 iptables 规则会快速上升,从而影响集群 Service 的连接速度以及 CPU 资源消耗。
为了应对大集群中 iptables 模式的性能问题,kubernetes v1.8 中引入了 ipvs 模式。ipvs 模式在 v1.9 中处于 beta 阶段,在 v1.11 中已经正式可用了。
ipvs 和 iptables 都是基于 netfilter,但 ipvs 具有以下优势:
ipvs 模式中参与数据转发的内核工具包括:
ipvs 两个部分组成,分别是用户空间的管理工具 ipvsadm 以及运行在内核中 ipvs 模块。
如果抛开细节 kube-proxy 在 ipvs 模式下的工作实际上非常简单,下面我们模仿 kube-proxy 在一个节点上创建一个 VIP。通过这个这个 VIP,我们可以负载均衡一个 kubernetes 集群的 kube-apiserver。
主机 A 的地址为 172.24.37.57,vip 为 10.103.97.2:6789,kube-apiserver 地址包括以下三个:
在主机 A 上通过 ipvsadm 创建 ipvs 服务,并关联 Real Server(RS):
# 创建 server
$ ipvsadm -A -t 10.103.97.2:6789 -s rr
# 管理 Real Service
$ ipvsadm -a -t 10.103.97.2:6789 -r 172.28.126.39:6443 -m
$ ipvsadm -a -t 10.103.97.2:6789 -r 172.28.126.40:6443 -m
$ ipvsadm -a -t 10.103.97.2:6789 -r 172.28.126.41:6443 -m
# 添加NAT
$ iptables -t nat -A POSTROUTING -m ipvs --vaddr 10.103.97.2 --vport 6789 -j MASQUERADE
# 绑定VIP到一张 dummy 网卡
$ ip link add ipvs0 type dummy
$ ip addr add 10.103.97.2/32 dev ipvs0
# 开启 contrack
$ echo 1 > /proc/sys/net/ipv4/vs/conntrack
通过上述命令,即可实现主机 A 上对三个 kube-apisever 的负载均衡访问。
kube-proxy 通过 kube-apiserver 获取集群中所有的 Service 和 Endpoint 信息,在每个节点上创建 ipvs service/real server。
❝ipvs 包含 tunnel(ipip)、direct、nat 三中工作模式,kube-proxy 基于 nat 模式工作。参考:https://blog.csdn.net/qq_15437667/article/details/50644594
了解了 kube-proxy 的工作原理,我们可以稍稍挖掘一些细节。
ipvs 基于 netfilter 框架工作,工作原理同样基于在 TCP/IP 协议栈中注入钩子函数,那么 ipvs 的 hook 函数在 TCP/IP 协议栈的那些位置?
为了探明真相我在网上查了非常多的资料,但是并没有发现找到非常满意的答案。
绝大多数文章中 ipvs 在 NF_INET_FORWARD、NF_INET_FORWARD、NF_INET_POST_ROUTING 中添加了 hook 函数,如下图:
❝参考-1:https://github.com/liexusong/linux-source-code-analyze/blob/master/lvs-principle-and-source-analysis-part2.md 参考-2:https://blog.csdn.net/raintungli/article/details/39051435
上述文章都只分析了接收数据包的场景,并没有分析发送数据包的场景(即在 ipvs 网关机器上访问 vip)。
结合访问 service 的几种情形,我们可以梳理出以下场景:
id | Src | Dst | 经过的链路 |
---|---|---|---|
1 | k8s node | ClusterIP/NodePort | OUTPUT -> POSTROUTING ??? |
2 | 集群外 node | NodePort | PREROUTING -> INPUT -> ipvs nat -> POSTROUTING |
3 | Pod | NodePort/ClusterIP | PREROUTING -> INPUT -> ipvs nat -> POSTROUTING |
kube-proxy 在 node 上创建了一张 dummy 网卡(kube-ipvs0),使所有访问 ClusterIP 数据包能够在 INPUT Chain 上被 DNAT,上述场景中 2、3 正式这种情况。
场景 1 中数据包从本机 OUTPUT 发出并没有经过 INPUT 发生 DNAT,但实际情况是在 k8s node 主机上直接通过 ClusterIP/ 本机 IP:NodePort,依然是可以正常访问到 Pod 和实际不符。
查阅了 Linux Master 分支上的最新内核代码[1],发现在 2010 年 ipvs 已经移除了 NF_INET_POSTROUTING HOOK 点,并且在 NF_INET_LOCAL_OUT 添加了新 HOOK 用于支持 IPVS 的 LocalNode[2] 功能。
因此上述场景 1 中,我的疑问或许可以通过这个改动得以解释,即 OUTPUT 链上或许 ipvs 同样具有 dnat 能力!
❝提交记录:https://github.com/torvalds/linux/commit/cf356d69db0afef692cd640917bc70f708c27f14,https://github.com/torvalds/linux/commit/cb59155f21d4c0507d2034c2953f6a3f7806913d
在 ipvs 模式下,kube-proxy 依然使用 iptables 进行 SNAT/MASQUERADE,并且借助 ipset 工具通过 hash 的方式快速识别访问 Service。在这种情况下,kube-proxy 在 iptables 中注入的规则数量是固定不变的包括以下:
数据包入口:OUTPUT/PREROUTING,流量被导入到自定义链 KUBE-SERVICES。
Chain OUTPUT (policy ACCEPT)
target prot opt source destination
KUBE-SERVICES all -- 0.0.0.0/0 0.0.0.0/0 /* kubernetes service portals */
Chain PREROUTING (policy ACCEPT)
target prot opt source destination
KUBE-SERVICES all -- 0.0.0.0/0 0.0.0.0/0 /* kubernetes service portals */
自定义链 KUBE-SERVICES 中数据包被进行分类,其中第一条规则和第三条规则中 match-set KUBE-CLUSTER-IP dst,dst 的含义就是数据包 dst 包含在 ipset 的 KUBE-CLUSTER-IP 字典中时命中改规则,此时流量在 KUBE-MARK-MASQ 链中被打上 MASQUERADE 标记,然后直接进入 INPUT/OUTPUT chain 中的 ipvs 进行 DNAT。
其余访问本地网卡的流量,则进入 KUBE-NODE-PORT 链。
Chain KUBE-SERVICES (2 references)
target prot opt source destination
KUBE-MARK-MASQ all -- !10.233.64.0/18 0.0.0.0/0 /* Kubernetes service cluster ip + port for masquerade purpose */ match-set KUBE-CLUSTER-IP dst,dst
KUBE-NODE-PORT all -- 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL
ACCEPT all -- 0.0.0.0/0 0.0.0.0/0 match-set KUBE-CLUSTER-IP dst,dst
Chain KUBE-MARK-MASQ (3 references)
target prot opt source destination
MARK all -- 0.0.0.0/0 0.0.0.0/0 MARK or 0x4000
KUBE-NODE-PORT 链主要用于处理 externalTrafficPolicy 逻辑,其中 KUBE-NODE-PORT-LOCAL-TCP 字典记录 Local 模式的 NodePort, 而 KUBE-NODE-PORT-TCP 字典记录了全量 NoderPort。可以看到 Local 模式的数据包没有经过 KUBE-MARK-MASQ 即没有标记 MASQUERADE 。
访问 NodePort 流量会经过 OUTPUT/PREROUTING 上的其他规则,最后进入 ipvs 进行 DNAT。
❝PS:iptables 模式中由于 DNAT 在 iptables 上完成后直接进入 next chain,因此访问 NodePort 的数据包无法走完整的 PREROUTING、OUTPUT chain 导致 firewalld 防火墙规则失效,而在 ipvs 模式下并不存在这种问题。
Chain KUBE-NODE-PORT (1 references)
target prot opt source destination
RETURN tcp -- 0.0.0.0/0 0.0.0.0/0 /* Kubernetes nodeport TCP port with externalTrafficPolicy=local */ match-set KUBE-NODE-PORT-LOCAL-TCP dst
KUBE-MARK-MASQ tcp -- 0.0.0.0/0 0.0.0.0/0 /* Kubernetes nodeport TCP port for masquerade purpose */ match-set KUBE-NODE-PORT-TCP dst
经过 ipvs 转发的数据包,最后由 POSTROUTING 进入 MASQUERADE 。
❝A 访问 B 时,如果数据包(SRC:A,DST:B)在 B 上发生了 DNAT(SRC:A,DST:C),则 C 发出的应答包为(SRC:C,DST:A),由于 C 可能和 A 不是同一个子网因此包可能无法送达。 上述场景中需要进行 SNAT,整个流程变为 (SRC:A,DST:B)-> B(DNAT and SNAT) -> (SRC:B,DST:C) -> C 应答 ->(SRC:C,DST:B)-> B ->(SRC:B,DST:A) -> A
Chain POSTROUTING (policy ACCEPT)
target prot opt source destination
KUBE-POSTROUTING all -- 0.0.0.0/0 0.0.0.0/0 /* kubernetes postrouting rules */
Chain KUBE-POSTROUTING (1 references)
target prot opt source destination
MASQUERADE all -- 0.0.0.0/0 0.0.0.0/0 /* Kubernetes endpoints dst ip:port, source ip for solving hairpin purpose */ match-set KUBE-LOOP-BACK dst,dst,src
RETURN all -- 0.0.0.0/0 0.0.0.0/0 mark match ! 0x4000/0x4000
MARK all -- 0.0.0.0/0 0.0.0.0/0 MARK xor 0x4000
MASQUERADE all -- 0.0.0.0/0 0.0.0.0/0 /* kubernetes service traffic requiring SNAT */
将上述流程绘制成图如下:
在单个网卡上通过 ip addr 命令为网卡配置 VIP 时,无法通过 VIP 访问 NodePort 服务,网卡的信息如下:
2: rcosmgmt: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
link/ether fa:16:3e:d6:d2:99 brd ff:ff:ff:ff:ff:ff
inet 172.28.110.16/24 brd 172.16.1.255 scope global eth0
valid_lft forever preferred_lft forever
inet 172.28.110.7/24 scope global secondary eth0
valid_lft forever preferred_lft forever
进一步定位发现 kube-proxy 没有为 VIP(172.28.110.7) 地址创建 ipvs 服务。此时通过 curl 命令访问 vip:NodePort 卡死,但 tcpdump 测试发现报文传输正常。
造成上述奇怪现象的原因是:数据包没有经过 ivps dnat,直接发送到了 kube-proxy 的监听端口。
❝kube-proxy 为每个 NodePort 创建对应的监听端口,该端口仅用来标记 NodePort 端口已经被使用。
官方 issues/75443[3] 提及当前 ipvs 模式下,kube-proxy 获取本地网卡的 ip 地址时,无法获取辅助 IP,即被标记为 scope global secondary ip 地址。
该 ISSUES 中最后给出的规避方案是将 VIP 的子网掩码设置为 32。
Linux 内核中,通过 ip 命令在设备上添加 primary ip 同网段的 ip 时,这些 ip 显示为 secondary ip ,并且通过 ifconfig 命令无法查看到。如果添加的 ip 不是同网段的,那么都作为 primary ip。
kube-proxy 默认情况下,将 NodePort 绑定到本地路由表的 src 地址(即以这个 ip:NodePort 创建 ipvs-service),其代码实现等价于以下命令:
$ ip route show table local type local proto kernel | grep -v kube-ipvs0
local 172.28.110.7 dev rcosmgmt scope host src 172.28.110.16 # 172.28.110.7 是 secondary ip,即 vip
local 172.28.110.16 dev rcosmgmt scope host src 172.28.110.16 # 172.28.110.16 是 primary ip
从上述输出可以看出,rcosmgmt 虽然都有两条路由记录,但是 src 地址均是 primary ip,因此 kubeproxy 没有绑定 NodePort 到 secondary ip(VIP)。
❝PS:个人分析这是 ip 命令的实现造成的,具体为啥这样可能需要更深入分析。
仔细阅读 kube-proxy 源码后,发现还有更好解决方案。
用户可以在 kube-proxy 配置文件中添加 nodePortAddresses 配置,使 kube-proxy 直接从网卡配置的 ip 上获取 NodePort 绑定的地址,配置如下:
apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
nodePortAddresses:
- "127.0.0.1/8"
- "172.28.110.0/24"
上述配置,kube-proxy 会获取网卡所有配置的 ip 并选择属于 “172.28.110.0/24” 和 “127.0.0.1/8” 的地址来绑定 NodePort ,这种情况下 secondary ip 也能正常绑定。
kube-proxy 更新 ipvs 规则的逻辑在 pkg/proxy/ipvs/proxier.go 文件中, Proxier 类的 syncProxyRules() 方法中实现:
func (proxier *Proxier) syncProxyRules() {
// 省略其他逻辑 ...
// 当定义了 NodePort 时,获取本地 IP
if hasNodePort {
// 通过 GetNodeAddresses 接口获取本地 ip,该方法直接获取设备上配置的 ip 地址,并判断对应的 ip 是否包含在 NodePortAddresses 定义的网段中,如果是将 ip 加入返回的 list 中。
// 以下情况返回结果会插入 0.0.0.0/0,此时后续逻辑会再次通过路由表获取本地ip
// 1. 当 NodePortAddresses 为空或者定义了 0.0.0.0/0
// 2. 所有设备的 ip 都没有包含在 NodePortAddresses 中的 cidr 中
nodeAddrSet, err := utilproxy.GetNodeAddresses(proxier.nodePortAddresses, proxier.networkInterfacer)
if err != nil {
klog.Errorf("Failed to get node ip address matching nodeport cidr: %v", err)
} else {
nodeAddresses = nodeAddrSet.List()
for _, address := range nodeAddresses {
// ipGetter.NodeIPs() 获取本地 ip,该方法通过本地路由表获取 ip。
if utilproxy.IsZeroCIDR(address) {
nodeIPs, err = proxier.ipGetter.NodeIPs()
if err != nil {
klog.Errorf("Failed to list all node IPs from host, err: %v", err)
}
break
}
nodeIPs = append(nodeIPs, net.ParseIP(address))
}
}
}
// Build IPVS rules for each service.
for svcName, svc := range proxier.serviceMap {
// 省略其他逻辑 ...
if svcInfo.NodePort() != 0 {
// 省略其他逻辑 ...
// 为每个 nodeIP:NodePort 创建 svc
for _, nodeIP := range nodeIPs {
// ipvs call
serv := &utilipvs.VirtualServer{
Address: nodeIP,
Port: uint16(svcInfo.NodePort()),
Protocol: string(svcInfo.Protocol()),
Scheduler: proxier.ipvsScheduler,
}
}
// 省略其他逻辑 ...
}
}
}
kube-proxy 可以通过 nodePortAddresses 选择 NodePort 绑定在那些本地 ip 上。当该值为空时 kube-proxy 回退到通过本地路由表来选择 NodePort 绑定的 ip,此时可能出现辅助 ip 无法绑定的情况。
当使用 Kube-Proxy ipvs 模式时,在 kubernetes 节点上无法使用 127.0.0.1:NodePort 访问服务,而在 Kube-Proxy iptables 模式时该问题并不存在。
按照官方 issues/67730[4] 的分析,原因是:内核 crosses_local_route_boundary 函数进行路由合法性检查失败,数据包被丢弃。
如果有使用 127.0.0.1 访问 NodePort 的需求可以使用 iptables 模式。kube-proxy 配置 iptables 模式时,会自动开启内核的 route_localnet 参数:
route_localnet - BOOLEAN
Do not consider loopback addresses as martian source or destination
while routing. This enables the use of 127/8 for local routing purposes.
default FALSE
开启上述配置后,linux 将支持 127.0.0.1 地址的路由,并正常进行 NAT 地址转换。
来源(版权归原作者所有):https://lqingcloud.cn/post/kube-proxy-02/