
1. 先从最头疼的问题说起Pod 网络地址漂移到底怎么解在K8s里干活你迟早会撞上一堵墙——网络。很多新手上手跑通了一个Nginx正准备高兴下一秒就把Pod删了重建发现IP变了连不上了。这不是玄学这是K8s网络的本质Pod是“临时公民”IP是“临时身份证”每次重启都可能换号。用过Docker的人会有个错觉容器嘛不就是背后有个网桥容器通过veth插到网桥上然后拿一个网桥网段的IP就能通信了。这一套在单机场景下没问题但到了K8s集群问题直接翻倍。因为K8s的核心诉求是“跨节点调度”同一个Deployment的Pod可能分布在不同的物理节点上Pod A在节点1Pod B在节点2两边根本没有共同的二层网络怎么通信如果只是“跨节点通信”这一个问题方案其实也不少。比如把节点直接串成一个大二层用路由协议打通或者做Overlay隧道让节点不管底层基础设施是什么都能在逻辑上成为一个大二层网络。但这还不够K8s对网络有几个硬性要求Pod和Pod之间必须能直接通信不需要NATPod和Service之间要能通信节点和Pod之间要能通信。这三个要求合起来就形成了容器网络模型也叫CNM。在K8s里真正干活的是CNI插件。CNI全称是Container Network Interface容器网络接口它是一套标准任何网络方案只要实现了这套标准就能被Kubelet调用完成Pod网络的创建和销毁。目前社区里应用最广的两大插件就是题目里写的flannel和calico。一个主打“简单省事”一个主打“功能全面高性能”。这篇文章不打算从零讲CNI标准也不会把每个插件源码翻一遍而是以我实际在集群里部署、排查、调优的经历为主线把flannel和calico的核心原理、工作模式、参数选择、常见坑一次说清楚。看完你至少能回答这几个问题为什么我的集群Pod之间偶尔通偶尔不通为什么flannel装完一切正常但跨节点延迟有点高为什么calico这么难配但大型集群都在用2. 容器网络模型与CNI通用架构2.1 容器网络模型里四条铁律在深入插件之前有必要把K8s对网络的那几条硬性要求拆开看。这不是学术概念每条都直接影响你选型和工作方式。第一Pod内的多个容器共享同一个网络命名空间。一个Pod里可以有多个容器它们共用localhost通信共享同一个IP和端口空间。这就意味着Pod内的容器不能用同一个端口比如nginx占用了80同Pod里的其他容器就不能再监听80。这个设计是为了让Pod成为最小的调度单位减少了通信的复杂度。第二Pod与Pod之间可以不经过NAT直接通信无论它们在哪台节点上。这句话是整个CNI模型的核心。想想看如果每次跨Pod通信都要经过NAT转换那IP地址的“身份”就名存实亡了日志审计、服务发现、网络策略全都乱了套。因此所有CNI方案的首要目标是保证集群内任意两个Pod的流量从发出到接收源IP和目标IP始终不变通信路径上不做地址转换。第三Pod与Service之间也能直接通信。Service是一层虚拟抽象背后是iptables或IPVS规则。这条规则通常由kube-proxy实现要求CNI插件只需要保证Pod的流量能出网、能到达节点和集群内部即可Service本身是kube-proxy的活儿。第四节点的网络栈必须能访问Pod。比如你在节点上执行curl 10.244.1.5也应该能通。这条要求看起来简单但其实直接排除了很多“只做NAT”的方案也推动了host-gw和BGP这类纯路由方案的诞生。理解了这四条你再看所有CNI方案本质上都是在回答两个问题一是如何给每个Pod分配一个唯一的IP且网段规划不重复二是如何让这些IP在跨节点的场景下还能被路由到目标节点。前者靠IPAM后者靠路由或隧道。2.2 CNI插件的调用流程CNI这套标准其实很简洁。Kubelet在创建Pod时会去读取CNI配置目录下的配置文件一般是/etc/cni/net.d/目录里面有一个以数字开头的配置文件比如10-flannel.conflist。这个数字就是插件的执行优先级Kubelet会按顺序执行。CNI定义了三类操作ADD、DEL、CHECK。ADD就是给容器添加网络DEL是删除网络CHECK是检查网络配置是否正确。每个插件都需要实现这三个接口。Kubelet调用插件的方式也不是直接执行二进制文件而是把容器信息通过环境变量和标准输入传给插件插件处理完再把结果通过标准输出返回。这里有个容易忽略的点CNI插件常常需要多个二进制配合工作。比如flannel的CNI插件是flannelcalico的CNI插件是calico但它们在底层可能还会依赖IPAM插件比如host-local或calico-ipam。配置里会指定type: flannel和ipam: { type: host-local }Kubelet拿到这个配置后会先执行主插件再执行IPAM插件两个插件都是独立的二进制文件放在/opt/cni/bin/目录下。如果你发现Pod一直ContainerCreating卡在“Failed to create pod sandbox”先别急着怀疑网络打开kubelet的日志看看很大概率是CNI插件二进制文件缺失或者配置文件里的路径写错了。2.3 为什么每个节点都要规划独立子网这是很多新手在手动安装K8s时会忽略的一步。你需要在每个节点上给Pod分配一个不冲突的子网段。比如节点1用10.244.0.0/24节点2用10.244.1.0/24以此类推。这样从全局看所有Pod的IP地址是一个整体网段但具体落在哪个节点上由节点IP段的规划决定。flannel的官方方案里默认使用10.244.0.0/16作为整个集群的Pod CIDR然后切成10.244.x.0/24每个节点领取一个/24。这个分配工作是flanneld进程通过Kubernetes API的lease机制完成的。节点上的flanneld会通过kube-controller-manager来申请一个子网并把分配结果写入节点的podCIDR字段。为什么要这么设计因为IPAM是分布式的每个节点自己申请自己的网段不用中心化的DHCP服务器。好处是扩展性好新节点加入时自动就能拿到不冲突的网段坏处是如果网段规划错了比如两个节点拿到了重叠的网段那整个集群的网络就会出各种玄学问题。这个问题在calico里同样存在配置calico时务必确认IP池和节点的子网规划没有冲突。3. Flannel深入拆解Overlay方案与三种工作模式3.1 VXLAN模式下数据包如何穿越节点flannel最经典的模式是VXLAN。用一句话概括它就是在现有的三层网络上再叠加一层虚拟的二层网络。这就是Overlay网络的核心思路。每个节点上的flanneld会创建一个虚拟网卡flannel.1这个网卡就是VXLAN的VTEP设备。VTEP的全称是VXLAN Tunnel End Point隧道端点。每个节点的flannel.1会有一个Mac地址这个Mac地址对应的IP就是VTEP的IP。Pod的流量到达节点的flannel.1后会被封装成VXLAN报文然后通过UDP发到目标节点的8472端口目标节点的flannel.1接收后解封装再转给目标Pod。详细点说当节点A上的Pod A10.244.0.2要访问节点B上的Pod B10.244.1.5时整个数据链路是Pod A的流量通过veth进入节点A的cni0网桥flannel创建的二层网桥。cni0通过ARP查找到目标IP 10.244.1.5对应的Mac地址。但这个Mac地址不是Pod B的而是节点B上flannel.1的Mac地址。为什么因为flanneld会在启动时通过Kubernetes API拿到所有节点的子网信息知道10.244.1.0/24在节点B上然后把这个子网的路由信息写入主路由表下一跳指向节点B的flannel.1的IP并且它会主动在节点A上添加一条“静态ARP”记录把10.244.1.5的Mac地址对应到flannel.1的Mac地址。这样一来Pod的流量在二层上就会把目标Mac设为flannel.1的Mac从而被送到VTEP设备。flannel.1拿到原始报文后查看文目的IP10.244.1.5根据UDP端口8472把原始报文作为VXLAN payload封装起来外层加上目的节点的IP和UDP头发到节点B。节点B的flannel.1收到后解封装把原始报文扔给本机网络栈网络栈根据路由表再转发到Pod B。这就是为什么flannel用起来“无脑”你不需要在每个节点上配置路由它通过网络层自己搞定了跨节点的转发。代价是每次通信都多了一层UDP封装性能会有所下降。3.2 host-gw模式牺牲灵活性换性能如果你的底层网络本身就支持VLAN、或者各节点的二层可达你可以考虑用host-gw模式。这个模式下flannel完全不做封装直接把Pod子网的路由写进节点的路由表。比如节点A和节点B在同一个二层网络里节点A要访问Pod B的网段10.244.1.0/24flanneld会在节点A上添加一条路由10.244.1.0/24 via 192.168.1.101 dev eth0这里的192.168.1.101就是节点B的物理IP。整条链路就是Pod A - cni0 - 节点A路由表 - eth0 - 节点B - cni0 - Pod B。没有额外封装性能接近裸机网络。但host-gw模式有一个非常硬的限制所有节点必须在同一二层网络内。如果节点跨了VLAN、跨了三层网络这条路由就走不通了因为你不能假设一个二层的下一跳能直接跑到另一个网段去。所以host-gw只适合物理机在同一个网段、没有防火墙拦截的场景。云环境里如果你能保证节点在同一个VPC子网也可以考虑用但如果用云上VPC的路由表来打通的话flannel文档不建议这么干云厂商的路由数量有上限Pod多了会超出配额。3.3 UDP模式一个只剩下教学价值的旧方案flannel早期版本还有一个UDP模式。这个模式其实是在用户态做一次隧道封装性能非常差因为每个包都要经过两次内核态与用户态的切换。flannel官方在很早就默认不使用UDP模式了因为性能比VXLAN差了一个数量级。我看到很多老教程还在讲UDP模式建议直接忽略。你不需要为了“全模式能力”或者“兼容性”去开启它。现在的flannel版本里如果你不指定默认就是VXLAN。UDP模式主要用于“没有VXLAN内核支持”这种极端历史场景今天几乎不存在这种限制。3.4 部署Flannel的配置参数详解flannel的部署极简单一条命令就能跑起来kubectl apply -f https://raw.githubusercontent.com/flannel-io/flannel/master/Documentation/kube-flannel.yml但不建议你直接盲用默认配置。我习惯先把YAML文件拉下来改一改再部署。有两个关键参数值得关注。一个是net-conf.json里的Network字段它决定了Pod的IP段。默认值是10.244.0.0/16。如果你集群里的Service CIDR用了其他网段或者和其它网络的IP段冲突就需要改这里。有两个注意点这个值必须和kube-controller-manager的--cluster-cidr参数保持一致改完之后整条网段就固定了后期不能随意变更否则IPAM会错乱。另一个是Backend字段。默认是Type: vxlan。你可以在这里指定Type: host-gw切换到host-gw模式。如果在VXLAN模式下还想对VNI或者端口做修改可以加VNI: 1和Port: 8472来指定。这里有个我想强调的小坑flannel的VXLAN模式里默认使用的UDP端口是8472但有些网络设备对高UDP端口有限制或者做了负载均衡的干扰。如果发现跨节点通信时有不稳定的丢包可以换个不常用的端口比如4789VXLAN的标准端口但改了之后所有节点的flanneld和防火墙规则也要同步改。另外还有一个配置细节flannel的PublicIP。在云环境里节点往往有内网IP和外网IPflanneld会自动探测节点的IP但如果探测结果不对比如探测到了外网IP会导致VXLAN封装后的外层包无法被对方解析。处理办法是在flanneld的启动参数里指定--public-ip或者在Deployment的环境变量里加FLANNELD_PUBLIC_IP强制指定内网IP。4. Calico深入拆解BGP与完全路由4.1 Calico的架构组件与核心设计calico和flannel走的是截然不同的思路。flannel倾向于“隧道”calico倾向于“路由”。calico的核心组件有四个Felix运行在每个节点上的DaemonSet负责管理防火墙规则iptables/ipset、路由表以及虚拟网卡。BIRDBIRD Internet Routing Daemon负责BGP路由协议的实现。calico把每个节点都当成了一个BGP Speaker节点之间通过BGP协议自动学习Pod网段的路由。calicoctl命令行工具用于管理calico配置比如查看节点状态、设置IP池等。CNI插件calico、calico-ipam负责与Kubelet交互创建网络和分配IP。这套架构的本质是每个节点都是一个自治的路由器通过BGP路由协议把Pod子网的信息在集群内广播。当节点A要访问节点B的Pod时它只需要查询BGP学到的路由把包直接发给节点B的物理IP即可。没有隧道封装性能优秀。4.2 IPIP模式与VXLAN模式冠以“隧道”之名其实各有所长虽然calico主打无隧道但它也提供了IPIP和VXLAN两种封装模式来应对“底层三层网络不可达”的场景。IPIP模式是在原有的IP包外面再套一个IP包协议号是4外层IP头就是两个节点的物理IP。这个模式最大的好处是能穿越不同网段只要外层IP能路由到即可。缺点是会让MTU变复杂如果外层网络的MTU是1500IPIP封装后整个包超出1500会导致分片或丢包。因此在使用IPIP时要么把底层网络MTU调大要么把Pod的MTU减小。calico的VXLAN模式跟flannel的VXLAN大同小异都是UDP封装端口默认4789。区别在于calico在很多版本里对VXLAN的配置更灵活并且可以和BGP模式混合使用。我自己的经验是如果你在同机房、同VPC的节点上跑优先用BGP模式性能最好如果你的节点跨了多个不可直接路由的网段那用VXLAN模式更稳因为不需要对底层网络做过多假设。IPIP模式能不用尽量不用它虽然能透明路由但MTU问题在云环境里经常搞死人。4.3 Felix如何实现NetworkPolicycalico最大的卖点之一是NetworkPolicy。K8s原生NetworkPolicy在很多CNI方案里是“摆设”但calico是真正把它们落实到iptables里的。Felix在节点上维护一组资源每个Pod的IP会被标记成的一个workload endpointFelix会为每个endpoint生成对应的iptables链。当你在集群里定义NetworkPolicy允许A访问B但禁止C访问BFelix会把这些规则转换成iptables规则插入到节点的PREROUTING或FORWARD链中。具体执行时数据包从Pod A出来到达节点的网络栈时会经过cali-FORWARD等自定义链。Felix在这些链上放置了大量规则用于匹配源IP、目标IP、协议、端口。一条Policy里的多个规则是OR关系而不同Policy之间是AND关系这一点我在排错的时候吃过亏。曾经我自定义了两个NetworkPolicy一个只放行80端口另一个放行了所有端口结果发现流量还是不通因为两个Policy之间取的是交集规则严格的那个反而成了瓶颈。4.4 BGP模式下数据链路拆解假设集群有两个节点节点A的Pod网段是10.48.0.0/24节点B的Pod网段是10.48.1.0/24。它们在同一个二层网络里calico采用BGP模式。节点A上的BIRD会通过BGP协议向节点B宣告“10.48.0.0/24 在节点A上”节点B同理。这样两个节点都维护了一张完整的路由表。节点A的Pod要访问10.48.1.5时流量从Pod出去到节点的tunl0或eth0取决于是否封装节点A查看路由表发现10.48.1.0/24的下一跳是节点B的物理IP然后直接把包发过去。节点B收到后根据路由表将包转到目标Pod。相比flannel host-gwcalico BGP模式的最大优势就是“自动学习路由”。host-gw需要flanneld通过API Watch所有节点变化然后手动增删路由表项而calico通过BGP协议自动维护路由节点多了以后你可以在配置里启用Route Reflector来减少BGP连接数但默认的全互联模式在节点数量超过100后会导致BGP会话数爆炸。5. 网络异常排查与常用诊断工具网络排查是基本功。我总结了一套“三层定位法”遇到网络问题先按层次来排查第一层是Pod内网络第二层是节点到Pod第三层是节点间的跨节点通信。5.1 从Pod里看网络栈怀疑Pod网络有问题第一步是进入Pod看看自己的网络到底长什么样。kubectl exec -it my-pod -- ip addr kubectl exec -it my-pod -- ip route如果Pod的IP没有正常配置你会看到eth0的IP为空或者根本没有任何路由。这时就要去看Kubelet日志或者CNI日志journalctl -u kubelet | grep -i cni journalctl -u kubelet | grep -i error有段时间我遇到Pod一直ContainerCreating排查下来发现是cni0网桥被误删了flannel插件的bridge插件没有成功重建网桥。Kubelet的日志里报的是“failed to setup network for pod”这其实说明问题根本不在于K8s本身而在于底层的CNI网络设备状态异常。这种情况有个笨办法但很有效把所有Pod驱逐到一个节点把这个节点的flannel pod重启然后看网桥和路由是否恢复。5.2 不同模式下抓包分析跨节点通信慢或者不通时抓包能直接告诉你问题出在传输层还是网络层。在flannel VXLAN模式下你在源节点抓包会看到UDP封装tcpdump -i eth0 udp port 8472 -nn如果你看到外层目的IP是目标节点的IP但目标节点没有回应那说明目标节点的flannel.1设备有问题或者UDP端口被防火墙挡了。如果你根本看不到8472端口的包那问题更早可能在源节点容器侧就断了。在calico BGP模式下抓包要看是否走了tunl0或者eth0直发。用ip route get 10.48.1.5可以确认下一跳。如果返回的是tunl0说明当前是IPIP封装模式如果返回的是eth0直发说明是纯BGP路由模式。如果跨节点流量通了但延迟高得离谱先检查两个节点的MTU。Pod侧MTU如果大于底层MTU就会导致分片表现就是“时而通时而不通”很典型。查看MTU可以用ip link show flannel.1 ip link show cni0建议Pod侧MTU设置成底层网络MTU减去外层封装的头部大小。比如底层MTU 1500VXLAN封装多了50字节那Pod侧的MTU就是1450这样才不会分片。6. 常见问题速查表网络排错大多数时候是重复劳动我把高频问题和处理思路整理成一个参考表。问题现象可能原因处理办法Pod一直ContainerCreatingkubelet日志报CNI失败flanneld或calico CNI二进制缺失、配置文件权限错误检查/opt/cni/bin/下是否有对应二进制重新apply CNI YAMLPod能访问本节点其他Pod跨节点不通VXLAN端口被防火墙拦截或VTEP设备异常telnet测试8472端口ip link show flannel.1确认设备存在跨节点通但延迟高MTU不匹配导致分片检查底层网络MTU调低Pod侧MTU使用host-gw模式但跨网段节点不通节点不在同一二层网络检查节点间VLAN隔离改用VXLAN模式calico创建Policy后流量被切断Policy间是AND关系规则过严检查所有Policy的selector和port用calicoctl get networkpolicy查看配置节点重启后Pod网段丢失etcd没有保存IPAM数据确认etcd备份正常重启后检查calico/fannel的statusPod IP分配重复CIDR规划冲突检查所有节点的podCIDR确认没有重叠访问Service有时通有时不通kube-proxy模式问题或后端Pod IP变化检查kube-proxy日志确认后端endpoints更新正常这些场景里最容易被忽视的两个一是“防火墙拦截UDP”二是“MTU不匹配”。如果集群网络看起来一切正常但延迟或丢包不稳定先看这两项往往比深入内核要快得多。7. 选型建议与个人实操体会7.1 适用场景对照每次有人问我集群到底该选flannel还是calico我的建议都是先看你的诉求。下面这份对照表简单粗暴对比项flannelcalico部署难度极低一条命令搞定相对复杂需要关注BGP和IP池性能host-gw接近裸机VXLAN有损耗纯路由模式性能好封装模式有损耗跨网段支持VXLAN模式可以跨三层IPIP/VXLAN可以跨三层NetworkPolicy不原生支持强大的原生支持大规模集群节点多了会依赖API Watch有压力BGP自动路由更适合大规模但需部署Route Reflector云环境适用性VXLAN模式对云网络很友好纯路由模式强依赖云网络支持我的经验是如果你只想快速搭一套内部用的集群节点不多、没有强网络隔离需求flannel是最省心的如果你要上生产Pod数量上千有严格的访问控制需求那calico是更合适的选择前提是你愿意花时间理解BGP和它的iptables模型。7.2 一些我在实操中才明白的细节有些坑只有自己跑过一遍才知道。第一无论用哪个插件都建议先在测试环境验证“节点重启”这个场景。一台节点重启后flanneld或calico的Felix需要在新节点环境里重新申请IPAM子网、重建网桥、恢复路由表。如果你的etcd没有备份节点重启后Pod的IP可能错乱导致集群状态不健康。第二别轻易在生产环境直接改CNI插件。我之前有过一次经历为了想要NetworkPolicy从flannel切换到calico结果一切换就发现节点之间的路由还没收敛好整整折腾了一个小时。如果要切换准备一个完全独立的备份集群或者干脆在业务低峰期做做好回滚预案。第三calico的NetworkPolicy功能再强大也不要滥用。每条Policy都会转化成大量iptables规则规则数量过多时节点的网络转发性能会下降得很明显。我建议你把规则控制在合理的数量区间定期用calicoctl get ippool和calicoctl get bgppeer检查配置把不再使用的规则清理掉。第四不管用哪个组件都要留意“天然的MTU差异”——集装箱网络的MTU是围绕底层基础设施设计的不是你想调就能调的。在云环境里如果底层启用了Jumbo Frame9000字节MTU那VXLAN链路上的MTU可以做相应放大但如果是普通1500的物理网络别硬撑老老实实设1450。最后还想说一点网络不是“配完就不管”的东西。Pod的IP漂移、节点扩缩容、防火墙策略调整每一个变化都会影响网络层。建议你从一开始就做好基础网络基线记录包括每个节点的Pod子网分配、路由表的关键路由、CNI配置文件这些对日后故障排查的帮助比你想象的还要大。