前段时间我们 K8s
集群遇到一个网络问题,一直没空整理,现在终于可以理一理了。
环境描述
K8s version: v1.20
网络方案:Canal + macvlan
Pod CIDR: 192.168.0.0/16
发现问题
集群里有一个 node
节点,访问不了本节点启用 macvlan
网络的容器。
- 本节点其它容器可以访问这部分容器
- 其它节点可以访问这部分容器
- 其它节点上的容器也可以访问这部分容器
例如,这个 node
节点有一个启用 macvlan
网络的容器 ,flannel
网卡(eth0
) ip
是 192.168.8.42
,在宿主节点上 ping 192.168.8.42
无法 ping
通。
- 在宿主节点访问
# ping 192.168.8.42
PING 192.168.8.42 (192.168.8.42) 56(84) bytes of data.
^C
--- 192.168.8.42 ping statistics ---
112842 packets transmitted, 0 received, 100% packet loss, time 115549205ms
- 在节点的其它容器访问
/ # ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
3: eth0@if17780: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1450 qdisc noqueue
link/ether 46:ba:74:99:47:7f brd ff:ff:ff:ff:ff:ff
inet 192.168.8.43/32 brd 192.168.8.43 scope global eth0
valid_lft forever preferred_lft forever
/ # ping 192.168.8.42
PING 192.168.8.42 (192.168.8.42): 56 data bytes
64 bytes from 192.168.8.42: seq=0 ttl=63 time=0.118 ms
64 bytes from 192.168.8.42: seq=1 ttl=63 time=0.042 ms
- 在其它节点的容器访问
# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
3: eth0@if375779: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default
link/ether 2e:35:84:1a:e7:08 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 192.168.17.245/32 brd 192.168.17.245 scope global eth0
valid_lft forever preferred_lft forever
[root@rancher-dg-tn7 ~]# ping 192.168.8.42
PING 192.168.8.42 (192.168.8.42) 56(84) bytes of data.
64 bytes from 192.168.8.42: icmp_seq=1 ttl=62 time=0.257 ms
64 bytes from 192.168.8.42: icmp_seq=2 ttl=62 time=0.164 ms
这场景挺奇怪的,容器间能互通,其它节点也能通,宿主节点却不能访问。
初步判断跟网络插件关系不大,不然就是整个节点的容器都不能访问了,网络问题就是让人头大。
排查思路
- 先来看看节点上的路由,会不会是节点路由表上没有到容器的路由明细。
# route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 10.xx.xx.1 0.0.0.0 UG 0 0 0 ens192
10.xx.xx.0 0.0.0.0 255.255.255.0 U 0 0 0 ens192
172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker0
...
...
192.168.8.40 0.0.0.0 255.255.255.255 UH 0 0 0 cali8f6e7372819
192.168.8.42 0.0.0.0 255.255.255.255 UH 0 0 0 califda2d1a1007
这么一看,节点上的路由没什么问题,节点访问 192.168.8.42
这个容器时数据包直接通过 califda2d1a1007
这个虚拟设备进入容器。
接着尝试对这个设备进行抓包,验证一下内核数据包是不是进入容器。
# tcpdump -i califda2d1a1007 icmp
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on califda2d1a1007, link-type EN10MB (Ethernet), capture size 262144 bytes
14:18:06.368393 IP 172.17.0.1 > 192.168.8.42: ICMP echo request, id 3225, seq 1400, length 64
14:18:07.392402 IP 172.17.0.1 > 192.168.8.42: ICMP echo request, id 3225, seq 1401, length 64
^C
通过这个抓包可以发现两个问题:
- 数据包确实通过
califda2d1a1007
设备进入容器了,但容器没有数据包响应。 - 发起
icmp request
的源IP
为什么是172.17.0.1
? 这可是节点docker0
的IP
啊。
# ifconfig docker0
docker0: flags=4099<UP,BROADCAST,MULTICAST> mtu 1500
inet 172.17.0.1 netmask 255.255.0.0 broadcast 172.17.255.255
- 分析容器
net namespace
进入容器 net namespace
,查看网络结构,如前面贴出来的我们的网络架构图,这个容器有两张网卡,一张是 eth0
(flannel
),另外一张是 eth1
(macvlan
)
# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
3: eth0@if17779: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default
link/ether 02:96:6c:66:6e:51 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 192.168.8.42/32 brd 192.168.8.42 scope global eth0
valid_lft forever preferred_lft forever
4: eth1@if17778: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether aa:b3:23:7f:75:07 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 10.xx.xx.203/24 brd 10.xx.xx.255 scope global eth1
valid_lft forever preferred_lft forever
接着对容器的 eth0
网络进行抓包,可以看到跟在节点上对 califda2d1a1007
抓包的结果一致。
# tcpdump -nn -i eth0 icmp
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
14:53:01.472399 IP 172.17.0.1 > 192.168.8.42: ICMP echo request, id 3225, seq 3446, length 64
14:53:02.496402 IP 172.17.0.1 > 192.168.8.42: ICMP echo request, id 3225, seq 3447, length 64
^C
再看看容器的路由,默认路由是 macvlan
的网卡 eth1
,Pod CIDR
的路由指向 eth0
, 172.23.0.0
是集群 Service CIDR
,也是指向 eth0
,这都是正确的。唯独没有指向 172.17.0.1
这个节点 docker0
的路由,这也能就是前面抓包我们看到的结果,解析了为什么在宿主 node
节点 ping
不通这个容器。
# route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 10.xx.xx.1 0.0.0.0 UG 0 0 0 eth1
10.xx.xx.0 169.254.1.1 255.255.255.0 UG 0 0 0 eth0
10.xx.xx.0 0.0.0.0 255.255.255.0 U 0 0 0 eth1
169.254.1.1 0.0.0.0 255.255.255.255 UH 0 0 0 eth0
172.23.0.0 169.254.1.1 255.255.128.0 UG 0 0 0 eth0
192.168.0.0 169.254.1.1 255.255.0.0 UG 0 0 0 eth0
尝试给这个容器增加一条指向宿主节点 docker0
的路由,果然网络就通了。
route add -net 172.17.0.0 netmask 255.255.0.0 eth0
# route del -net 172.17.0.0 netmask 255.255.0.0
分析原因
现在知道了宿主节点访问不到这个容器是因为缺少路由,但同时也带来了新的问题。
为什么宿主节点访问本节点容器的时候使用的 IP
是 docker0
的 IP
,而不是节点本身的物理 IP
?
回头看一下宿主节点本身的网络信息:
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
2: eno1: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
link/ether 34:73:79:14:ef:88 brd ff:ff:ff:ff:ff:ff
3: eno2: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
link/ether 34:73:79:14:ef:89 brd ff:ff:ff:ff:ff:ff
4: ens2f0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq master ens192 state UP group default qlen 1000
link/ether 04:3f:72:bb:a1:b6 brd ff:ff:ff:ff:ff:ff
5: ens2f1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq master ens224 state UP group default qlen 1000
link/ether 04:3f:72:bb:a1:b7 brd ff:ff:ff:ff:ff:ff
6: ens3f0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq master ens192 state UP group default qlen 1000
link/ether 04:3f:72:bb:a1:b6 brd ff:ff:ff:ff:ff:ff
7: ens3f1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq master ens224 state UP group default qlen 1000
link/ether 04:3f:72:bb:a1:b7 brd ff:ff:ff:ff:ff:ff
10: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
link/ether 02:42:f8:95:c1:b7 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
valid_lft forever preferred_lft forever
11: ens192: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
link/ether 04:3f:72:bb:a1:b6 brd ff:ff:ff:ff:ff:ff
inet 10.xx.xx.38/24 brd 10.xx.xx.255 scope global ens192
valid_lft forever preferred_lft forever
12: ens224: <BROADCAST,MULTICAST,PROMISC,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
link/ether 04:3f:72:bb:a1:b7 brd ff:ff:ff:ff:ff:ff
-
ens192
是节点真实IP
的网卡,flannel
网络跨节点通讯也是这个这个网卡,它是一个activebackup
类型的teamd
。 -
ens224
也是一个teamd
,用于macvlan
网络通讯。 -
ens3f0
这些是真正的物理网卡。
看宿主节点网络信息发现一个比较有趣的现象,docker0
前面的数字是要比 ens192/ens224
这些 teamd
网卡要小的。网卡前面的这个数字,在 Linux
上是 ifindex
,在路径 /sys/class/net/docker0/ifindex
可以查到。到这里,我们猜想宿主节点访问本节点容器时使用 docker0
的 IP
跟这个 ifindex
相关:ifindex
越小,它的优先级更高。
为了验证这个想法,我们想过去修改 ifindex
,使得 ens192
的 ifindex
比 docker0
的更小。但找不到修改方法,最后查阅了一些内核文档,得知这个 ifindex
是由一个内核函数 dev_new_index()
分配的,它的分配顺序依赖硬件检测、驱动注册、服务启动的先后。
既然修改 ifindex
这个思路不可行,那我们可以反向验证,找一个 “正常” 节点,在宿主节点上可以访问本节点的所有容器,包括启用了 macvlan
的容器。
接着,然后重启节点的网络服务。
根据
dev_new_index()
的原理,ens192/ens224
这两个teamd
必然会分配一个新的ifindex
且大于docker0
的重启节点
networking
服务前
# cat /sys/class/net/ens192/ifindex
10
# cat /sys/class/net/ens224/ifindex
11
# cat /sys/class/net/docker0/ifindex
12
- 重启节点
networking
服务后
# systemctl restart network
# cat /sys/class/net/ens192/ifindex
17774
# cat /sys/class/net/ens224/ifindex
17775
# cat /sys/class/net/docker0/ifindex
12
- 如实验前设想,重启
networking
服务后,宿主节点开始无法访问本节点已启用macvlan
网络的容器,其它不受影响。
解决方案
在我们的容器网络规划里,docker
自带的这个 bridge
网络我们并没有用到,所以我们的路由规则也没有加上这个指向。可以说,在这种隐藏的场景下,这是一条被遗忘的路由。
总结一下前面的思路,这个问题是由于节点运行一段时间之后重启过网络服务引起的,解决这个问题,有几个方案:
-
添加临时路由
- 直接在容器上
route add
添加一条路由即可,这个方案容器一旦重启就会失效。
- 直接在容器上
-
重启宿主节点
- 重启宿主节点可以让节点网络信息重新初始化,包括网卡的
ifindex
,但这个方案也是不保险,一旦有需要重启networking
服务,就还是会复现这个场景。
- 重启宿主节点可以让节点网络信息重新初始化,包括网卡的
-
给
macvlan controller
增加路由规则,然后重启容器。- 这个方案较为完整,既然是因为缺少路由引起的,那在控制器上把路由规则完善即可,若不希望重启容器,还可以给容器添加临时路由。容器重启后也会获取到控制器下发的最新路由。