最近在部署一个集群环境的时候,发现集群中一个子节点与其他子节点不通,而 master 节点可与任何子节点互通,通过抓包排查后,发现是 Linux 路由决策导致的。因此,在此记录下来,希望对大家有所帮助。
1、环境及现象
1.1、环境
- K8s v1.20(一个 Master 和两个 Worker)、calico v3.21.1
- Master:openEuler 24.03 (LTS-SP1)、网卡 enp1s0、IP(172.16.13.95)
- Node2:openEuler 24.03 (LTS-SP1)、网卡 enp1s0、IP(172.16.12.36)
- Node3:openEuler 24.03 (LTS-SP1)、网卡 eno1、IP(172.16.12.117)
1.2、现象
- Master 可以 ping 通 Node2 和 Node3 上的 Pod, Node2 和 Node3 不能互相 ping 通对方的 Pod
PS:这里有一个小坑,看到网卡可能就会猜测是 Node3 有问题,因为 Node3 的网卡名称和 Master/Node2 不一样,有经验的小伙伴可能会想到去给 daemonset/calico-node 添加环境变量 IP_AUTODETECTION_METHOD。当然,我就踩了这个坑,配置后还是没有解决。
kubectl set env daemonset/calico-node -n kube-system IP_AUTODETECTION_METHOD=interface=^((bond|ens|eth|enp|em|eno1).*)...env:- name: IP_AUTODETECTION_METHODvalue: interface=^((bond|ens|eth|enp|em|eno1).*)
...
2、排查
2.1、抓包
- 配置了 calico-node 的环境变量后还是没有解决,然后就开始抓包看到底是哪一步不通的
- Node2(172.16.12.36)上 PodA:10.245.102.188
- Node3(172.16.12.117)上 PodB:10.245.13.23
# 举例:node1 上的 podA ping node2 上的 podB# ICMP 包流量方向(IPIP/VXLAN 模式):
podA → node1.calixxx → node1.tunl0(封装)→ node1.eth0 → node2.eth0 → node2.tunl0(解封装)→ node2.calixxx → podB
# BGP 模式
podA → node1.calixxx → node1.eth0 → node2.eth0 → node2.calixxx → podB# VXLAN 模式:目标 podB 的 IP 会被路由到 tunl0,封装为 VXLAN 数据包
# IPIP 模式:类似于 VXLAN,但使用 tunl0 封装为 IPIP 数据包
# BGP 模式(无封装):直接通过 eth0 发送到 node2(无需隧道)
2.1.1、Node 2 上 Ping Node3 的 PodB
# Node2 检查目标 IP 是否可达
ip route get 10.245.13.23# Node2 主机上 ping PodB
ping 10.245.13.23# Node2 上抓包
tcpdump -i any icmp and host 10.245.13.23# Node3 上抓包
tcpdump -i any icmp and host 10.245.13.23
- 这里抓包可以看到,请求从 Node2 上 tunl0 出去,但 Node3 上并没有 tunl0 接收,直接到了 Node3 的 calixxx 虚拟网卡
- 查看 Node2 和 Node3 系统日志,发现确实有丢包的情况(如何查看丢包可见下方 4.1)
2.1.2、Node3 上 Ping Node2 的 PodA
# Node3 检查目标 IP 是否可达
ip route get 10.245.102.188# Node3 主机上 ping PodA
ping 10.245.102.188# Node2 上抓包
tcpdump -i any icmp and host 10.245.102.188# Node3 上抓包
tcpdump -i any icmp and host 10.245.102.188
- 这里我们就能看到,Node3 上发送的 ICMP 包 到 PodA, PodA 正常接收且返回了包,但 Node3 上没有接收到返回的包,查看 Node2 的系统日志后发现是被 Node2 丢包了
- 这里就猜测是 Node2 的 tunl0 没法正确的转发流量,Calico 使用的 IPinIP 模式,是否没有正确的封装 "源 IP- 目的 IP" 请求头导致的
# tunl0 是IPIP隧道接口,若未启用或配置错误会导致丢包
# 检查接口状态
ip link show tunl0
# 检查隧道配置
ip tunnel show
2.2、关闭 IPinIP 模式
- 在关闭 IPinIP 模式后,发现网络竟然通了,这也证实确实是 IPinIP 模式导致的
# Master:查看 IP in IP 是否开启
calicoctl get ipPool default-ipv4-ippool -o yaml
2.3、研究 IPinIP 模式为 Always 失败原因
- 关闭 IPinIP 后通信正常,查询资料后发现 IPinIP 模式还有另外一个参数 ipipMod: CrossSubnet(用于跨子网节点),虽然我们的节点都是在同一网段(具体见下方),但没有其它报错,然后就尝试了这个参数,没想到也是通
# Master
2: enp1s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000link/ether 00:e0:4c:9a:d6:f6 brd ff:ff:ff:ff:ff:ffinet 172.16.13.95/22 brd 172.16.15.255 scope global noprefixroute enp1s0valid_lft forever preferred_lft foreverinet6 fe80::2e0:4cff:fe9a:d6f6/64 scope link noprefixroute valid_lft forever preferred_lft forever# Node2
2: enp1s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000link/ether 00:e0:4c:9d:eb:5e brd ff:ff:ff:ff:ff:ffinet 172.16.12.36/22 brd 172.16.15.255 scope global noprefixroute enp1s0valid_lft forever preferred_lft foreverinet 172.16.12.101/24 scope global secondary enp1s0valid_lft forever preferred_lft foreverinet6 fe80::2e0:4cff:fe9d:eb5e/64 scope link noprefixroute valid_lft forever preferred_lft forever# Node3
2: eno1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000link/ether a8:5e:45:6c:ee:00 brd ff:ff:ff:ff:ff:ffinet 172.16.12.117/22 brd 172.16.15.255 scope global dynamic noprefixroute eno1valid_lft 78541sec preferred_lft 78541secinet6 fe80::aa5e:45ff:fe6c:ee00/64 scope link noprefixroute valid_lft forever preferred_lft forever
# Master:修改 IP in IP 模式为 CrossSubnet
calicoctl patch ipPool default-ipv4-ippool --patch '{"spec":{"ipipMode": "CrossSubnet"}}'
这里就有一个疑问了,主机网络是在一个子网下,为什么会使用 CrossSubnet 就正常呢,然后开始查主机网络环境,发现 Node2 网卡 enp1s0 上有两个 IP,一个为 172.16.12.36/22(加入集群使用的 ip),一个为 172.16.12.101/24(其他服务使用的 ip)
猜测是 172.16.12.101/24 这个 ip 导致的,删除这个 ip 后集群通信也正常了,这个 ip 和 Node3 不在一个子网,修改为在一个子网后也是正常的
# Node2:修改 172.16.12.101/22 的子网掩码为 24
ip addr del 172.16.12.101/22 dev enp1s0
ip addr add 172.16.12.101/24 dev enp1s0
3、结论
- 最后,经大佬指点后,发现是 Linux 路由决策导致的,这也就解释了上面抓包异常的现象,Node2 使用 172.16.12.101 这个 ip 与 Node3 通信,所以就会有 tunl0 转发 ICMP 包出现丢包的现象
- 结论:Linux 内核会根据最长前缀匹配(Longest prefix Match)原则选择源 IP(即系统会选择与目标地址在更小子网范围内的源 IP)
4、其他
4.1、临时添加 iptables 日志规则
## 1. 临时添加日志规则
# 在 iptables 的 INPUT、FORWARD 和 OUTPUT 链(根据流量方向)中插入日志规则,记录 ICMP 包
#--icmp-type 8:表示 ping 请求(Echo Request)
#--log-prefix:自定义日志前缀,方便过滤
#--log-level 4:日志级别(4 对应 warning)# 记录 INPUT 链的 ICMP 包(目标为本机)
iptables -I INPUT -p icmp --icmp-type 8 -j LOG --log-prefix "ICMP-INPUT-DROP: " --log-level 4# 记录 FORWARD 链的 ICMP 包(经过本机转发的包)
iptables -I FORWARD -p icmp --icmp-type 8 -j LOG --log-prefix "ICMP-FORWARD-DROP: " --log-level 4# 记录 OUTPUT 链的 ICMP 包(从本机发出的包)
iptables -I OUTPUT -p icmp --icmp-type 8 -j LOG --log-prefix "ICMP-OUTPUT-DROP: " --log-level 4## 2. 查看系统日志
# 日志会记录到 /var/log/syslog 或 /var/log/messages(取决于系统)
tail -f /var/log/messages | grep "ICMP-"## 3. 删除临时日志规则(排查完成后)
iptables -D INPUT -p icmp --icmp-type 8 -j LOG --log-prefix "ICMP-INPUT-DROP: " --log-level 4
iptables -D FORWARD -p icmp --icmp-type 8 -j LOG --log-prefix "ICMP-FORWARD-DROP: " --log-level 4
iptables -D OUTPUT -p icmp --icmp-type 8 -j LOG --log-prefix "ICMP-OUTPUT-DROP: " --log-level 4
4.2、Calico IP-in-IP 封装请求头详解
4.2.1、IP-in-IP 封装后的数据包结构
- [外层 IP 头][内层 IP 头][传输层头(如 TCP/UDP)][应用数据]
4.2.2. 外层 IP 头 (新增的封装头)
- 协议号: 4 (表示这是一个 IP-in-IP 封装包)
- 源 IP: 发送节点的隧道端点 IP (通常是发送节点的 IP)
- 目的 IP: 接收节点的隧道端点 IP (通常是接收节点的 IP)
- TTL: 通常设置为 64 或 255
- 其他字段: 标准的 IPv4 头部字段
4.2.3. 内层 IP 头 (原始数据包的 IP 头)
- 源 IP: 原始数据包的源 Pod IP
- 目的 IP: 原始数据包的目的 Pod IP
- 协议号: 原始协议 (如 TCP=6, UDP=17)
- 其他字段: 保持原始数据包的 IP 头不变
4.2.4、示例
# 假设:
# ● 发送节点 IP: 192.168.1.1
# ● 接收节点 IP: 192.168.1.2
# ● 源 Pod IP: 10.10.1.1
# ● 目的 Pod IP: 10.10.2.1外层 IP 头:源 IP: 192.168.1.1目的 IP: 192.168.1.2协议: 4 (IP-in-IP)内层 IP 头:源 IP: 10.10.1.1目的 IP: 10.10.2.1协议: 6 (TCP)TCP 头:源端口: xxxx目的端口: xxxx其他 TCP 字段...# 注意:
# 1. IP-in-IP 封装会带来约 20 字节的开销 (额外的 IP 头)
# 2. 在 Calico 中,IP-in-IP 可以配置为以下模式:
# ● Never: 从不使用
# ● CrossSubnet: 只在跨子网时使用
# ● Always: 总是使用
4.3、安装使用 tcpdump 命令
# 安装 tcpdump
apt-get install tcpdump # 在 Ubuntu/Debian 系统中
yum install tcpdump # 在 CentOS/RHEL 系统中# 使用 tcpdump
# 替换 <interface> 为实际的网络接口名称(如 eth0, tunl0)
tcpdump -i <interface> icmp
# 监控 eth0 的 ICMP 包
tcpdump -i eth0 icmp
# 监控所有网络接口的 ICMP 包
tcpdump -i any icmp
# 过滤 host
tcpdump -i any icmp and host x.x.x.x
# 保存抓包信息为文件(后续可使用 Wireshark 打开具体分析)
tcpdump -i tunl0 icmp and host 10.245.13.43 -w /home/icmp_packets.pcap