最近在部署一個集群環境的時候,發現集群中一個子節點與其他子節點不通,而 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