背景

在单个网卡上通过 ip addr 命令为网卡配置 VIP 时,无法通过 VIP 访问 NodePort 服务,网卡的信息如下:

1
2
3
4
5
6
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 提及当前 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),其代码实现等价于以下命令:

1
2
3
4
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 绑定的地址,配置如下:

1
2
3
4
5
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() 方法中实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
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 无法绑定的情况。

参考