作为一个常年泡在k8s集群里的运维,我太清楚网络这块的水有多深了。很多朋友用kubeadm顺利地把集群初始化成功,Master和Node节点状态也都是Ready,结果一创建Pod就傻眼:Pod一直Pending,或者同节点的Pod能通,跨节点的Pod ping不通,更别提Service的ClusterIP访问时好时坏了。这些问题的根源几乎都指向同一个环节——容器网络。这一篇,我就把k8s网络通信这条线彻底捋一遍,重点是现在生产环境里最主流的两个CNI插件:flannel和calico。我会从它们各自的原理、适用场景、部署要点,到实际排障手段都过一遍,让你看完之后不仅能选型,遇到问题也知道从哪里下手。
这篇文章适合刚完成集群基础搭建、正准备接入业务流量的人,也适合那种“网络通了但不知道为啥通”的运维同学。我不会只给你甩一堆YAML,而是会把每个关键步骤背后的“为什么”讲清楚,这样你在企业内部做技术方案评审、出网络故障报告时,才有东西可写、有依据可讲。
1. K8s网络通信的核心模型与CNI机制
1.1 K8s网络模型的四个硬性约定
K8s对Pod网络提出了四点要求,这四点直接决定了我们后面选什么插件、怎么配置。第一,集群内每个Pod都拥有独立的ClusterIP,所有Pod之间可以不通过NAT直接通信;第二,所有Node上的Pod都能与所有其他Node上的Pod通信;第三,Pod与Service之间也能直接通信;第四,Pod访问外部网络时通过NAT映射,外部访问Pod则通过Service或其他入口。简而言之,K8s希望Pod就像传统虚拟机一样,拿到一个真实可路由的IP,任何两个Pod之间能裸连。
这个模型听起来简单,但落地时有个最直接的矛盾:Pod IP是K8s自己分配出来的网段,比如默认的10.244.0.0/16或者10.233.0.0/16,这些网段在物理网络里根本不存在,路由器也不认识。那怎么让跨节点的Pod流量到达对方?这就是CNI插件存在的根本意义——它需要在每台Node上做手脚,让Pod网段的路由在集群内部变得“可见”,或者把Pod流量封装起来穿过物理网络。
1.2 CNI插件的标准工作流程
CNI(Container Network Interface)解决的其实是“容器运行时不知道怎么给容器配网”的问题。Kubelet在启动一个Pod时,会调用CRI(Container Runtime Interface)创建容器,但容器的网络命名空间是空的,没有网卡、没有IP。这时候CRI会回过头来调用CNI插件,把网络配置好。
我举个最直观的例子。Kubelet调用CNI时,实际上是在执行一个可执行文件,并传入一个网络配置的JSON。拿最基础的bridge插件来说,它做的事情是:创建一个Linux网桥cni0,用veth pair把容器的一端接入cni0,另一端放到容器的网络命名空间里,同时给容器内的eth0配置IP、设置默认路由。整个流程就是下面这个JSON在驱动:
{ "cniVersion": "0.3.1", "name": "mynet", "type": "bridge", "bridge": "cni0", "isGateway": true, "ipMasq": true, "ipam": { "type": "host-local", "subnet": "10.244.0.0/24" } }你注意看后面的ipam字段,这里用的是host-local这种IP地址管理插件,它的逻辑是从一个子网里跳着分配地址。而flannel和calico之所以被称为“CNI插件”,是因为它们在标准的bridge或类似组件之上,还实现了自己的IPAM(比如calico-ipam)和网络数据路径。这也解释了为什么你在每台Node上会看到一堆cni0、flannel.1、tunl0之类的网卡:这些都是插件在配置网络时创建的虚拟设备。
1.3 两种数据路径:Overlay和路由方案
既然底层物理网络不认Pod网段,插件们就得各显神通。思路大体分两类。第一类是Overlay(覆盖网络),典型代表是flannel的VXLAN模式:它把Pod的报文封装在UDP里,外层用Node的真实IP做源和目的,到了目标Node再解封装。好处是对底层网络没有要求,只要Node间三层能通就行;坏处是每个包都要多一层封装和解封装,CPU开销和时延都上去了。
第二类是纯路由方案,典型代表是calico的BGP模式,或者flannel的host-gw模式。它们的思路是:直接在每台Node上加路由表,比如“去10.244.1.0/24请走下一跳10.168.1.21”,这样Pod流量不经过隧道,直接通过物理网卡转发,性能和传统网络几乎一样。但代价是,它要求所有Node的网络二层是互通的,也就是说Node之间能直接通过ARP找到对方,如果中间隔着三层设备或跨机房,路由模式就玩不转,这时候只能回归overlay,或者开启calico的IPIP封装。
这两种路径没有绝对的优劣,只有适配度的问题。我在第4节会专门做选型对比,这里先记住结论:网络环境简单、对性能敏感,优先考虑路由模式;网络环境复杂、Node跨网段多,优先考虑overlay。
2. Flannel实战:最简单直接的Pod互通方案
2.1 Flannel的三个后端到底选哪个
Flannel最初是做覆盖网络出名的一个组件,它的配置里有一项backend,决定了数据路径。很多人配置时只抄官方YAML,根本不看Backend类型,结果被网络性能坑了才发现问题。
第一个是历史上最早的udp后端,用用户态的flanneld进程封装UDP包,性能极差,现在基本没人用了,我提到它纯粹是为了让你在旧文档里看到时知道这是个过时方案。第二个是默认的vxlan后端,这是目前生产环境里最常见的选择。VXLAN是内核实现的overlay方案,把Pod的二层帧封装到UDP里,默认走4789端口,由内核的VTEP设备flannel.1处理,性能比用户态UDP方案好得多。第三个是host-gw,它直接在每个Node上写路由,Pod流量从主机网卡原样出去,性能和原生网络几乎没有区别。但就像前面说的,它要求所有Node在同一个二层网络里,也就是通过交换机直接相连,中间不能有路由器。
我给出的选型建议很直接。如果是测试环境、云服务器节点跨VPC、或者底层网络自己都说不清二三层拓扑,直接用vxlan,省心。如果确定整个集群所有Node都在一个广播域内,且业务对时延敏感,那就用host-gw,把MTU还回来,性能好不少。
2.2 部署Flannel的详细过程
用kubeadm初始化的集群里,部署flannel基本就是apply一个文件的事。但要注意,kube-controller-manager启动参数里必须带--allocate-node-cidrs=true和--cluster-cidr=10.244.0.0/16,否则Node不会拿到PodCIDR,flannel也就没法分配网段。
官方manifest里最重要的一段是ConfigMap,里面定义了Pod网段和Backend类型。这是典型配置:
apiVersion: v1 kind: ConfigMap metadata: name: kube-flannel-cfg namespace: kube-system data: net-conf.json: | { "Network": "10.244.0.0/16", "Backend": { "Type": "vxlan" } }部署完之后,花两分钟做三件事来确认它正常工作。第一,看Pod状态,kubectl get pods -n kube-system -o wide,确保flannel-kube-agents的所有Pod都Running。第二,在Node上执行ip addr show flannel.1,确认VTEP设备有IP。第三,看路由表,ip route | grep 10.244,你会看到类似10.244.1.0/24 dev flannel.1 scope link的条目,同时每个Node之间也有一条通过自己VTEP IP的前缀路由。如果这些都有,基本可以断定网络组件工作正常。
2.3 VXLAN模式下的跨节点通信路径
我用自己的经验把VXLAN模式下跨Node的通信路径拆开讲,保证你理解后排查问题有了体感。
同Node上的两个Pod通信最简单:PodA的包从veth进到cni0网桥,网桥根据目的MAC直接转发给PodB的veth,全程不出宿主机。
跨Node就复杂了。Node1上的PodA(10.244.1.2)要访问Node2上的PodB(10.244.2.3)。PodA发现目标IP不在自己所在的子网,于是把包发往默认网关,也就是cni0网桥的IP。宿主机收到后查路由表,找到10.244.2.0/24应该走flannel.1这个设备。内核于是开始VXLAN封装流程:它需要一个VTEP对端的MAC地址,这个MAC查询ARP表,没有就发ARP请求。flanneld守护进程会监听ARP请求,并且用自己的机制回复目标VTEP的MAC。拿到MAC后,内核在原始报文外面包一层UDP头,源IP是Node1的物理网卡IP,目标IP是Node2的物理网卡IP,端口是4789。这个UDP包到了Node2后,Node2的内核解封装,还原出原始的Pod报文,再根据路由表转发给Node2上的cni0,最后送到PodB。
如果你要抓包验证,在Node1上执行tcpdump -i eth0 udp port 4789 -nn -e,就能看到源和目的MAC、IP都是Node的物理地址,里面嵌套的才是Pod的IP信息。这个“外层是Node地址,内层是Pod地址”的特征,就是我判断流量是否走了VXLAN封装的关键。
2.4 Flannel的先天短板
我必须坦白讲,flannel胜在简单,但它的短板在企业生产环境里会越来越明显。
第一,它不支持NetworkPolicy,这意味着你无法通过K8s原生的网络策略去做Pod级别的访问控制。如果你的企业有等保要求,或者安全部门明确要求做微隔离,那flannel基本可以告别生产环境了。第二,它没有BGP能力,所有路由都是flanneld自己计算下发,集群规模大了之后,路由条目和更新延迟都是问题。第三,它在多网卡环境下容易犯迷糊,如果Node有多块网卡,你得手动指定--iface参数,否则flannel可能选错物理网卡,导致跨节点通信全部失败。
我在实际项目里见过太多因为网卡选错导致整个集群网络瘫痪的案例,这里提醒你注意:每个Node上都要检查flanneld的启动参数和日志,确认它用的网卡IP确实是Node之间能互通的那个IP。
3. Calico实战:BGP与NetworkPolicy的组合拳
3.1 Calico的核心组件和职责
Calico相比flannel,是一套更完整也更复杂的三层网络方案。它的架构里有几个关键角色:
Felix,跑在每个Node上,负责管理和配置iptables规则、路由规则以及网卡接口,确保Pod的流量按照策略被放行、阻断或转发。BIRD,这是BGP路由协议的客户端实现,每个Node上的BIRD会把本机管理的Pod网段通过BGP广播给其他Node,同时从别的Node学习路由。calicoctl或kubectl calico则是管理工具,用于查看和配置Calico的策略、节点状态。
你有没有发现,Calico本质上是在把整个集群当成一个“微型互联网”来管理——每一台Node都相当于一个路由器,通过BGP协议交换各自管理的Pod网段路由信息。这种设计让它天然适合大规模部署,因为BGP本来就是互联网级的路由协议。
3.2 IPIP和VXLAN两种封装模式怎么选
Calico的数据路径有两种封装模式加一种无封装模式。IPIP模式是Calico最经典的封装方案,它把Pod报文封装进一个IP包里,外层协议号是4,也就是IP in IP,对应的虚拟设备是tunl0。VXLAN模式和flannel类似,走UDP 4789。无封装的就是BGP纯路由模式,Pod报文不封装,直接通过物理网络转发。
部署时你会在calico的ConfigMap或安装参数里看到CALICO_IPV4POOL_IPIP这个关键项,它有几种取值。Always表示所有跨Node流量都走IPIP封装,Never表示从不封转,走纯BGP路由,CrossSubnet表示只有跨子网的流量才封装,同子网内走纯路由。这个设计非常实用,因为同机架同交换机下的流量封装纯属浪费,CrossSubnet能兼顾性能和跨网段连通性。
我的建议是:如果网络环境简单,把CALICO_IPV4POOL_IPIP设为Never,享受最好的性能;如果Node跨VPC或跨机房,设成Always或CrossSubnet都行,但要清楚封装带来的时延损耗。切换这个参数需要重建calico的IPPool资源或者重启所有calico-node,业务会短暂中断,一定要在维护窗口操作。
3.3 部署Calico的两种方式对比
现在企业里部署Calico的主流方式有两种。一种是老式的manifest直装方式,官方会给你一个包含所有RBAC、DaemonSet、ConfigMap的YAML包,你改改网段参数就可以apply。另一种是新版的Operator方式,需要先安装tigera-operator,再定义一个CustomResource看板。
我给多数生产环境的建议是用Operator方式。原因很简单:它把Calico组件的生命周期都托管了,版本升级是平滑的,配置变更也能通过自定义资源来调整,很适合后续持续运维。如果只是临时测试或者维护一套存量老集群,那manifest直装也没问题。
Operator安装大致是这么几步。先apply operator清单:
kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.0/manifests/tigera-operator.yaml然后创建一个CustomResource文件,关键参数是cidr和encapsulation。cidr必须和kubeadm初始化时指定的--pod-network-cidr一致,否则Pod网络会乱套。示例:
apiVersion: operator.tigera.io/v1 kind: Installation metadata: name: default spec: calicoNetwork: ipPools: - name: default-ipv4-ippool cidr: 10.244.0.0/16 encapsulation: VXLANCrossSubnet这个封装方式我选的是VXLANCrossSubnet,不同子网的Node之间走VXLAN,同一子网内走纯路由。灵活,而且不用太担心底层网络限制。
部署完成后,kubectl get pods -n calico-system看到calico-node和calico-kube-controllers都Running,再用calicoctl node status查看BGP peer状态。如果BGP会话是Up,说明各Node已经通过BGP交换了路由,跨Node通信马上就能通。
3.4 NetworkPolicy:Calico比Flannel多出来的核心竞争力
Calico能在企业生产环境里被大量选用,很大一部分原因是它原生支持Kubernetes NetworkPolicy。K8s的NetworkPolicy是一套声明式API,允许你定义Pod之间哪些流量可以被放行。它不只是在网络层做限制,还能到四层的端口级别,甚至配合Calico的扩展可以做到七层的HTTP方法级别。
我举个例子。现在有一个后端服务叫backend,只允许前端服务frontend访问它的8080端口,其余Pod一概不许。那NetworkPolicy就长这样:
apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: backend-policy namespace: production spec: podSelector: matchLabels: app: backend policyTypes: - Ingress ingress: - from: - podSelector: matchLabels: app: frontend ports: - port: 8080这段配置的意思非常直白:我不管你是哪个命名空间的,只要Pod的label不是app=frontend,你访问backend的8080端口就会被Calico在宿主机上用iptables规则给DROP掉。
你可能会问,这不就是安全组干的事吗?对,但区别在于:安全组是配在虚拟机或物理机层面的,粒度粗;NetworkPolicy是配在Pod层面的,能够做到同一台Node上不同Pod的精细隔离。这在微服务架构里特别有价值,比如合规审计要求各个服务之间必须白名单访问,NetworkPolicy就是落地工具。我服务过的所有企业客户,凡是安全要求严格的,最后几乎都选了Calico。
4. 企业场景选型:Flannel还是Calico
4.1 六个维度的直接对比
很多刚接触K8s的人会问:既然Calico功能这么全,为什么还有人用Flannel?答案在运维成本和性能开销上是有差别的。
| 对比维度 | Flannel(vxlan/host-gw) | Calico(IPIP/BGP/VXLAN) |
|---|---|---|
| 部署复杂度 | 低,apply即用 | 中高,组件多,BGP概念需理解 |
| NetworkPolicy | 不支持 | 完整支持,扩展策略更强 |
| 数据路径 | overlay或host路由 | 纯BGP路由或隧道封装 |
| 大规模集群支持 | 一般,路由更新机制朴素 | 好,BGP天然适合大规模 |
| 底层网络要求 | vxlan模式要求低,host-gw要求二层通 | 纯BGP要求二层通,封装模式要求低 |
| 资源占用 | 少,flanneld很轻 | 较多,每节点有Felix和BIRD进程 |
单独看每一种方案,都能跑通,但你把它放到企业的具体约束里,答案就很清楚了。
4.2 不同场景下的现实选择
如果你的集群是开发测试环境、边缘节点的小集群、或者业务对性能完全不敏感,我就推荐用Flannel vxlan。它配置简单,出问题的概率小,团队成员对它的理解和维护难度都低。我自己做PoC测试时也经常临时装一个flannel,十分钟搞定,方便验证业务逻辑。
但如果是生产环境,并且你看到下面几个条件中的任意两个,我建议直接上Calico。条件一,规模超过30个Node,希望路由收敛更快更稳;条件二,需要NetworkPolicy做应用层访问隔离,跑合规流程;条件三,集群跨多个网段,底层网络复杂;条件四,有一个独立网络团队,愿意为网络方案多花点运维精力。
别小看这个决策。曾经有个客户,用Flannel跑了半年,业务稳定,后来甲方要求提供网络隔离方案,才发现需要整个切到Calico。这个迁移虽然不算地狱级难度,但要维护窗口、要同步改安全策略、还要处理存量连接的中断,工作量大不少。所以从一开始就做好选型,比后面亡羊补牢划算得多。
4.3 从Flannel迁移到Calico的注意点
如果你需要在已有的Flannel集群上切换到Calico,这里有几个我踩过的坑,必须坦白提醒你。
首先,网络层的基础设施要清干净。Flannel在每台Node上创建了cni0网桥、flannel.1设备,还往iptables里写了很多规则,这些不会因为你卸载flannel的DaemonSet就自动消失。你必须手动在每个Node上删掉这些残留,具体操作包括ip link delete flannel.1、ip link delete cni0,以及清空CNI配置文件目录/etc/cni/net.d/下以10-开头的flannel配置,换成calico生成的配置。
其次,切换期间Pod会中断,这个是绕不过去的。要提前通知业务方,最好是选在一个几乎没有流量的时间窗口操作。我建议先部署好Calico的所有组件,确认BGP会话正常,再一次性卸载Flannel。操作顺序是反过来的话,集群会短暂出现“没有任何CNI可用”的状态,新建Pod全部失败。
最后,原有的ServiceClusterIP不会变,因为service网段在kubeadm阶段就定死了,CNI插件不碰这块。但是NetworkPolicy是你之前没有的东西,切换后如果业务网络异常,先看看是不是有什么历史遗留的默认拒绝策略,不要一上来就怀疑Calico本身出故障,这个方向性错误我见过太多次了。
5. 网络故障排查实战记录
5.1 排查网络问题的基础工具箱
K8s网络故障和传统网络故障有个不同点:很多问题藏在不同层级的虚拟设备里。我平时排查的时候有一个固定顺序:先看Pod的内部状态,再逐层往外看宿主机的网卡、路由、iptables,最后才抓包看数据包是否真的到了某个点。
常用的命令集合我列一下:kubectl get pod -o wide确认Pod所在的Node;kubectl exec -it <pod> -- /bin/sh进容器里看路由和网卡;宿主机的ip addr看各网卡IP;ip route看路由表;ip neigh看ARP邻居表;arping测试二层连通性;ping -c 4测试三层连通性;tcpdump -i <iface>抓包看协议栈行为;iptables -L -n -t nat/filter查一下有没有被意外DROP。如果你发现某个Pod网络时通时断,优先怀疑MTU,这个坑我在下一小节细说。
5.2 经典场景一:跨Node的Pod ping不通
这是所有K8s网络问题里最经典的一个。一个Pod能ping通同Node的另一个Pod,但是跨Node就不行。遇到这个情况,我按下面的思路逐步排查。
第一步,在源Pod里kubectl exec执行ping <目标PodIP>,确认不通。第二步,在源Pod所在的宿主机上ping <目标PodIP>,如果宿主机能通而Pod不通,问题一定出在Pod里的路由或宿主机的策略上;如果宿主机也不通,问题就更靠外一层。第三步,检查源Node和目标Node之间的物理连通性,ping <目标NodeIP>,确认底层网络没断开。第四步,查看源Node路由表,有没有10.244.2.0/24这样的前缀路由。如果没有,说明CNI没有把路由同步完成,要么是flanneld有问题,要么是Calico的BGP会话没起来。第五步,检查防火墙,很多云环境的安全组默认只放行某些端口,VXLAN的UDP 4789、Calico的BGP 179端口都需要在Node的安全策略里放行。
还有一招很多新手不知道:检查宿主机iptables的FORWARD链。K8s的流量很多都要经过宿主机的FORWARD链,而不是INPUT链,但某些安全加固脚本会把FORWARD策略默认设为DROP,这就导致Pod流量到了宿主机直接被丢。我在某客户那里排查了一个下午,最后发现就是某个基线加固脚本把FORWARD设成了DROP。你可以用iptables -L FORWARD -n看策略,发现是DROP就赶紧加放行规则。
5.3 经典场景二:Pod之间有IP但Service访问失败
这种情况的表现是,Pod的IP之间可以直接访问,但换成ClusterIP就不行,或者时好时坏。
ClusterIP是虚拟IP,由kube-proxy负责把流量转发到后端的Pod上。现代K8s默认用IPVS模式来做负载均衡。排查时,先确认kube-proxy的转发模式是iptables还是ipvs:kubectl logs -n kube-system kube-proxy-xxxx | grep mode。如果是ipvs模式,看ipvsadm -Ln里是不是有ClusterIP的记录,没有的话说明Service的endpoints没同步或kube-proxy异常。然后检查IPVS规则里的后端地址,如果有多个Pod,看是不是都在。
还有一种情况是连接跟踪表被打满,现象表现为新连接建立失败而旧连接正常。这是在Node上查看cat /sys/module/nf_conntrack/parameters/hashsize和conntrack -L数量,如果接近表容量上限,要么调大参数,要么排查是不是有大量短连接导致表项激增。
5.4 经典场景三:MTU不一致导致的大包不通
这个问题我愿意单独拿出来讲,因为它真的非常隐蔽。现象是ping通,但curl业务接口很慢或直接超时,或者ping -s 1472都不通但普通ping通。
本质原因:物理网卡MTU是1500,VXLAN封装会把Pod报文加一个50字节左右的外层头,那Pod网卡MTU如果不减,逻辑上超过物理承载上限,大包就被丢弃。Flannel的vxlan模式默认会给Pod网卡设置MTU为1450,Calico的VXLAN也是类似逻辑。如果有人在创建CNI配置时手动改了MTU,或者物理网络本身MTU是1400(比如某些叠加了其他隧道的网络),就会导致包能发出去但对方收不完整。
排查方法用kubectl exec到Pod里执行cat /sys/class/net/eth0/mtu,然后逐层比对。处理方式是把Pod的MTU调成比物理网卡小适当值,或者在CNI配置里显式指定MTU。这个坑在云环境里尤其常见,因为云厂商的底层网络可能已经叠加了各种隧道,物理MTU可能根本不到1500。
5.5 排障逻辑的最终心得
我可以很负责任地讲,K8s网络排障,八成的坑都可以归纳到三个方向:路由不对、策略阻塞、MTU不匹配。路由不对,看路由表和BGP状态;策略阻塞,看iptables和云安全组;MTU不匹配,看各层网卡的MTU值和抓包对比。你把这三个方向想清楚,哪怕不知道某个具体命令,也能顺着思路找到问题。
我个人在实际操作中的体会是:别一上来就拿tcpdump去抓包,先在一层一层把“哪里通、哪里不通”确认完,最后再抓包验证细节,效率才是最高的。很多新手喜欢直接抓包,结果抓了一堆数据也不知道看什么,浪费半天时间。链路状态确认是排障的地基,地基都不打牢,分析纯属碰运气。
另外再分享一个小技巧:网络插件在集群初始化之后第一件事要做的就是检查Pod的网段规划。很多人在搭集群时随手填一个Pod网段,后来发现和公司内部某段网络冲突,或者和VPC路由冲突,导致Pod之间访问出现各种奇怪的路由跳变。这个规划一定要在初始化之前做,和生产环境的现有网段避开,最好用A类私有地址里比较不常用的段,比如10.244.0.0/16、10.233.0.0/16。这种事看起来技术含量不高,但直接影响集群的长期稳定,值得认真对待。