有一回我们集群跨节点 Pod 突然大面积 ping 不通,BGP 对等体全军覆盖、路由表里该有的条目也都在,看起来一切正常,业务就是死活不通。查到凌晨才靠 tcpdump 发现,底层交换机把协议号 4 的 IPIP 报文当成异常包丢弃了。那次排障之后,我才认真把 Calico 的 IPIP 完整吃透:从工作模式、封装流程,到版本选择、排障手段,全部重新过了几遍。
这篇内容就从实际运维视角出发,把 Calico IPIP 的来龙去脉讲清楚。内容包括 IPIP 报文从源 Pod 到目标 Pod 的流转路径、三种模式的选型分析、指定版本下载安装的完整操作,以及我在生产环境里踩过的一堆坑。适合觉得自己“会用 Calico 但没真正搞懂封装细节”的 Kubernetes 网络维护者,也适合准备把集群搬入新机房或云上 VPC、正在纠结封装模式怎么选的同学。
1. 纯三层 Calico 为什么还要默认开启 IPIP
Calico 对外宣传的标签是“纯三层网络方案”,数据面走 Linux 内核路由、控制面走 BGP,不像早期 Flannel、Weave 那样非得上 overlay。但实际部署时你会发现,默认创建的 IPPool 经常带着 IPIP 封装。看似矛盾,背后其实是生产网络环境太复杂,纯路由在裸奔时根本兜不住。
1.1 Calico 的基础数据路径:BGP 学习路由,内核直接转发
要理解 IPIP 存在的意义,先把 Calico 的正常数据路径捋一遍。每个 Node 上的 calico-node 会通过 BGP 把自己负责的 Pod 网段通告出去,其他节点学到之后,在 Linux 路由表里生成对应条目。Pod A 访问 Pod B 时,源节点内核根据路由表找到目标 Pod 对应的下一跳,也就是目标节点的 IP,直接把数据包从物理网卡发过去。
这种模式下没有隧道封装,数据包在底层网络里是“裸奔”的。代价最小,性能损耗基本为零,这也是 Calico 在性能测试里表现普遍优于 VXLAN 类方案的原因。
但这里有个隐含前提:目标节点的 IP 是底层网络可达的,而且从源节点发出的、带着 Pod 源地址的数据包,底层网络也愿意转发。
1.2 多数底层网络做不到“对 Pod 网段透明”
云上的 VPC、物理机房的交换机,它们认识的是节点 IP,比如192.168.1.10、10.0.2.20这类网段。Pod 网段是集群内部自己规划出来的,比如172.20.0.0/16,底层路由表根本没有它的条目,也没人愿意为这个内部分配动态路由协议。
就算你愿意跟网络管理员协调,把 Pod 网段加进底层路由表,后续扩容拓扑一变,又是一番折腾。更别提跨机房、跨 VPC 场景,网络边界根本不归你管。
所以 Calico 设计了一个“保底机制”:当底层网络不可控时,把 Pod 发出的数据包外面再套一层 IP 头。外层源地址是源节点 IP、外层目的地址是目标节点 IP,底层网络只看到“节点和节点在通信”,完全不需要理解 Pod 网段。这套机制就是 IPIP。
1.3 为什么默认偏向 IPIP 而不是 VXLAN
常见容器网络里,IPIP 和 VXLAN 都有封装能力,但 Calico 默认首选 IPIP,核心原因是开销和实现复杂度。
封装开销方面,IPIP 只在原始 IP 包外面加一个 20 字节的 IPv4 头,数据面改动小,性能损耗低。VXLAN 则要加 VXLAN 头、UDP 头和外部 IP 头,总共多出 50 字节左右,而且多一次 UDP 封装与解封装,CPU 消耗也更高。
实现上,IPIP 直接复用 Linux 内核的ipip模块,节点上出现一个tunl0隧道接口,用的也是内核原生路由机制,不依赖用户态进程处理数据面。生产环境里,纯物理机机房、自建虚拟化平台、绝大多数传统 IDC,IPIP 都能稳稳跑起来。
不过 IPIP 也有天然短板,最典型的是协议号不常见。IPIP 外层头的协议号固定是 4,部分网络设备或云平台安全组没有放行这个协议,就会直接丢包。遇到这类环境,就得改成 VXLAN 或者关闭封装,这也是后文排障章节要重点讲的内容。
2. IPIP 报文从 Pod 到 Pod 到底经历了什么
很多文章喜欢把 IPIP 称为隧道,但从数据面实现看,它更像是“路由表里一个特殊的 dev”。要真理解 IPIP,跟着一个数据包从源 Pod 走到目标 Pod 最靠谱。
2.1 源节点上发生的事:路由命中 tunl0,内核完成封装
假设集群节点和 Pod 网段如下:
| 资源 | 地址 |
|---|---|
| 节点 A | 192.168.1.10 |
| 节点 B | 192.168.2.20 |
| Pod A(在节点 A) | 172.20.1.2 |
| Pod B(在节点 B) | 172.20.2.3 |
节点 A 上,BGP 已经把 Pod B 所属网段的路由同步了下来。执行ip route,通常能看到类似这样的条目:
172.20.2.0/26 via 192.168.2.20 dev tunl0 proto bird其中via 192.168.2.20是下一跳,也就是节点 B 的 IP;dev tunl0告诉内核,匹配这条路由的数据包要走 tunl0 隧道接口。
Pod A 发出的原始数据包到了内核协议栈,完成路由查找后,发现出口设备是tunl0。内核的ipip模块会立刻在外面套一个新的 IPv4 包头,协议号填 4。外层头部关键字段大概长这样:
Outer Source IP: 192.168.1.10 Outer Destination IP: 192.168.2.20 Protocol: 4 (IP-in-IP)此时数据包的长度比原始包多了 20 字节。内核再把封装后的包交给物理网卡 eth0 发出去。
2.2 目标节点上发生的事:解封装后二次路由
节点 B 的物理网卡收到外层 IP 包后,先把外层的 IP 头剥掉,把原始 Pod 数据包留给本机协议栈继续处理。这里要特别注意,解封装后的包并没有立刻送到 Pod B 的 veth,而是再次进行路由查找。
节点 B 上同样有一条自己维护的直连路由,比如:
172.20.2.0/26 dev eth0 scope link这里的 dev 是 Pod B 所在节点上的 veth 设备侧,具体设备名可能是vethxxx。内核第二次路由查找命中后,数据包被送入对应 veth,最终到达 Pod B 的协议栈。
在外界看来,整个过程就是节点 A 发了一个外层 IP 包给节点 B,B 收到后解包再转给内部 Pod。底层网络始终感觉不到 Pod 网段的存在。
2.3 紧绑定的 MTU 变化:1500 变 1480 不是玄学
IPIP 多了 20 字节外层头,这直接影响链路 MTU。物理网卡 MTU 是 1500 时,封装后的包最大只能是 1500,那原始 Pod 内层数据包就只能控制在 1480,否则就会分片。
Calico 在启用 IPIP 时会自动计算 Pod 接口的 MTU,通常把veth的 MTU 设置成 1480。因此 Pod 里ip link看到的 MTU 仍然是一个相对整的值,不需要手工调整。但如果物理网卡启用了 jumbo frame,比如把 MTU 调成了 9000,那就得同步调整 Calico 对 Pod MTU 的配置,否则内层浪费了不少空间。
实际排查时,如果发现小包能通、大包经常卡住,第一个就要想到 MTU 问题。用 ping 测试时,最直接的办法是从1450开始逐步调大 payload,一点一点试探临界点。
如果你想在节点上直接观察 IPIP 报文,而不是看 Pod 内的包,可以这样抓包:
tcpdump -i eth0 ip proto 4 -nn -c 100抓到以后,展开外层 IP 头,能看到protocol: 4,源目地址都是节点 IP,这就说明 IPIP 封装确实在跑。
3. Always、CrossSubnet、Never 到底该怎么挑
不少人在配 Calico IPPool 时,对ipipMode的三个值只停留在“能用就行”的层面,随手填个 Always,生产环境跑出问题才知道疼。模式选型不是看哪个名头好听,而是要看底层网络到底能为你承担多少。
3.1 三种模式的行为差异与典型场景
| 模式 | 行为 | 典型场景 | 主要代价 |
|---|---|---|---|
| Always | 对任何目标 Pod 的数据包都做 IPIP 封装 | 底层网络完全不可控,依赖路由不可用 | 同子网节点间也多 20 字节开销 |
| CrossSubnet | 目标节点与源节点不在同一子网时封装,同子网不封装 | 底层网络收留节点之间,但不收留 Pod 网段,同机房同网段带宽敏感 | 需要能识别节点子网,依赖底层二层域 |
| Never | 从不封装,纯裸路由 | 底层路由完全支持 Pod 网段 | 一旦底层网络不认 Pod 网段,立刻断连 |
Always 的最大问题是,哪怕节点 A 和节点 B 就在同一个交换机下、二层网络本来就能把数据包送过去,它仍然要强行封装,白白增加开销。如果你在性能敏感的物理机房内网,All Pod 都在同一大二层里,依旧用 Always,延迟多了、CPU 多了,但收益几乎为零。
Never 恰恰反过来。它要求底层网络真的能直接转发 Pod 网段流量,要么你已经把 Pod 网段路由下发到三层交换机和云路由表里,要么全网全网其实跑在同个子网。实际生产里,很多新落地的小集群节点间确实有完整路由,但 Pod 网段没有同步到网络设备,这时用 Never 就是自找麻烦。
CrossSubnet 最像“中间路线”:同子网流量不封装,利用二层交换机的原生转发;跨子网流量再封装,解决底层网关不认 Pod 网段的问题。在多数本机房多网段、自建 VPC 多子网的场景下,CrossSubnet 是性价比最平衡的选项。
3.2 CrossSubnet 看起来很完美,但有一个前提
CrossSubnet 判断是否封装的依据,是源节点和目标节点的 IP 是否在同一个子网。同子网时走裸路由,跨子网时走 IPIP 封装。听起来很适合云上 VPC,但我必须提醒你,CrossSubnet 并不能解决跨子网流的底层路由问题。
举个例子:节点 A 在192.168.1.0/24,节点 B 在192.168.2.0/24,中间隔了一台三层交换机。如果这台交换机不允许转发协议号 4 的报文,那 CrossSubnet 模式下跨子网流量照样会断。所谓“CrossSubnet 自动解决跨子网问题”,前提是底层允许 IPIP 报文通过。
所以生产环境的最佳实践是:先把底层设备安全策略搞清楚。如果设备不支持协议号 4,那跨子网流量就不该寄希望于 CrossSubnet,要么关封装然后打通底层路由,要么直接用 VXLAN 这类基于 UDP 的封装模式,因为 UDP 4789 在大多数网络设备里英文文件更容易放行。
3.3 云环境里更要谨慎:安全组不认识 IPIP
我见过好几个团队把 Calico 从物理机迁到公有云时,沿用 IPIP 默认配置,结果跨节点 Pod 一开机就不通。原因高度统一:云安全组入站规则没有放行协议号 4。云平台的安全组操作界面一般默认放行 TCP、UDP、ICMP,很少人意识到还要单独放“IP-in-IP”或者协议号 4。
如果你在云上,建议优先确认安全组的协议支持情况。开通协议号 4 以后,IPIP 基本能正常跑;但出于稳定性和后续可维护性考虑,纯云原生环境我更推荐把 IPPool 切到 VXLAN 模式,业务侵入更小。
4. 指定版本 Calico 的下载与 IPIP 模式落地
Calico 的安装方式相对灵活,但“下载指定版本”这个问题,不少初次接触的人很容易踩坑:点进 GitHub 默认分支下载的 manifests 可能是还在开发中的版本,跑起来跟当前 Kubernetes 版本对不上。这篇单独拿出一个章节写清楚版本选择和下载方法。
4.1 版本选择:先看兼容矩阵,再定版本号
Calico 和 Kubernetes 之间有明确的版本兼容矩阵,通常发布在 Calico 官方文档的 “Product compatibility” 页面。选版本的第一个动作,不是拉最新 Release,而是确认你的 Kubernetes 小版本在哪个范围内兼容。
大致规律是:Calico v3.26 对应 Kubernetes 1.26 到 1.30 附近,v3.27 会覆盖更后面的版本,v3.28 再往后扩展。具体到某个 K8s 小版本,应该以官方兼容矩阵为准。我的建议是选择比 Kubernetes 小版本落后一两个版本的 Calico,既稳定,相关排障案例也相对多。
另外提醒一点,尽量折腾“官方 Release tag”里的文件,而不是默认分支master或main。默认分支的 manifests 可能包含尚未正式发布的特性,放到生产环境一旦出问题,资料都很难找。
4.2 从 GitHub Release 下载指定版本
需要calicoctl或者 Calico manifests 文件时,直接从 GitHub Release 页下载固定 tag 的资源即可。比如要下载 v3.27.2 的 calicoctl:
curl -sSLO https://github.com/projectcalico/calico/releases/download/v3.27.2/calicoctl-linux-amd64 mv calicoctl-linux-amd64 calicoctl chmod +x calicoctl同时把对应版本的 manifests 下载下来,便于离线检查或重复安装:
wget https://github.com/projectcalico/calico/archive/refs/tags/v3.27.2.tar.gz tar xvf v3.27.2.tar.gz cd calico-3.27.2如果更习惯 Helm,也可以从官方 Helm 仓库拉取指定 chart 版本:
helm repo add projectcalico https://projectcalico.docs.tigera.io/charts helm pull projectcalico/tigera-operator --version v3.27.24.3 安装时直接把 IPIP 模式定下来
Calico 的新版本推荐通过 Operator 方式安装。安装前把 operator 和自定义资源准备好,然后在Installation自定义资源里指定 IPPool 封装模式即可。
先创建命名空间并应用 operator:
kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.2/manifests/tigera-operator.yaml然后准备一个custom-resources.yaml,这是决定 IPIP 模式的关键文件:
apiVersion: operator.tigera.io/v1 kind: Installation metadata: name: default spec: calicoNetwork: ipPools: - name: default-ipv4-ippool blockSize: 26 cidr: 172.20.0.0/16 encapsulation: IPIP natOutgoing: Enabled - name: default-ipv4-ippool blockSize: 26 cidr: 172.20.0.0/16 encapsulation: IPIP natOutgoing: Enabled nodeSelector: all()注意encapsulation字段支持IPIP、VXLAN、None三种值。如果希望使用CrossSubnet,需要在 IPPool 里显式配置ipipMode: CrossSubnet,Operator 安装后也可以通过修改 IPPool 完成调整。如果你使用的是较老的calico.yaml直接安装方式,则通常要通过环境变量或在 manifests 中的 IPPool 资源里改ipipMode。
应用自定义资源:
kubectl apply -f custom-resources.yaml这套方式的好处是,版本明确、IPIP 模式从一开始就固定,后续不容易出现模式漂移。
4.4 安装后检查现有 IPPool 的封装状态
已经跑了一段时间的集群,想确认当前用的是不是 IPIP,直接查看 IPPool 即可。新版 Calico 把 IPPool 做成了 CRD,可以直接用 kubectl 查询:
kubectl get ippool -o yaml重点看ipipMode和vxlanMode两个字段。如果ipipMode: Always,那说明当前确实是彻底封装。如果ipipMode: CrossSubnet,那就是按子网条件进行封装。
同时检查节点上的隧道接口:
ip addr show tunl0 ip tunnel show启用 IPIP 后,每个节点通常能看到tunl0,状态为 UP,且隧道类型为ipip。如果 IPIP 没有启用,可能看不到这个接口,或者接口处于 DOWN 状态。
4.5 跑完安装后如何验证 IPIP 真的生效
验证不能只看配置,还要看数据面行为。第一步,跑两个位于不同节点的测试 Pod,从 Pod A 去 ping Pod B。接着在源节点上抓包:
tcpdump -i eth0 ip proto 4 -nn -c 20如果能抓到协议号为 4 的 IPIP 报文,就能确认封装确实发生在源节点上。如果只看到普通 ICMP 包,可能是 IPIP 模式没有真正开启,或者流量走了别的方式。
另一个侧证是查看节点上的 calico-node 日志:
kubectl logs -n kube-system calico-node-xxxxx | grep -i ipip不过这个日志信息比较有限,最终以tcpdump抓包为准。
5. IPIP 生产环境排障复盘
配置我们都会配,真正拉开差距的永远是问题出现时的定位能力。这里把我在生产环境里跟 IPIP 反复搏斗的几条经验完整梳理一遍,希望能帮你少走点弯路。
5.1 跨节点 Pod 不通:从 BGP 状态到协议号的完整排查链路
跨节点 Pod 不通,很多人第一反应是查 CNI 配置,但 IPIP 场景下有一套更高效的排查顺序。
第一步,先看 BGP 对等体是否正常。在安装了 calicoctl 的节点执行:
calicoctl node status如果 BGP 对等体不是 Established,先解决 BGP 问题,否则后面所有路由都是空谈。
第二步,看源节点路由表是否学到目标 Pod 网段:
ip route | grep 172.20.2.0第三步,检查 tunl0 接口状态:
ip addr show tunl0如果接口 DOWN,IPIP 封装就没法落地。可以尝试加载内核心模块看看:
modprobe ipip第四步是抓包。这一步能把问题从路由层推向策略层。在源节点执行:
tcpdump -i eth0 ip proto 4 -nn -c 50如果发现 IPIP 报文有出无回,或者根本抓不到协议号 4 的包,重点指向中间网络设备或云安全组。
有一次我们排查时,BGP 和路由全部正常,tunl0 也开着,但跨子网的包始终无法到达。反复确认后,发现是对端机房的核心交换机 ACL 没有放行协议号 4。底层网络团队增加一条允许 IP protocol 4 的规则后,流量瞬间通了。这种问题从应用层往下看不到任何异常,只有从抓包里才能发现外层 IP 包被静默丢弃。
5.2 大包不通小包通:MTU 问题最常见
IPIP 的固定 20 字节开销,让 MTU 问题成为高频故障。现象通常是:同一节点内 Pod 互通正常,跨节点小流量正常,业务传输大文件时卡住或者 TCP 连接频繁超时。
定位方法是从业务 Pod 里向目标 Pod 发包,逐步试探 MTU 临界点:
kubectl exec -it pod-a -- ping -M do -s 1450 <pod-b-ip>如果 1450 字节的包能通,把 payload 逐步加大到 1472 或更大,很可能就出现不通。因为 Pod 内 MTU 是 1480,ICMP 消息本身有 8 字节 ICMP 头加上 20 字节 IP 头,能承载的最大 payload 通常是 1452 左右,再往大就会引发分片或丢弃。
解决思路有三种,根据线上情况挑一个:
- 调整底层物理网卡 MTU,把节点物理链路调大,Calico 才能顺势增加内层 MTU。
- 直接改 Calico 的 Pod MTU 配置,让 Pod 接口与封装后的 MTU匹配。
- 改用 VXLAN 或关闭封装,从源头减少额外头部带来的压力。
如果只是测试阶段,直接把物理网卡 MTU 调成 9000 并同步调整 Calico MTU 相关参数,大包基本不会再出问题。
5.3 修改 IPIP 模式后存量 Pod 不重建,迟早踩坑
有一种操作特别危险:集群已经稳定运行,你为了调性能,直接把 IPPool 的ipipMode从Always改成CrossSubnet,只改了配置而没有重建存量 Pod。结果会出现一部分老 Pod 的路由行为和新配置不一致,流量走不通,然后又去查了半天 BGP。
原因不复杂。IPIP 模式切换会影响节点上的路由走向和 tunl0 的启用状态,但已经创建出来的 Pod 的 veth 网卡、节点上的 conntrack 状态并不会立刻刷新。新旧状态叠加,就可能出现“同一个集群里,一部分 Pod 能通,一部分不能通”的诡异现象。
正确的操作流程是:
- 修改 IPPool 的
ipipMode或新建 IPPool。 - 保存并应用配置。
- 滚动重启 calico-node,确保节点侧路由重建。
- 对存量业务 Pod 做一次滚动重建,让数据面状态彻底对齐。
- 再用
kubectl get ippool -o yaml确认最终模式。
如果你担心业务中断,建议在低峰期操作,还应该准备一个快速回滚方案:保留旧的 IPPool 定义,改动后一旦发现问题,恢复旧模式并重启节点即可。
5.4 别忽视 conntrack 对 IPIP 流量的影响
IPIP 场景下,conntrack 导致的问题也很常见,尤其当你切换 IPIP 模式或者重建大量业务 Pod 后。Linux 的 conntrack 表会保留旧连接状态,而 IPIP 报文经过两次 IP 头处理,conntrack 对外层连接和内层连接的处理逻辑不完全一样。某些场景下,你会看到内层 Pod 之间的 TCP 连接被为 INVALID 状态,被防火墙规则直接丢掉。
如果遇到这种问题,可以尝试清理节点上的 conntrack 表项,或者重启对应节点的 calico-node,让数据面重新建连。当然,最省事的办法还是建立固定的运维流程,每次改动 IPIP 模式后都主动做一次节点滚动重启,把 conntrack 残留清干净。
5.5 从 IPIP 切换到 VXLAN 的注意事项
当底层网络明确不允许协议号 4,或者云厂商只放行 UDP 时,就需要考虑把封装模式从 IPIP 切到 VXLAN。切换前,先确认节点上是否支持 VXLAN 隧道,Linux 内核一般自带了vxlan模块,不用额外编译。
操作上,还是先改 IPPool 的vxlanMode字段。这和 IPIP 模式改动一样,都建议先试验再铺开,并且重建相关业务 Pod。切换期间,BGP 路由信息并不会消失,但数据面行为会变,要特别关注 conntrack 残留和防火墙策略对 UDP 4789 的放行情况。
VXLAN 的排障抓包,与 IPIP 的最大区别在于抓包过滤器不再是ip proto 4,而改成:
tcpdump -i eth0 udp port 4789 -nn -c 100看到 UDP 4789 的包,说明 VXLAN 封装在跑。VXLAN 的头部开销比 IPIP 多约 50 字节,MTU 相应要再减一次,这一点千万记得同步配置。
写在最后的几个小建议
把 Calico IPIP 真正搞熟之后,最大的感受是:网络问题往往不是“配错机密”那么简单,更多是底层网络、内核路由、隧道封装三者叠加后的系统性复杂。无论是模式选错导致性能下降,还是安全组没放行协议号导致断连,定位问题的关键都是先想清楚“数据包在哪个环节变成了什么样子”。
如果你现在正在处理一个稳定的生产集群,建议先抽空执行一次kubectl get ippool -o yaml,把你当前的 IPIP 模式记录下来,再确认一遍 tunl0 状态和物理链路 MTU。这些日常巡视动作,看起来不起眼,但真要出了故障,能帮你省下好几个小时的排障时间。等踩过几次坑,就会发现 IPIP 本身并不复杂,复杂的是它背后那只“底层网络的手”。