上周帮一个团队排查线上超时问题,业务方坚持说代码没问题,查来查去最后定位到跨节点通信链路上的MTU配置不对,短报文能过、大包全被丢。这类问题我见得太多了,大多数人对于Pod间通信的理解停留在"能ping通就行",一旦链路出问题就不知道从哪里下手。Kubernetes里的Pod间通信方式其实是一套完整的设计逻辑:单Pod内共享网络栈、同节点走网桥、跨节点走Overlay或路由协议、上层用Service做负载均衡和发现。这篇文章把这套逻辑串起来讲清楚,同时给出排错时的检查顺序,适合刚开始接触K8s网络的初学者,也适合被网络问题折磨过的实战玩家。
1. 先从一张虚拟网卡说起:单Pod内的通信真相
要看懂Pod间通信,我建议先把视角切到一个Pod内部。很多文章上来就讲跨节点,实际上最容易忽略的反而是一个最基础的场景:同一个Pod里的两个容器,它们之间根本不需要"网络通信"这回事。
原因在Kubernetes的Pod模型。一个Pod不是一堆容器的简单捆绑,而是让这些容器共享同一个网络命名空间(Network Namespace)。具体来说,每个Pod启动时,kubelet会先创建一个基础设施容器(比如pause容器),这个容器不跑业务,职责就是创建并持有网络命名空间。之后Pod里其他容器创建时,都会通过容器运行时把自己“塞进”pause容器持有网络命名空间里。
Pod内的容器看到的是一套网卡、同一个IP地址、同一张路由表。两个容器要通信,直接通过localhost互访,端口不冲突就完事。我见过不少新手在部署Nginx和PHP-FPM同Pod组合时,怀疑是不是要配置什么网络,其实根本不用——只要监听127.0.0.1或者Pod IP都能通,走的是Linux内核协议栈内部路径,根本不经过物理网卡。
为什么要这样设计?两个原因。一是效率:容器共享网络栈后,进程间通过loopback通信,省去了跨网络栈的封包解包和协议栈开销,性能和同一个宿主机上的两个进程通信几乎没区别。二是标准化:对Pod外部来说,不管Pod里有1个容器还是3个容器,它的网络身份(IP、端口、主机名)只有一个。Service、网络策略、探针等都能以Pod为粒度设计,不用关心容器拆分。
1.1 用命令验证共享网络栈
想亲眼看一下这个机制,两条命令就够了。先进入Pod里的容器A执行ip addr,记下IP和网卡名;再进入同一Pod的容器B执行相同命令,你会发现IP完全相同、网卡列表也一模一样。这两处输出如果不一致,说明这个容器没有共享Pod的网络栈,可能是配置了独立网络命名空间,或者你误操作到了别的Pod里。
同理,在容器A里启动一个监听5000端口的服务,容器B直接curl localhost:5000就能访问,不需要任何路由和防火墙配置。
1.2 单Pod通信的注意事项
这个模型有一个硬性约束:同一个Pod里的容器不能用同一个端口监听,因为是共享的本地地址空间,一监听就会直接冲突报错。我见过有团队把一个Pod里的多个业务容器都默认监听8080,结果服务在集群里注册的时候地址一样,调度器都看不出问题,但流量根本没法隔离到具体容器。
另外,这个共享网络栈机制也带来了一个安全隐患:容器的隔离性不是完全独立。如果安全要求很高,就得在镜像设计和容器权限上做额外约束,不能指望网络命名空间来隔离Pod内容器。
2. 同节点Pod的"串门"路线:veth pair与cni0网桥
两个Pod在同一个节点上,它们之间怎么通信?先记住两条:一条虚拟网线(veth pair),加一个虚拟交换机(Linux Bridge)。
每个Pod在宿主机上都会有一对veth网卡,一头在Pod的网络命名空间里,名字通常叫eth0;另一头挂在宿主机上,名字是一串随机的vethXXXX。这一对网卡中间是虚拟网线连起来的,从Pod eth0里出去的包,从宿主机vethXXXX那侧出现。所有宿主机侧的veth,最终都接在一个叫cni0(CNI实现不同,网桥名字可能有差异,原理一样)的Linux网桥上。
网桥的原理和物理交换机差不多。它在内核里维护一张MAC地址到端口的映射表。Pod A要给Pod B发包时,包从Pod A的eth0出来,经过veth对走到宿主机的vethA,cni0网桥根据目标MAC地址查端口表,直接从这个虚拟交换机的另一个端口(vethB)转发出去,最后落到Pod B的eth0。整个过程发生在L2数据链路层,不经过宿主机路由,同节点Pod间通信延迟极低,吞吐看本机内核转发能力。
2.1 一个容易搞混的点:host-gw不是二层通信
这里我插一个常见误解:很多人看到Flannel有host-gw模式,以为这种模式也是靠网桥做二层通信,其实不对。host-gw走的是三层路由,cni0网桥只负责Pod接入网桥后的二层互通,跨节点部分完全靠宿主机路由表逐跳转发。也就是说,cni0解决的是"本节点Pod如何接入网络",路由或Overlay解决的是"不同节点的Pod在网络上如何被关联起来",这是两回事,别混在一起理解。
2.2 宿主机上验证链路状态的命令
在宿主机上验证这套链路,我一般用这几条命令:
ip link show | grep veth:看宿主机侧的veth网卡是否正常brctl show cni0:查看网桥下面挂的端口列表(部分新发行版需要先安装bridge-utils)tcpdump -i cni0 icmp:在网桥上看ARP和ICMP包,直观看到二层转发过程
如果同节点Pod间ping不通,问题通常集中在这么几个位置:Pod的eth0和veth对没正确挂上、cni0网桥被误删或异常重启、网卡promisc模式丢失导致桥接失效。排查顺序从ip link看网桥和veth状态开始,比一上来就翻网络策略高效得多。
还有一个实践经验:同节点走网桥的链路几乎没有额外协议开销。所以对性能敏感、需要频繁互访的两个服务,如果条件允许,可以考虑用调度策略让它们落在同一节点上。但要注意,这本质上是在用可用性换性能,节点挂了两个服务一起不在,反而会造成更大的故障范围,需要权衡。
3. 跨节点Pod的"异地传输":VXLAN与Underlay路由的正面较量
跨节点是Pod间通信最核心也最复杂的场景。K8s网络模型定了个硬性要求:集群内每个Pod都获得一个唯一的集群范围内IP,并且任意两个Pod之间无需NAT即可直接互通。但物理网络可不知道你的Pod网段,宿主机自己的IP和Pod IP往往不在一个网段,跨节点的包根本送不出去。
解决这个矛盾有两条主流流派:Overlay和Underlay。按我的理解,Overlay说白了就是把Pod的IP包"装进"宿主机IP包里运输;Underlay则是让物理路由协议直接学习Pod网段路由。两条路线没有绝对优劣,看场景选择。
3.1 Overlay方案:VXLAN怎么工作
Overlay的代表是Flannel的VXLAN模式、Cilium的VXLAN模式。数据包路径大概是这样的:Pod A(10.244.1.2)发到Pod B(10.244.2.3),包先通过cni0走到宿主机,宿主机路由表发现目标IP是10.244.2.0/24网段,下一跳指向一个叫flannel.1的VTEP设备。这个VTEP负责做VXLAN封装:在原始IP包外面加一层UDP头(目标端口4789)和VXLAN头,外层源地址是本节点IP,目标地址是对端节点IP。隧道包经过物理网络到达对端宿主机,对端VTEP解封装,还原出原始的Pod IP包,再交给cni0网桥发给目标Pod。
VXLAN的本质是UDP封装,对底层网络几乎没要求,三层网络也能跑,隧道建起来就行,这是它最大的优点。代价是每包多出约50字节的开销(VXLAN头、UDP头、IP头),封解包要消耗CPU,实测吞吐通常比纯路由模式低10%到20%。
3.2 MTU问题:小包通大包不通的元凶
跨节点排错里最常见也最经典的坑是MTU。宿主机的物理网卡MTU如果是1500,VXLAN隧道口的MTU就应该设置成1450甚至更低,这少掉的几十个字节是给封装修头预留的。如果这个配置没对齐,你会遇到一个特别典型的现象:小包和ping都通,一传大文件、大HTTP响应就卡死或超时,因为大于隧道MTU的包被丢弃,分片逻辑又没生效。很多团队在这种问题上白查好几天代码,最后发现只是MTU的锅。
3.3 Underlay方案:BGP和主机路由直通
Underlay的代表是Calico的BGP模式、Flannel的host-gw模式。它们不做包封装,而是直接往宿主机路由表里写"Pod网段往哪个节点走"。Flannel host-gw模式通过etcd分发所有节点的Pod网段信息,每个节点动态维护路由表;Calico更进一步,用BGP协议把Pod网段路由广播给物理交换机和路由器,让网络设备直接知道Pod IP该怎么走。
这样数据包经过的路径最短,没有封装开销,转发性能几乎和裸机网络一样,是追求性能时的首选。代价是对底层网络有要求:host-gw要求节点间二层可达,云环境里一般同VPC内满足;Calico BGP则要交换机支持BGP或配合网络设备调整配置。
我的选型建议很务实:大多数云上环境,如果你不想招惹云平台VPC路由表的限制,Flannel VXLAN或者Calico IPIP模式起步没问题,稳定、少折腾。如果集群规模大、流量高,或者对P99延迟敏感,直接上Calico BGP或者Cilium的eBPF模式。别在一个数百节点的集群上为了省事坚持用host-gw,路由表维护和排错会让人怀疑人生。
3.4 跨节点排错时容易忽略的端口
我实际踩过一个典型的坑:两个节点用Calico BGP,但忘记在防火墙上放行TCP 179端口,BGP Peer一直建不起来,路由表不更新,跨节点始终不通。排查时calicoctl node status一直显示peer未建立,纠结了很久才发现是安全组规则问题。跨节点排错永远不要忽略宿主机防火墙这一层,VXLAN模式要放行UDP 4789,BGP模式要放行TCP 179,IPIP模式要放行IP协议号4。
4. Service这座"中转站":ClusterIP、DNS与服务发现
Pod有了IP、能互通之后,接下来是K8s里使用频率最高的通信方式:通过Service访问一组Pod。为什么非得绕这一层?因为Pod是"一次重建换一个IP(默认情况下)"的临时资源,不适合直接当访问入口。Service提供一个稳定的逻辑地址(ClusterIP),由kube-proxy在宿主机上完成到后端Pod的转发。
4.1 kube-proxy的两种模式
Service背后的实现机制分两个层面。第一层是kube-proxy,它监听API Server里的Endpoints或EndpointSlice资源,把Service后端Pod的IP和端口列表同步到每个节点,然后用iptables或IPVS写入转发规则。
默认的iptables模式利用DNAT,把访问ClusterIP:Port的包改写为目标PodIP:Port。IPVS模式则是内核态的负载均衡,直接把ClusterIP绑定到IPVS虚拟服务器上,支持rr、wrr、lc等调度算法。集群内Service规则超过几百条之后,iptables的链遍历开销会肉眼可见,IPVS是更稳妥的选择。我自己维护的集群在Service数量到200条左右时,就能感觉到新建连接延迟差异。
4.2 ClusterIP是用ICMP测不了的
第二层值得单独说:有多少人拿ping ClusterIP去测Service通不通?我见过太多次了。ClusterIP是虚拟IP,它只有和Service指定端口组合起来才有意义。kube-proxy规则根本不处理ICMP,所以ping一个ClusterIP大概率是无响应,这不代表Service坏了。正确测法是curl ClusterIP:Port或者用应用协议去访问。
还有一点要分清:ClusterIP只存在于kube-proxy规则的网络命名空间里,你在Pod里能访问它是因为设置的路由规则让包走了本机转发,它不是一个真实存在的网卡地址。理解这一点,后面排错才不会钻牛角尖。
4.3 DNS服务发现与常见问题
服务发现方面,CoreDNS负责把Service名称解析成ClusterIP。同命名空间里,应用直接用Service名就行;跨命名空间需要写全限定域名:<service>.<namespace>.svc.cluster.local。K8s会自动在每个Pod的/etc/resolv.conf里配置ndots、search domain等参数,这也是为什么很多容器里curl一个Service名就能找到它。
4.4 一次印象深刻的Service链路排查
我做过一次典型的Service链路排查,过程印象深刻。现象:A服务访问B服务偶尔超时。排查时先确认B的Pod存活正常,进A容器curl B的Service ClusterIP,通;curl PodIP,也通;但业务就是偶发失败。最后用ipvsadm -Ln看了IPVS调度规则,结合conntrack -L看连接表,发现大量连接被分布到同一个刚重启的Pod上,那个Pod启动慢,导致这些连接全部超时绕了一大圈。其实如果先把后端Pod的Ready状态和Endpoints确认清楚,能少大半时间。这也是为什么我建议所有人在验证Service链路时,第一步永远是三件事:看Pod是否Ready,看Endpoints里有没有对应IP,最后才去查kube-proxy规则。
5. 更高级的通信姿势:Headless Service与StatefulSet的稳定标识
日常微服务大多通过ClusterIP访问就够了,但有些场景必须有更直接的Pod寻址方式,比如数据库主从、消息队列、分布式存储。它们需要知道每个后端Pod的确切IP和状态,要访问到具体某个实例,而不是被负载均衡随机分流。这时候要用Headless Service。
5.1 ClusterIP设为None的含义
Headless Service的创建方式和普通Service几乎一样,区别是spec.clusterIP设为None。这个特殊值让K8s不分配ClusterIP,也不创建对应的iptables/IPVS转发规则。DNS方面,CoreDNS会为它生成一条记录,但返回的不是虚拟IP,而是后端每个Pod的IP列表。客户端拿到多个IP后自己决定访问哪一个,负载均衡策略完全由应用控制。对需要自己实现读写分离、分片的系统来说,这个自由度太关键了。
5.2 StatefulSet与稳定DNS名
配合StatefulSet使用时,Headless Service的价值更明显。StatefulSet的每个Pod有稳定的主机名和序号,比如zk-0、zk-1、zk-2。它的Pod名称会作为DNS子域名的一部分。假设Service叫zk-hs,命名空间ns1,那么zk-0.zk-hs.ns1.svc.cluster.local能稳定解析到zk-0这个Pod的IP,即使Pod重启IP变了,这个DNS名也不变。这样ZK、ES、ClickHouse这类有状态服务做节点发现时,不用自己维护IP列表。
5.3 使用Headless Service的注意事项
这里有个容易忽略的点:Headless Service如果配合Pod的hostname和subdomain字段一起使用,集群内部才具备完整的"稳定网络标识"效果。如果后端Pod用的是普通Deployment,Headless Service的DNS记录会指向多个随机Pod,效果和ClusterIP负载均衡差别不大,别指望它提供稳定的序号化访问。
对于有状态集群内部节点发现,我的经验是优先让应用使用Pod的DNS名称而不是裸IP来发现彼此。比如ZK配置里填zk-0.zk-hs.ns1而不是某个具体的Pod IP,这样即使Pod被重新调度、IP变了,配置文件完全不用改。踩过一次坑之后,我在所有有状态应用的配置模板里都固定了这一条:用DNS名,不要用IP。
6. Pod间通信排错实录:按这个顺序查不用慌
最后这部分是我的保留节目——排错顺序。Pod间通信出问题的时候,很多人第一反应就是翻YAML、找网络策略,乱查一气。我的顺序固定是下面这套,时间成本最低,方向感最强。
第一步,确认目标Pod状态和调度位置。一条kubectl get pod -n <namespace> -o wide搞定:看Pod是否Running且Ready,记下源Pod和目标Pod分别落在哪个节点。如果目标Pod本身是CrashLoopBackOff或Pending,那根本没到通信问题的层面,先解决Pod本身。
第二步,确认Pod的基本网络属性。用kubectl get pod -o yaml | grep -E 'podIP|hostIP|nodeName'看Pod IP和宿主机IP,确认Pod不是hostNetwork模式。如果是hostNetwork,检查入口完全不同,要按宿主机端口来测。这一步还能排查Pod IP是否为0.0.0.0这种异常。
第三步,从源Pod发到目标Pod IP。用kubectl exec -it <源Pod> -- ping <目标PodIP>和curl测试关键端口。这里记住,NetworkPolicy可能不允许ICMP(ping被策略放行控制),出现"ping不通但业务端口通"是完全可能的。反过来,如果你开了NetworkPolicy但没有放行对应流量,这条链路上所有包都会被拒。
第四步,判断问题在哪个层面:同节点还是跨节点。同节点就到宿主机上看二三层状态:veth在不在、cni0桥状态健康不健康。跨节点先看宿主机路由表ip route里有没有对应Pod网段的路由,再看隧道或路由协议状态:Flannel VXLAN看flannel.1设备链路和隧道对端,Calico跑calicoctl node status确认BGP peer状态,顺带确认UDP 4789、TCP 179这些端口有没有被防火墙挡住。
第五步,如果涉及Service,按"Endpoints → kube-proxy规则 → 应用日志"的顺序查。kubectl get endpoints <svc>看有没有后端;iptables -t nat -L -n | grep <clusterIP>或ipvsadm -Ln看转发规则在不在;规则空转但后端正常,多半是kube-proxy没同步,先尝试重启kube-proxy或等它重试。注意一个常见失误:明明有Pod但Endpoints为空,十有八九是Pod标签和Service selector没对上。
第六步,排查MTU和防火墙。小包通、大包不通,十有八九是MTU配置问题,熟悉VXLAN封装开销后直接检查隧道口MTU和宿主机物理网卡MTU是否匹配。防火墙方面,不管云安全组还是节点上的firewalld或ufw,一定要确认放行K8s工作所需端口。顺便检查各节点时间同步是否正常——如果节点时钟偏差大,BGP和证书协商都会出现各种奇怪问题,这个算是我遇到的隐藏彩蛋。
上面这套流程走完,九成的Pod间通信问题都能定位到根因。剩下的,基本都是不同CNI插件之间的路由冲突、多个网络平面叠加、以及数据面组件自身Bug,这类问题要结合CNI日志和内核conntrack信息继续深挖。到那一步,已经不是在"排查通信方式"了,而是在排查整个集群网络。
我在实际项目中的一个体会是:与其等问题出现再查,不如在集群上线时就固定好一套通信基线,比如统一记录各节点的MTU、CNI版本、NetworkPolicy清单。Pod间通信方式再多,核心链路也就那么几条,把这些链路的健康状态监控起来,踩坑的次数会少得多。