做Kubernetes网络绕不开Calico,而Calico绕不开三种流量模式:纯BGP、IPIP、VXLAN。很多刚上手的朋友问:既然已经有BGP,为什么还要搞IPIP?原因是纯BGP模式依赖一个很容易被忽略的前提——所有节点都在同一个二层网络里,或者底层网络愿意帮你转发Pod网段的路由。现实里这个前提经常不成立。你跨VPC建集群、多机房容灾,节点分布在不同的网段,BGP广播出去的Pod路由到了路由器那层就被丢掉了,因为路由器不认识192.168.0.0/16这种私有网段该往哪走。
IPIP的思路其实很朴素:在原始Pod数据包外面再套一个IP头,外层IP头的源和目的写的是节点自身的地址。底层路由器只需要处理普通IP包,完全不需要了解Pod网段。之所以有人选IPIP而不是VXLAN,主要看开销和性能。IPIP外层头只有20字节,VXLAN要36字节起步(UDP头加VXLAN头),封装越重性能越差。在“必须上overlay”的前提下,IPIP通常是性能优先的选择。这篇文章我会把Calico IPIP的原理、配置、MTU调优和故障排查全部摊开讲,都是我维护集群时真实验证过的东西。
1. 先说结论:Calico的IPIP模式到底解决什么问题
1.1 Calico网络模式家族:BGP、IPIP、VXLAN怎么选
Calico的三种模式本质上是在“性能”和“网络兼容性”之间做取舍。纯BGP模式性能最好,数据包从Pod出去不封不装,按路由表直接跳,延迟接近裸机网络。但它要求所有节点之间的路由对Calico负责的Pod网段完全可达。自建机房把交换机配好,BGP可以直接和物理网络对话,那纯BGP就是最优解。
一旦拓扑变成多子网、多VPC,或者底层网络管理员不愿意为容器网段动路由配置,纯BGP就会碰壁。VXLAN是走UDP封装,兼容性最强,公有云和各种网络设备都认,但33字节的额外开销(外层IP 20字节加UDP 8字节加VXLAN 8字节,实际常用计算还要加内层以太网头)让它的吞吐和延迟都差一些。IPIP恰好卡在中间:兼容性比纯BGP好,性能比VXLAN好,代价是外层IP头占用20字节,而且内核需要支持IPIP隧道协议。
很多老集群默认就是IPIP模式,原因是它足够通用。节点在同一个二层网络时IPIP能跑,跨子网时也能跑,不需要网络管理员配合改交换机。官方安装Calico时给的默认配置也倾向IPIP,你装完什么都不改,默认的IP池就会带ipipMode: Always。这一点让IPIP成为Calico最常见的工作模式,但也是很多人踩坑的起点。
1.2 什么场景IPIP是首选,什么场景别硬上
根据我维护过的环境,可以总结一个选型套路:同二层网络用纯BGP;跨二层但底层允许普通IP转发,用IPIP;公有云安全组限制多,或者对网络兼容性要求高,用VXLAN。
IPIP最适合自建机房或私有云。比如集群节点分布在两个网段,节点之间三层可达,但交换机不接BGP,你只需要打开IPIP,Pod之间的跨网段通信就能正常工作。底层路由器只转发节点IP,完全感知不到Pod网段。另外一个典型场景是外部设备需要访问Pod IP,纯BGP路由发布不出去,IPIP能把Pod流量“藏”在节点IP后面,外部访问节点IP,再由节点解封装转给Pod。
但有几个场景我强烈不建议硬上IPIP。AWS等公有云的安全组默认不认IP-in-IP这种自定义协议,你要额外放行协议号4,操作麻烦不说,有些云环境甚至根本不支持。其次是底层网络MTU已经被压到1450甚至更低的环境,你再减20字节,Pod实际可用MTU只剩1430,大包很容易触发分片。这时老老实实上VXLAN,UDP端口放行容易,MTU余量也更好估算。简单讲,IPIP解决的是“跨网段路由不可达”的问题,不是万能解药,选型前先搞清楚自己网络环境长什么样。
2. IPIP拆解:一次跨节点通信,数据包到底怎么走
2.1 IP-in-IP封装到底是个什么东西
IP-in-IP(IPIP)说起来很简单:把完整的一个IP数据包,当作另一个新IP数据包的载荷。外层新IP头的协议号标记为4,代表里面装的也是一个IP包,内层IP头保持Pod到Pod的源目地址不变。你可以理解成寄快递时,里面的包裹是一个完整的小盒子,外面再套一个大纸箱,大纸箱上写着节点A和节点B的地址,快递员只看纸箱就能把货送到,拆开后里面那个小盒子才是真正要投递的东西。
这个封装在Calico里是由内核的ipip隧道模块完成的。Calico会创建一个叫tunl0的隧道接口,路由表里凡是需要走IPIP的Pod网段,都会把出口指定到tunl0。当数据包进入tunl0时,内核自动在原有包外面加上新的IP头,再做路由转发。整个过程对Pod完全透明,Pod里的应用程序根本不知道外面还被套了一层头。
要注意的是,IPIP协议号是4,这不是TCP的4号保留位,也不是UDP,是一个独立编号。所以用tcpdump排查时,抓包过滤条件要写成ip proto 4,而不是tcp或udp的常规端口。这个细节很多人第一次排查时压根没想到,导致明明看到有流量,却不知道那就是IPIP封装后的包。
2.2 Calico BGP是怎么把路由传出去的
Calico的控制平面靠BGP。节点上运行着一个叫BIRD的BGP进程,它负责把本节点的Pod网段路由广播给其他节点。默认情况下节点之间是全mesh模式,也就是每个节点都和其他节点建立BGP会话。节点规模超过50个时,全mesh会话数量会膨胀到N乘以N的量级,这时候就需要引入路由反射器(Route Reflector)来收敛连接数。
BGP广播的内容很简单:每个节点会告诉别人“我这里有某个Pod网段,下一跳是我自己”。比如节点1的Pod CIDR是192.168.0.0/26,节点2是192.168.0.64/26,节点1通过BGP把192.168.0.0/26通告给节点2,下一跳指向节点1的IP。节点2收到后,在自己路由表里插入一条192.168.0.0/26 via 节点1 IP的路由。控制平面上的“下一跳”是节点IP,数据平面怎么封装,则取决于IP池的IPIP模式配置。
如果启用了IPIP,路由表里指向远端Pod网段的条目会带上dev tunl0,下一跳是远端节点IP,同时还有onlink标记。onlink的作用是告诉内核,虽然远端节点IP不在本机的直连网段内,但因为它要通过tunl0隧道访问,所以允许把这个IP当作可直连的下一跳。这个标记是Calico自动加上去的,你手动调路由时不要轻易删掉,否则隧道流量会起不来。
2.3 一次完整的数据包之旅
我举个实际例子。集群两个节点:
- node1 IP: 10.10.0.11,Pod网段192.168.0.0/24,上面跑了pod-a,IP是192.168.0.5
- node2 IP: 10.10.0.12,Pod网段192.168.1.0/24,上面跑了pod-b,IP是192.168.1.5
pod-a访问pod-b时,数据包的源地址是192.168.0.5,目标地址是192.168.1.5。包先到达node1的根网络命名空间,内核查路由表,看到192.168.1.0/24这条路由的出口是tunl0,于是触发IPIP封装。封装后的新包头部是:
外层源IP: 10.10.0.11 外层目标IP: 10.10.0.12 外层协议号: 4(IPIP) 内层源IP: 192.168.0.5 内层目标IP: 192.168.1.5然后node1通过eth0把封装后的包发出去。底层网络看到的是一个从10.10.0.11到10.10.0.12的普通IP包,按正常路由转发到node2即可。node2收到后,内核识别出协议号是4,解封装还原出内层数据包,再根据Pod路由转发给192.168.1.5。
这个过程中最需要注意的是,整条路径里底层路由器全程只关心节点IP,不需要知道192.168.x.x怎么走。这就是IPIP的核心价值。你在节点上抓包时也会看到,node1发出的是外层源目地址的包,node2收到后再拆开,抓包位置不同看到的内容完全不同。理解这条路径,后面排查性能问题和连通性问题就有方向了。
3. 配置实战:从安装到切换IPIP模式的完整操作
3.1 安装阶段指定IPIP模式
安装Calico时,如果用的是官方manifest,环境变量里就有个关键参数:CALICO_IPV4POOL_IPIP。这个变量控制默认IP池的IPIP策略,合法的值有Always、Never、CrossSubnet三种。Always表示所有节点之间的Pod流量都走IPIP封装,Never表示禁用IPIP,CrossSubnet表示只有跨子网流量才封装,同一个子网内的节点直连。
如果安装前就已经确定网络环境,建议直接在manifests里改好。比如两个网段之间的集群,用Always最省心;如果所有节点都在同一个大二层里,想追求性能,可以改成Never。示例如下:
env: - name: CALICO_IPV4POOL_IPIP value: "CrossSubnet"这里我踩过一个坑:只在安装时改了这个变量,但集群里已经存在旧IP池,改环境变量并不会自动修改已经创建的IPPool对象。所以升级或改网络模式前,一定要检查现有IPPool的ipipMode字段,改完环境变量后,还得手动编辑IPPool才能生效。
另一种常见安装方式是用Calico Operator。在Installation资源里设置calicoNetwork.ipPools下的ipipMode,比如:
spec: calicoNetwork: ipPools: - name: default-ipv4-ippool cidr: 192.168.0.0/16 ipipMode: Always natOutgoing: trueOperator的好处是升级时不会丢失配置。我一般推荐生产环境用Operator,临时测试环境才用kubectl直接apply manifest。
3.2 动态切换IPIP模式:改IPPool就够了
集群已经在运行,临时想切模式,不需要重装Calico,直接改IPPool对象就能生效。先看一下当前IPPool配置:
kubectl get ippool default-ipv4-ippool -o yaml输出里能看到spec.ipipMode字段。想改成Always,就执行:
kubectl patch ippool default-ipv4-ippool --type='json' \ -p='[{"op": "replace", "path": "/spec/ipipMode", "value": "Always"}]'改完之后,Calico会重新计算所有节点上的路由表。观察一下节点上的变化:
ip route | grep tunl0如果从Never改成Always,你会看到原本指向eth0的远端Pod网段路由,都变成了通过tunl0。切换过程一般在几秒到几十秒内完成,但数据面会有一个短暂的中断,生产环境建议选维护窗口操作。
我自己的经验是,先改一小批节点做验证,再全局切换。另外记得同步检查BGP状态,因为模式变化会影响路由通告和下一跳状态,最好在切换后用calicoctl node status扫一眼所有节点的BGP会话是否健康。
3.3 CrossSubnet:只对跨子网流量封装,性能更优
CrossSubnet是我在实际生产里最推荐的一种模式。它兼顾了性能和兼容性:同一个子网内的节点之间走纯BGP直连,完全没有封装开销;跨子网的节点之间才走IPIP。这样设计能在多机房间保持低延迟,又不用牺牲跨网段的连通性。
启用方式同样简单:
apiVersion: projectcalico.org/v3 kind: IPPool metadata: name: default-ipv4-ippool spec: cidr: 192.168.0.0/16 ipipMode: CrossSubnet natOutgoing: true配置完成后,你可以用一条命令验证效果。假设node1和node2在同一子网,node3在另一个子网,查看路由表时应该能发现:
192.168.1.0/24 via 10.10.0.12 dev eth0 # 同子网节点走eth0 192.168.2.0/24 via 10.20.0.13 dev tunl0 onlink # 跨子网节点走tunl0这个模式唯一的坑是,节点IP必须能正确体现所属子网。Calico在识别子网时会根据节点IP和掩码判断,如果节点上有多个网卡或网卡配了多个IP,可能会误判。我遇到过一次多网卡节点把内网IP当成了跨子网地址,导致同子网流量也走了IPIP,后来通过给calico-node容器设置IP自动检测规则解决了。官方默认的检测方式是first-found,生产环境建议根据实际网卡名或正则指定,避免误判。
4. MTU和性能调优:这些参数值得抠一抠
4.1 MTU计算:20字节的开销到底怎么算
IPIP封装带来20字节的额外IP头,意味着如果底层网卡MTU是1500,Pod内的网络接口MTU就不能超过1480,否则封装后总长度超过1500,会触发分片。计算方式很简单:
容器MTU = 底层网络接口MTU - 20(IPIP) 容器MTU = 底层网络接口MTU - 36(VXLAN)但实际环境里,底层MTU不一定正好是1500。比如某些云环境虚拟网卡MTU是1450,那IPIP模式下的容器MTU应该是1430;如果有些网络设备还会额外增加VLAN tag等开销,这个数字还要再往下调。我自己的做法是先确认所有节点实际物理网卡的MTU,取最小值再扣掉封装开销。
在Calico里可以通过IPPool的mtu字段显式设置:
spec: mtu: 1480如果不设置,较新版本的Calico会自动探测节点接口MTU并做相应扣除。自动探测大多数时候没问题,但多网卡或特殊VXLAN叠加的复杂环境可能会算错,所以我更倾向于在IPPool里写死一个值,统一管理。
4.2 IPIP、VXLAN、纯BGP三种模式的性能对比
从测试结果看,纯BGP模式由于没有封装,吞吐最高、延迟最低,最适合对性能敏感的业务。IPIP模式多了一层20字节的外层IP头,转发时CPU要多做一次封装和解封装,性能大概会损失5%到10%。VXLAN由于走UDP封装、头部更大,加上常见实现里要处理校验和,性能最差,损失可能在10%到20%左右。
我整理了一个大概的对比表格,方便你对照选型:
| 对比维度 | 纯BGP | IPIP | VXLAN |
|---|---|---|---|
| 额外头部开销 | 0字节 | 20字节 | 约36字节起步 |
| 吞吐损耗 | 最低 | 中等 | 较高 |
| 延迟 | 最低 | 中等 | 较高 |
| 跨子网支持 | 看底层路由配置 | 支持 | 支持 |
| 公有云兼容性 | 较差 | 一般 | 较好 |
| MTU余量 | 最大 | 中等 | 最小 |
从表格能看出,纯BGP永远是性能天花板。但现实中跨子网场景太多,IPIP是折中后的首选。CrossSubnet模式进一步把同子网流量降级成纯BGP,实际生产环境里大部分访问留在同子网内,所以整体性能损耗会明显小于Always模式。
4.3 实际调优经验:什么时候用Always、CrossSubnet、Never
调优首先要回答一个问题:你的流量是不是大量跨子网?如果业务本身是跨机房的,或者Pod分布在多个VPC,跨子网流量占比高,那CrossSubnet的优势会缩水,直接用Always反而简单可控。如果业务基本集中在一个子网内,只有少量管理面流量跨网段,CrossSubnet几乎就是最优解。
我还有一个建议是关注大包场景。很多计算任务会发送MTU接近上限的数据包,比如分布式存储、模型训练,这类流量对分片非常敏感。遇到这种业务,我建议即使开了IPIP,也要把MTU算准并显式设置,避免因为自动探测和实际链路不一致导致偶发大包丢包。排查分片问题时,ping命令很好用,比如:
ping -M do -s 1472 10.10.0.12这条命令验证的是1500 MTU下不带分片的最大数据包。如果底层网络MTU是1500,1472字节payload加28字节ICMP头正好是1500。如果这条ping通,说明二层链路支持1500;如果通不了,再逐级减小-s参数找实际上限。
5. 踩坑实录:IPIP常见故障与排查思路
5.1 案例一:跨子网Pod不通,同子网却正常
这种症状基本可以锁定问题出在IPIP封装或跨子网路由上。先检查IPPool的ipipMode是不是Always或CrossSubnet。如果配的是Never,跨子网Pod根本不会有隧道路由,自然不通。
再检查BGP状态。用calicoctl查看:
calicoctl node status看BGP对端是否都是Established状态。如果有节点是Active或Connect,说明BGP会话没建立成功,可能是TCP 179端口被防火墙阻断,也可能是节点AS号配置不一致。还有最容易被忽略的问题:云安全组没有放行IPIP协议。跨子网流量要真正跑通,底层必须允许IP-in-IP数据包,也就是协议号4。一般云控制台的安全组规则里会有一项Custom Protocol,选IP-in-IP协议号4放行,而不是只放行某个TCP/UDP端口。这个坑我踩过不止一次,每次都得提醒自己先看防火墙再查路由。
5.2 案例二:小包能通,大包一直不通
小包通、大包不通,九成是MTU问题。IPIP模式下,当Pod发送一个接近容器MTU大小的数据包,封装后超过底层链路MTU,如果设备不允许分片,包就被丢了。TCP场景下表现更典型:TCP握手能成,但一旦开始传输大数据块就卡住或不断重传。
解决办法分三步。第一步,确认底层链路真实MTU,用ping -M do逐级测底数;第二步,把IPPool的mtu字段改到理论值或略小一点;第三步,更新后重启受影响Pod,让Pod内的接口拿到新MTU。注意,只改IPPool不重建Pod,veth接口的MTU有可能不会立即更新,我吃过这个亏,业务方报障说还是不通,最后发现是Pod还保留着老MTU。
还需要检查宿主机上的tunl0接口MTU。tunl0的MTU决定了封装后外层包能否顺利发出。如果tunl0 MTU设得比物理网卡还大,大包在物理出口一样被丢。一般建议tunl0 MTU和物理网卡保持一致或小一点,配合IPPool里的容器MTU一起调。
5.3 案例三:BGP邻居建立不了,路由表空空的
如果路由表看不到远端Pod网段的路由,说明BGP分发出了问题。常见原因有几个:节点之间的BGP端口179被防火墙挡了;全mesh模式下节点数量太多导致会话数爆炸;或者某些节点配置了不同的AS号导致邻居拒绝建立。
排查时先看BGP会话状态:
calicoctl node status再看节点的BIRD日志。calico-node容器的日志里会输出BGP连接失败的详细原因,比如connection refused或hold timer expired。我遇到过一次比较隐蔽的情况:某个节点因为内核参数conntrack冲突导致BGP连接被重置,BGP会话反复建不起来。后来调整了节点上的conntrack相关设置才稳定下来。这类问题要靠日志一步步排查,不能只盯着Calico配置看。
如果节点数量超过50个,全mesh模式就非常吃力了。BGP会话数是N乘N的级别,建议尽早切换成路由反射器模式。Calico支持配置BGPPeer资源指定Route Reflector节点,配置完成后普通节点只和RR建立会话,网络压力会小很多。
5.4 常用排查命令清单
排查IPIP问题时,我一般按下面这个顺序走,效率最高:
# 1. 查看当前IPPool配置,确认ipipMode和mtu kubectl get ippool -o yaml # 2. 查看BGP节点状态,确认会话全Established calicoctl node status # 3. 查看路由表,确认tunl0相关路由是否存在 ip route | grep tunl0 # 4. 确认tunl0接口状态 ip link show tunl0 ip addr show tunl0 # 5. 抓包看IPIP封装流量 tcpdump -i eth0 ip proto 4 -nn # 6. 测试底层链路MTU ping -M do -s 1472 <节点IP>这个顺序能覆盖绝大多数问题:先看配置对不对,再看控制平面通不通,最后验证数据平面。抓包那一步特别关键,如果你在eth0上看到正常数量的ip proto 4包,说明IPIP封装已经工作了,问题可能出在后端虚拟网络或Pod侧;如果完全看不到,说明流量根本没走到封装这一步。
5.5 常见问题速查表
| 现象 | 可能原因 | 处理办法 |
|---|---|---|
| 跨子网Pod不通 | ipipMode为Never | 改IPPool为Always或CrossSubnet |
| 跨子网Pod不通 | 安全组未放行IP-in-IP协议 | 在安全组放行协议号4 |
| 同子网正常跨子网失败 | 底层路由器丢弃封装包 | 检查核心交换机是否允许IPIP协议转发 |
| 大包不通小包通 | MTU设置过大 | 按底层MTU减去20重置IPPool的mtu |
| 路由表没有远端Pod路由 | BGP会话未建立 | 检查TCP 179端口、节点AS号、BIRD日志 |
| BGP频繁断开 | conntrack或防火墙干扰 | 调整节点conntrack参数或放行相关协议 |
| 切换模式后路由未更新 | IPPool对象没改 | 编辑IPPool的ipipMode并等待Calico重算路由 |
| tunl0不存在或DOWN | 内核模块缺失 | modprobe ipip确保内核加载ipip模块 |
这几年维护Kubernetes集群,我最大的体会是,Calico IPIP本身不是特别复杂的东西,但非常考验对网络细节的理解。很多网络问题不是配置写错,而是底层链路、MTU、防火墙这些基础设施层面的因素和Calico的默认行为相互作用。遇到问题别急着改配置,先把数据包路径完整捋一遍,再对照命令清单一层层验证,往往比瞎试来得快。特别是CrossSubnet模式,它虽然聪明,但依赖节点子网判断,多网卡环境里一定要提前把IP检测规则配好,否则会踩到同子网流量也走封装的暗坑。希望这篇指南能帮你少走几步弯路。