拿到一个新搭好的Kubernetes集群,很多人第一件事就是跑一个nginx,看到Pod变成Running就以为万事大吉。等到真正业务联调的时候,Web应用访问不上数据库、订单服务调不通网关,才发现网络这一层从来没验证过。Kubernetes从Pod到Service这条链路,表面上就是“Service域名一解析、ClusterIP一访问、请求就转发到Pod”,实际上中间隔了Pod IP、ClusterIP、NodeIP三层网络,每一跳都有独立的转发逻辑和排错工具。这篇按我在多个集群里挨个扒链路、抓包、看规则表积累下来的经验,从Pod内部一路验证到Service外部,把每一步的原理、命令和坑位都讲清楚。无论你是在自己电脑上跑minikube做实验,还是在生产集群排查一次“诡异”的超时,这套验证思路都能直接用。我不想把这篇文章写成文档式的“K8s网络原理”,更想带你把链路亲手走一遍。读完之后你应该能做到:拿到一个集群,不管它用的是哪一种CNI插件、kube-proxy跑在什么模式,都能理直气壮地说清楚“一个请求到底经过了哪几跳”。1. 为什么要单独验证这条链路:三层网络的“地狱模式”1.1 集群里其实有三张“网”,别混在一起谈很多网络问题排查不清楚,根源是把三张网混成了一团。第一张是Pod网络,由CNI插件给每个Pod分配一个集群内可路由的IP,比如172.20.1.5,Pod和Pod之间靠它通信。第二张是Service网络,也就是ClusterIP网段,比如10.96.0.0/12,这个网段非常特殊,它没有一个真实的物理载体,ClusterIP不归任何网卡所有,完全靠节点上的转发规则“虚拟”出来。第三张是Node网络,也就是宿主机本身的IP,在云上对应虚拟机/物理机的内网地址,外部流量要进集群,第一步一定踩在Node IP上。打个比方,就像快递寄到一栋写字楼:Node IP是写字楼地址,ClusterIP是公司前台总机号,Pod IP是具体工位号。你拨总机,前台需要知道哪个工位的人负责这件事,然后帮你转接;而外部快递只认写字楼地址。三层地址各管一段,排查时也必须分段落定位。1.2 全链路验证到底要验证哪几段我习惯把Pod到Service的完整路径拆成五个分段来验证:分段链路内容核心机制验证工具第一跳同节点Pod间通信veth pair + cni0网桥ip route、tcpdump第二跳跨节点Pod间通信路由、隧道封装(VXLAN/IPIP)路由表、隧道抓包第三跳Pod访问Service ClusterIPkube-proxy生成DNAT规则iptables/ipvsadm第四跳Service域名解析到ClusterIPCoreDNS + Pod的resolv.confnslookup、dig第五跳外部流量进入集群NodePort/LoadBalancer/Ingresscurl、conntrack这五段不是孤立的,而是层层依赖。Pod到Pod如果不通,后面Service转发一定不通;Service转发不通,DNS解析出来也没用。所以我下面按顺序展开,每一段都给出验证命令、原理解释和最容易踩的坑。2. 搭一套能复现链路的最小验证环境2.1 环境选型:不是所有K8s发行版都适合做网络实验我见过有人直接在云厂商托管集群里做实验,结果想抓包看隧道封装,发现底层接口完全看不到;也有人用本地单节点集群,测跨节点通信时压根没有第二个节点。做全链路验证之前,先想清楚你要复现什么。如果你只是验证Service和DNS,单节点minikube就够;如果你要验证跨节点Pod通信和隧道封装,至少需要两个节点的集群。我个人更推荐用k3s或者kind起一个两节点集群——k3s默认集成flannel,网络链路真实,资源占用低;kind虽然也能起多节点,但节点本身是容器,网络层又套了一层虚拟化,观察隧道包时会有干扰。云上实验的话,在托管集群里用两台独立Node即可。提示:不管用哪种方式,验证环境要尽量贴近生产。同一套链路,flannel VXLAN和calico BGP走的数据路径完全不同,结论不能互相套用。2.2 准备好两个探针Pod,比什么都重要很多人验证网络喜欢用kubectl run临时起一个busybox,结果发现里面连curl都没有,更别说tcpdump、iproute2了。我最常用的是带全套网络调试工具的镜像,这一类镜像里比较典型的是netshoot系列的调试镜像,里面装了ip、tcpdump、dig、nslookup、curl、nc这些工具,一个探针就能干完所有活。我会创建一个专门用来做链路验证的Deployment,配置探针probe-tools,同时再部署一个测试后端web-demo,比如nginx镜像,作为Service的后端。为了让同节点和跨节点的场景都能测到,把两个Pod调度到不同节点:apiVersion: apps/v1 kind: Deployment metadata: name: probe-tools spec: replicas: 2 selector: matchLabels: app: probe-tools template: metadata: labels: app: probe-tools spec: containers: - name: tools image: netshoot command: ["sleep", "infinity"] imagePullPolicy: IfNotPresentapiVersion: apps/v1 kind: Deployment metadata: name: web-demo spec: replicas: 2 selector: matchLabels: app: web-demo template: metadata: labels: app: web-demo spec: containers: - name: nginx image: nginx:1.27 ports: - containerPort: 80再创建对应的Service:apiVersion: v1 kind: Service metadata: name: web-svc spec: selector: app: web-demo ports: - port: 80 targetPort: 80这套环境准备好之后,下面每一跳都可以直接在探针里操作。3. 第一跳:同节点Pod间通信,从veth到cni03.1 从Pod内部看自己的网络命名空间进入探针Pod之后,先看两样东西:IP地址和路由表。kubectl exec -it deploy/probe-tools -- ip addr输出里会看到Pod自己的eth0,比如172.20.1.5/24,还有一个lo回环。这个eth0并不真实存在于宿主机上,它只是一个虚拟网卡,真正连通的是宿主机上另一端的veth设备。接着看路由表:kubectl exec -it deploy/probe-tools -- ip route默认路由几乎都是:default via 172.20.1.1 dev eth0 172.20.1.0/24 dev eth0 proto kernel scope link src 172.20.1.5这个172.20.1.1是cni0网桥在宿主机上对应网段的网关地址。也就是说,Pod里的数据包从eth0出去之后,下一个动作是到cni0这个二层网桥,由网桥来决定往哪个veth口转发。3.2 在宿主机上定位veth对,理解“一根虚拟网线”veth pair是成对出现的虚拟网卡,就像一根虚拟网线,一端插在容器网络命名空间里变成Pod的eth0,另一端插在宿主机的网络栈上,挂到cni0网桥上。在宿主机上执行:ip link | grep -B1 "veth"你会看到很多vethxxx@ifN这样的设备,其中@ifN里的N,就是对端网卡在Pod网络命名空间里的序号。可以通过ethtool -S vethxxx查看peer_ifindex进一步确认到底对应哪个Pod。我常用的方法简单粗暴:在每个探针Pod里执行ip link show eth0,记下它的接口序号和MAC地址,再到宿主机上ip link | grep -A1 <MAC>,立刻就能把veth对应关系找出来。3.3 同节点连通性验证:在cni0上抓包才看得清同节点的两个Pod互ping时,流量只在宿主机内部走一遍:源Pod的eth0 → 源veth → cni0网桥 → 目的veth → 目的Pod的eth0,全程不经过宿主机物理网卡。验证命令很简单:kubectl exec -it deploy/probe-tools -- ping -c 3 172.20.1.6如果想亲眼看到这个链路,最有效的抓包位置是cni0:tcpdump -i cni0 -nn icmp -c 10能同时看到两个方向ICMP报文从cni0进出的过程。这里有个经验:很多人习惯在Pod里直接抓包,但容器内tcpdump只能看到自己eth0方向的流量,看不到网桥上的广播和转发动作,定位问题时视野太窄。要判断“这个包到底有没有出Pod”,在宿主机veth那头抓;要知道“这个包在二层怎么流转”,在cni0上抓。到这里,第一跳验证完成。同节点通信是K8s网络最基础的部分,一般不会出问题,它真正的意义在于帮你建立“数据包从容器出来,先走veth到网桥”的直观印象,后面理解Service转发会轻松很多。4. 第二跳:跨节点Pod通信,CNI容器网络后端的选择4.1 为什么跨节点就是另一回事了同节点通信靠网桥就能解决,但不同节点之间呢?Pod IP是每个节点分配的不同网段,节点之间隔着一层物理网络,数据包要跨IP网络传输,必须有路由条目和转发机制。这里就引出了CNI插件后端方案的差异,这也是网络上讨论最多、最容易产生分歧的地方。在探针Pod里ping一个位于其他节点的Pod IP,比如172.20.2.3。先看路由表,此时默认路由还是走eth0,但到达对端Pod网段的路由,在宿主机上会指向某个特殊接口——可能是tunl0或flannel.1,也可能是直接走物理网卡。这个差异完全取决于CNI后端。4.2 三种常见CNI后端方案,一张表说清楚方案数据面封装方式典型端口/协议性能适用场景flannel VXLAN内核VXLANUDP封装UDP 4789中等中小集群,部署简单calico IPIPIPIP隧道IP-in-IP封装协议号4中等偏上跨网络但不想暴露Pod网段calico BGP(underlay)路由直通无封装BGP 179最好网络设备支持BGP的大型集群cilium eBPFeBPF直接路由无封装或VXLAN视配置而定最好,可观测性强对性能和可观测性要求高选方案本质是“性能”和“部署简单”之间的权衡。小团队从flannel起步完全没有问题,它最大的优势是依赖少、内核里就能跑。但要记住,VXLAN封装带来额外负载,吞吐量会比underlay方案低一些。我在实际压测里见过,同样两台机器,flannel VXLAN和calico BGP的TCP吞吐能差出30%以上,这是网络报文多封装一层头部造成的必然开销。4.3 实际验证跨节点链路:盯住隧道端口验证跨节点通信时,不要只关心ping通不通,更要看数据包走的是哪条路。假如集群用的是flannel,在源节点宿主机上执行:tcpdump -i any udp port 4789 -nn -c 20去ping对端Pod的同时,你会看到大量从本机eth0出去的UDP 4789报文,里面封装了原始的ICMP包。这就是VXLAN隧道:Pod发出的原始数据包在源节点被内核封装进UDP,送到对端节点的4789端口,对端解封装后再从自己的cni0转发给目标Pod。如果集群是calico IPIP,抓包方式改成:tcpdump -i any proto 4 -nn -c 20proto 4就是IP-in-IP协议,能看到外层是两台宿主机IP,内层是Pod IP。识别不同后端后,排查方向会很不一样:flannel出了问题是看VXLAN端口通不通;calico IPIP除了看端口,还要确认IPIP协议有没有被防火墙拦;underlay方案则要检查交换机上的路由条目和BGP peer状态。4.4 一个很容易被忽略的坑:MTU不一致导致“ping通但业务不通”这是我在一个跨节点验证里踩过的典型问题。两个节点Pod之间ping能通,但一用curl传大数据就卡死或超时,排查一圈发现是隧道接口MTU和物理网卡MTU不匹配。VXLAN隧道会额外占用50字节,如果物理网卡MTU是1500,隧道接口默认就得降到1450,但某些情况客户端没有正确协商MTU,导致需要分片的包被丢弃。验证手段很简单,用ping -s 2000发一个大包测试,或者查一下隧道接口的mtu值:ip link show flannel.1 ip link show tunl0这个坑特别容易出现在混合环境里,比如有的Node是VM,物理网卡MTU本身就只有1450,链路MTU层层递减,到应用层就只剩1400了。遇到跨节点业务大包超时,先查MTU,不要一上来就怀疑代码。5. 第三跳:Pod直连Service,ClusterIP的魔法5.1 ClusterIP不存在于任何网卡上,它活在规则里跨节点Pod通信解决了,下一步就是从Pod访问Service ClusterIP。第一次知道ClusterIP不在任何节点上“落地”的时候,很多人会觉得不可思议。它不绑定真实网卡,没有ARP应答,甚至你在节点上ip addr都看不到它。那数据包发到ClusterIP为什么能通?因为每个节点上的kube-proxy会实时watch API Server里的Service和Endpoints对象,把“ClusterIP → 一组后端Pod IP”的映射关系写进本机的NAT规则里。当一个Pod访问ClusterIP时,数据包先正常走出Pod的网络命名空间,到达cni0,然后进入宿主机的网络栈,在netfilter的PREROUTING/OUTPUT链上被规则匹配,执行DNAT,把目标地址从ClusterIP改写成一个具体的Pod IP。后续转发就复用跨节点/同节点Pod通信链路。ClusterIP的“IP层是假的”,但“转发链是真的”。5.2 kube-proxy三种模式,性能差异可以很大kube-proxy有userspace、iptables、ipvs三种模式,现在基本见不到userspace了,生产环境主要在iptables和ipvs之间选。iptables模式用NAT规则链实现负载均衡:一个Service对应一条KUBE-SVC链,链上挂多个KUBE-SEP链,每个KUBE-SEP负责DNAT到一个后端Pod。后端数量变大时,规则数量线性增长,最直观的副作用是规则更新慢,集群内Service频繁变动时可能出现短暂的转发断开。ipvs模式基于内核的IPVS模块,本身就是为负载均衡设计的,用哈希表维护Service和RealServer映射,后端增删只改表项,不产生海量iptables规则链条。它还支持rr、lc、sh等多种调度算法,适合大规模集群。模式规则规模负载均衡算法对大规模集群友好度userspace用户态代理轮询差,基本淘汰iptables与后端数量线性增长随机/轮询(statistic)Service多时更新慢ipvs哈希表,规则数基本固定rr、lc、sh等高,推荐5.3 动手看一条ClusterIP规则的完整链路我用一个探针Pod持续访问web-svc,在节点上抓规则。先看当前kube-proxy模式:kubectl logs -n kube-system -l k8s-app=kube-proxy | grep -i "Using"如果是iptables模式,用iptables-save把规则导出来看:iptables-save -t nat | grep -A 10 "KUBE-SVC"典型输出长这样:-A KUBE-SVC-XXXXX -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-POD1 -A KUBE-SVC-XXXXX -j KUBE-SEP-POD2 -A KUBE-SEP-POD1 -p tcp -m tcp --dport 80 -j DNAT --to-destination 172.20.1.5:80 -A KUBE-SEP-POD2 -p tcp -m tcp --dport 80 -j DNAT --to-destination 172.20.1.6:80KUBE-SVP里用了statistic --mode random实现50%概率分流,另一条规则落到第二个后端。至于为什么第一个后端显示DNAT到172.20.1.5:80,实际上KUBE-SEP里还会有MASQUERADE规则,把源IP也伪装掉,这样后端Pod回包时能正确回到宿主机网络栈,再由conntrack恢复原始连接。如果是ipvs模式,用ipvsadm看:ipvsadm -Ln | grep -A 3 "10.96.12.10"输出里能看到Tcp 10.96.12.10:80 rr,下面是两个Realserver地址。相比一长串iptables规则,ipvs的视图简洁得多。验证这一步时我最常用的命令是:在探针里curl http://web-svc:80,同时在节点上开一个对Pod IP的tcpdump,比如:tcpdump -i cni0 -nn host 172.20.1.5 and port 80 -c 10注意:抓包位置在cni0,能看到报文目标地址已经被改成了Pod IP。对比Pod eth0上的报文目标地址(原始ClusterIP),非常直观地感受到什么叫DNAT。提示:如果Pod访问ClusterIP报connection refused,先别急着怀疑规则。检查一下Service的selector有没有正确匹配到Pod,kubectl get endpoints web-svc如果为空,规则链上根本没有后端可转,直接拒连。这个是最常见的“Service不通”原因,通常不是规则坏了,而是Endpoints没生成。6. 第四跳:Service名称到ClusterIP,DNS发现链路6.1 CoreDNS在集群里的角色,说穿了就是个名字翻译器网络通了之后,应用一般不会直接用ClusterIP,而是用Service名字。集群内建的DNS服务(默认是CoreDNS)负责把Service名字翻译成ClusterIP。这件事还要结合Pod自己的resolv.conf才能完整解释。进入探针Pod,执行:cat /etc/resolv.conf输出类似:search default.svc.cluster.local svc.cluster.local cluster.local nameserver 10.96.0.10 options ndots:5nameserver指向CoreDNS的ClusterIP;search是搜索域列表;ndots:5意味着,如果域名里点的数量小于5,解析器会先把域名拼上search域依次尝试,而不是直接走FQDN查询。这个细节日常不起眼,排查DNS慢的时候它就是主角。6.2 实测DNS解析:短域名和FQDN会走完全不同的路径在探针里执行:nslookup web-svc你会看到它实际上查询的是web-svc.default.svc.cluster.local,并且返回的A记录就是ClusterIP。再加一条:dig web-svc.default.svc.cluster.local +short返回的结果一样。但用dig web-svc +short,走的就是ndots逻辑下的多轮搜索,可能返回不权威的中间结果,也可能因为search域顺序导致解析到错误目标。我建议所有验证都直接用FQDN,例如web-svc.default.svc.cluster.local。很多线上DNS“诡异超时”,原因就是应用用了短名,而search域列表较长,每层搜索都要等待查询超时后才进入下一层。特别是Pod内没有配置完整FQDN、又依赖外部短域名的情况下,一次DNS查询可能被“人为”拖到几秒。6.3 三个常见的DNS验证误区第一个误区是只看解析结果,不看解析耗时。DNS解析通不代表快,dig末尾会给出耗时,如果每次查询几百毫秒,就要检查CoreDNS性能或上游DNS配置,而不是直接跳过。第二个误区是忽略Headless Service的特殊表现。无头Service没有ClusterIP,CoreDNS返回的不是虚拟IP,而是后端Pod IP列表。所以验证时要区分普通Service和无头Service,它们的DNS行为天然不同。第三个误区是Service名字里带了不符合RFC的字符,比如下划线。某些版本不会给出友好的报错,而是直接解析失败或返回空结果,排查时很容易联想到网络策略或其他玄学问题,实际上就是DNS名字本身不合法。7. 第五跳:从集群外部访问,NodePort、LoadBalancer与Ingress7.1 NodePort完整链路:外部流量怎么进到集群内部内部链路都通了,最后验证从集群外部到Service的路径。最基础的方式是NodePort。创建Service时指定type: NodePort,集群会在每个Node上开放一个固定端口,比如30080。外部请求访问任意Node的30080端口,数据包进入宿主机的网络栈后,被iptables/ipvs规则匹配,走一遍DNAT,转发到后端Pod。如果入口Node不是后端Pod所在Node,还会发生一次跨节点转发。这也是NodePort模式容易被诟病的地方:额外一跳不仅增加延迟,还可能因为转发节点的资源争抢影响长连接服务质量。可以通过externalTrafficPolicy: Local让入口流量只转发到本Node的Pod,但副作用是后端Pod分布不均时可能出现部分Node“空转”。验证命令很简单:curl http://<nodeip>:30080在Node上抓包看链路:tcpdump -i eth0 -nn tcp port 30080 -c 10再结合iptables-save | grep KUBE-NODEPORT就能看到规则是如何从NodePort链跳转到KUBE-SVC链的。7.2 LoadBalancer和Ingress的定位,别混为一谈NodePort是基础,生产上几乎都会再套一层负载均衡或Ingress。LoadBalancer类型实际就是云厂商提供一个四层负载均衡器,把公网流量转发到各Node的NodePort(不同云厂商实现略有差异);Ingress则是集群内部的七层入口网关,根据域名和路径路由到不同的Service。很多人在云上同时开了LoadBalancer和Ingress,然后疑惑“为什么外部域名通了,但直接用NodePort访问不行”,这时候先分清两者:LB负责四层接入,Ingress负责七层路由,如果Ingress入口本身就绑在NodePort上,那么NodePort不通,Ingress一定也不通。组件工作层级转发依据适合场景NodePort四层端口测试、无LB环境LoadBalancer四层端口/IP云上对外暴露TCP/UDP服务Ingress七层域名/路径HTTP/HTTPS路由、网关7.3 我推荐的对外访问验证顺序外部访问出问题时,我习惯按这个顺序从里到外排查:在集群内Pod里直接curl ClusterIP:验证Service转发本身。在Node上curl ClusterIP:验证节点网络栈到Service正常。在Node上curl本机NodePort:验证本机NodePort规则。在另一个Node上curl NodePort:验证跨节点转发和回程路径。在集群外通过LB或Ingress域名访问:验证防火墙、安全组和入口配置。这五步每一层都有清晰的边界,能精确把问题锁定在一个很小的范围内,而不是盲目重启kube-proxy或者怀疑CNI插件。8. 这条链路里最容易翻车的5个坑,踩过才长记性8.1 坑一:kube-proxy模式与预期不一致排查问题前先确认kube-proxy到底跑的哪种模式,我吃过这个亏。一度以为集群是ipvs模式,手动改了一条iptables规则做验证,结果规则完全没生效,排查半天发现集群实际是iptables模式。确认方式:kubectl logs -n kube-system -l k8s-app=kube-proxy | grep -i "Using"如果输出Using ipvs,那看规则用ipvsadm;如果输出Using iptables,那就用iptables-save。模式和工具匹配,才能看到真实规则。这个问题在集群升级或重新安装后尤其容易出现。8.2 坑二:conntrack造成的“假连通”,坑了一下午有一次排查线上偶发超时,现场访问很稳定,但业务就是有周期性报错。最后发现问题出在conntrack:Service后端Pod滚动更新后,老连接还留在conntrack表里,继续着原来的DNAT目标,新连接却已经分配到新Pod上。表面上看“刚才还通”,实际上你验证的可能是一条已经建立的老连接。正确做法是验证连通性时强制新建连接,curl加--connect-timeout,必要时主动清一下conntrack条目:conntrack -L | grep <service-cluster-ip> conntrack -D -d <service-cluster-ip>生产环境千万别乱删conntrack,但排查问题时这招能迅速确认是不是连接跟踪在作怪。8.3 坑三:DNS缓存和search域联手制造“幽灵解析”Pod里跑的业务如果使用了系统级DNS缓存,你在Pod里nslookup看到的是新解析结果,但业务进程可能还拿着缓存的旧ClusterIP。更麻烦的是有些镜像内置了dnsmasq之类的缓存,和K8s默认的ndots:5叠加后,短域名的查询行为完全不可预测。排查时不要只看nslookup,也别只看应用的日志,两个都要看。如果Pod里解析正常、应用就是不通,进业务容器里看看/etc/resolv.conf,确认它到底走哪里的DNS,再决定是清缓存还是调整应用配置。8.4 坑四:NetworkPolicy在“默默执法”K8s默认没有NetworkPolicy时,所有Pod之间互通;一旦有人创建了NetworkPolicy,哪怕只有一个,规则也只对匹配到的标签生效。遇到过不止一次:两个命名空间之间Pod互相能ping通,但业务端口访问被拒,翻遍Service和iptables都找不到问题,最后发现是某个NetworkPolicy只放行了特定端口。验证方式:kubectl get networkpolicy -A重点看policy的podSelector和ingress规则,比对一下出问题Pod的标签。这一步虽然简单,但很容易被忽略,因为网络策略不像Service和Endpoint那么直观。8.5 坑五:安全组和防火墙对NodePort的“选择性放行”云厂商的安全组规则有时候很隐蔽。之前遇到一个现象:nc -vz nodeip 30080通了,但业务访问老是超时。排查才发现安全组只放行了特定源IP的访问,而nc正好是从放行网段里发出来的,业务流量来自另一个网段,被安全组拦在门外。做外部访问验证时,一定要用真实业务流量的源IP和目标端口来测,不要拿一个“测试环境专用”的IP测完就下结论。同时检查所有环节的放行策略:云安全组、节点防火墙、本地iptables的INPUT链。四层和七层各测一遍,才能确定问题到底在哪一段。整个链路验证到这里就算是完整走了一遍。我个人的习惯是,新集群上线前一定先做一遍这五跳验证,把每段的结果截图存档,后面业务出问题的时候可以直接对比“基准线”,省掉大量从零排查的时间。最后再分享一个小技巧:验证过程中所有抓包文件统一命名加时间戳,比如link-check-svc-20250101.cap,因为网络问题往往不是“现在坏了”,而是“某次变更之后坏了”,有历史抓包对比,才能快速确认回归点在哪一环。
拿到一个新搭好的Kubernetes集群,很多人第一件事就是跑一个nginx,看到Pod变成Running就以为万事大吉。等到真正业务联调的时候,Web应用访问不上数据库、订单服务调不通网关,才发现网络这一层从来没验证过。Kubernetes从Pod到Service这条链路,表面上就是“Service域名一解析、ClusterIP一访问、请求就转发到Pod”,实际上中间隔了Pod IP、ClusterIP、NodeIP三层网络,每一跳都有独立的转发逻辑和排错工具。这篇按我在多个集群里挨个扒链路、抓包、看规则表积累下来的经验,从Pod内部一路验证到Service外部,把每一步的原理、命令和坑位都讲清楚。无论你是在自己电脑上跑minikube做实验,还是在生产集群排查一次“诡异”的超时,这套验证思路都能直接用。我不想把这篇文章写成文档式的“K8s网络原理”,更想带你把链路亲手走一遍。读完之后你应该能做到:拿到一个集群,不管它用的是哪一种CNI插件、kube-proxy跑在什么模式,都能理直气壮地说清楚“一个请求到底经过了哪几跳”。1. 为什么要单独验证这条链路:三层网络的“地狱模式”1.1 集群里其实有三张“网”,别混在一起谈很多网络问题排查不清楚,根源是把三张网混成了一团。第一张是Pod网络,由CNI插件给每个Pod分配一个集群内可路由的IP,比如172.20.1.5,Pod和Pod之间靠它通信。第二张是Service网络,也就是ClusterIP网段,比如10.96.0.0/12,这个网段非常特殊,它没有一个真实的物理载体,ClusterIP不归任何网卡所有,完全靠节点上的转发规则“虚拟”出来。第三张是Node网络,也就是宿主机本身的IP,在云上对应虚拟机/物理机的内网地址,外部流量要进集群,第一步一定踩在Node IP上。打个比方,就像快递寄到一栋写字楼:Node IP是写字楼地址,ClusterIP是公司前台总机号,Pod IP是具体工位号。你拨总机,前台需要知道哪个工位的人负责这件事,然后帮你转接;而外部快递只认写字楼地址。三层地址各管一段,排查时也必须分段落定位。1.2 全链路验证到底要验证哪几段我习惯把Pod到Service的完整路径拆成五个分段来验证:分段链路内容核心机制验证工具第一跳同节点Pod间通信veth pair + cni0网桥ip route、tcpdump第二跳跨节点Pod间通信路由、隧道封装(VXLAN/IPIP)路由表、隧道抓包第三跳Pod访问Service ClusterIPkube-proxy生成DNAT规则iptables/ipvsadm第四跳Service域名解析到ClusterIPCoreDNS + Pod的resolv.confnslookup、dig第五跳外部流量进入集群NodePort/LoadBalancer/Ingresscurl、conntrack这五段不是孤立的,而是层层依赖。Pod到Pod如果不通,后面Service转发一定不通;Service转发不通,DNS解析出来也没用。所以我下面按顺序展开,每一段都给出验证命令、原理解释和最容易踩的坑。2. 搭一套能复现链路的最小验证环境2.1 环境选型:不是所有K8s发行版都适合做网络实验我见过有人直接在云厂商托管集群里做实验,结果想抓包看隧道封装,发现底层接口完全看不到;也有人用本地单节点集群,测跨节点通信时压根没有第二个节点。做全链路验证之前,先想清楚你要复现什么。如果你只是验证Service和DNS,单节点minikube就够;如果你要验证跨节点Pod通信和隧道封装,至少需要两个节点的集群。我个人更推荐用k3s或者kind起一个两节点集群——k3s默认集成flannel,网络链路真实,资源占用低;kind虽然也能起多节点,但节点本身是容器,网络层又套了一层虚拟化,观察隧道包时会有干扰。云上实验的话,在托管集群里用两台独立Node即可。提示:不管用哪种方式,验证环境要尽量贴近生产。同一套链路,flannel VXLAN和calico BGP走的数据路径完全不同,结论不能互相套用。2.2 准备好两个探针Pod,比什么都重要很多人验证网络喜欢用kubectl run临时起一个busybox,结果发现里面连curl都没有,更别说tcpdump、iproute2了。我最常用的是带全套网络调试工具的镜像,这一类镜像里比较典型的是netshoot系列的调试镜像,里面装了ip、tcpdump、dig、nslookup、curl、nc这些工具,一个探针就能干完所有活。我会创建一个专门用来做链路验证的Deployment,配置探针probe-tools,同时再部署一个测试后端web-demo,比如nginx镜像,作为Service的后端。为了让同节点和跨节点的场景都能测到,把两个Pod调度到不同节点:apiVersion: apps/v1 kind: Deployment metadata: name: probe-tools spec: replicas: 2 selector: matchLabels: app: probe-tools template: metadata: labels: app: probe-tools spec: containers: - name: tools image: netshoot command: ["sleep", "infinity"] imagePullPolicy: IfNotPresentapiVersion: apps/v1 kind: Deployment metadata: name: web-demo spec: replicas: 2 selector: matchLabels: app: web-demo template: metadata: labels: app: web-demo spec: containers: - name: nginx image: nginx:1.27 ports: - containerPort: 80再创建对应的Service:apiVersion: v1 kind: Service metadata: name: web-svc spec: selector: app: web-demo ports: - port: 80 targetPort: 80这套环境准备好之后,下面每一跳都可以直接在探针里操作。3. 第一跳:同节点Pod间通信,从veth到cni03.1 从Pod内部看自己的网络命名空间进入探针Pod之后,先看两样东西:IP地址和路由表。kubectl exec -it deploy/probe-tools -- ip addr输出里会看到Pod自己的eth0,比如172.20.1.5/24,还有一个lo回环。这个eth0并不真实存在于宿主机上,它只是一个虚拟网卡,真正连通的是宿主机上另一端的veth设备。接着看路由表:kubectl exec -it deploy/probe-tools -- ip route默认路由几乎都是:default via 172.20.1.1 dev eth0 172.20.1.0/24 dev eth0 proto kernel scope link src 172.20.1.5这个172.20.1.1是cni0网桥在宿主机上对应网段的网关地址。也就是说,Pod里的数据包从eth0出去之后,下一个动作是到cni0这个二层网桥,由网桥来决定往哪个veth口转发。3.2 在宿主机上定位veth对,理解“一根虚拟网线”veth pair是成对出现的虚拟网卡,就像一根虚拟网线,一端插在容器网络命名空间里变成Pod的eth0,另一端插在宿主机的网络栈上,挂到cni0网桥上。在宿主机上执行:ip link | grep -B1 "veth"你会看到很多vethxxx@ifN这样的设备,其中@ifN里的N,就是对端网卡在Pod网络命名空间里的序号。可以通过ethtool -S vethxxx查看peer_ifindex进一步确认到底对应哪个Pod。我常用的方法简单粗暴:在每个探针Pod里执行ip link show eth0,记下它的接口序号和MAC地址,再到宿主机上ip link | grep -A1 <MAC>,立刻就能把veth对应关系找出来。3.3 同节点连通性验证:在cni0上抓包才看得清同节点的两个Pod互ping时,流量只在宿主机内部走一遍:源Pod的eth0 → 源veth → cni0网桥 → 目的veth → 目的Pod的eth0,全程不经过宿主机物理网卡。验证命令很简单:kubectl exec -it deploy/probe-tools -- ping -c 3 172.20.1.6如果想亲眼看到这个链路,最有效的抓包位置是cni0:tcpdump -i cni0 -nn icmp -c 10能同时看到两个方向ICMP报文从cni0进出的过程。这里有个经验:很多人习惯在Pod里直接抓包,但容器内tcpdump只能看到自己eth0方向的流量,看不到网桥上的广播和转发动作,定位问题时视野太窄。要判断“这个包到底有没有出Pod”,在宿主机veth那头抓;要知道“这个包在二层怎么流转”,在cni0上抓。到这里,第一跳验证完成。同节点通信是K8s网络最基础的部分,一般不会出问题,它真正的意义在于帮你建立“数据包从容器出来,先走veth到网桥”的直观印象,后面理解Service转发会轻松很多。4. 第二跳:跨节点Pod通信,CNI容器网络后端的选择4.1 为什么跨节点就是另一回事了同节点通信靠网桥就能解决,但不同节点之间呢?Pod IP是每个节点分配的不同网段,节点之间隔着一层物理网络,数据包要跨IP网络传输,必须有路由条目和转发机制。这里就引出了CNI插件后端方案的差异,这也是网络上讨论最多、最容易产生分歧的地方。在探针Pod里ping一个位于其他节点的Pod IP,比如172.20.2.3。先看路由表,此时默认路由还是走eth0,但到达对端Pod网段的路由,在宿主机上会指向某个特殊接口——可能是tunl0或flannel.1,也可能是直接走物理网卡。这个差异完全取决于CNI后端。4.2 三种常见CNI后端方案,一张表说清楚方案数据面封装方式典型端口/协议性能适用场景flannel VXLAN内核VXLANUDP封装UDP 4789中等中小集群,部署简单calico IPIPIPIP隧道IP-in-IP封装协议号4中等偏上跨网络但不想暴露Pod网段calico BGP(underlay)路由直通无封装BGP 179最好网络设备支持BGP的大型集群cilium eBPFeBPF直接路由无封装或VXLAN视配置而定最好,可观测性强对性能和可观测性要求高选方案本质是“性能”和“部署简单”之间的权衡。小团队从flannel起步完全没有问题,它最大的优势是依赖少、内核里就能跑。但要记住,VXLAN封装带来额外负载,吞吐量会比underlay方案低一些。我在实际压测里见过,同样两台机器,flannel VXLAN和calico BGP的TCP吞吐能差出30%以上,这是网络报文多封装一层头部造成的必然开销。4.3 实际验证跨节点链路:盯住隧道端口验证跨节点通信时,不要只关心ping通不通,更要看数据包走的是哪条路。假如集群用的是flannel,在源节点宿主机上执行:tcpdump -i any udp port 4789 -nn -c 20去ping对端Pod的同时,你会看到大量从本机eth0出去的UDP 4789报文,里面封装了原始的ICMP包。这就是VXLAN隧道:Pod发出的原始数据包在源节点被内核封装进UDP,送到对端节点的4789端口,对端解封装后再从自己的cni0转发给目标Pod。如果集群是calico IPIP,抓包方式改成:tcpdump -i any proto 4 -nn -c 20proto 4就是IP-in-IP协议,能看到外层是两台宿主机IP,内层是Pod IP。识别不同后端后,排查方向会很不一样:flannel出了问题是看VXLAN端口通不通;calico IPIP除了看端口,还要确认IPIP协议有没有被防火墙拦;underlay方案则要检查交换机上的路由条目和BGP peer状态。4.4 一个很容易被忽略的坑:MTU不一致导致“ping通但业务不通”这是我在一个跨节点验证里踩过的典型问题。两个节点Pod之间ping能通,但一用curl传大数据就卡死或超时,排查一圈发现是隧道接口MTU和物理网卡MTU不匹配。VXLAN隧道会额外占用50字节,如果物理网卡MTU是1500,隧道接口默认就得降到1450,但某些情况客户端没有正确协商MTU,导致需要分片的包被丢弃。验证手段很简单,用ping -s 2000发一个大包测试,或者查一下隧道接口的mtu值:ip link show flannel.1 ip link show tunl0这个坑特别容易出现在混合环境里,比如有的Node是VM,物理网卡MTU本身就只有1450,链路MTU层层递减,到应用层就只剩1400了。遇到跨节点业务大包超时,先查MTU,不要一上来就怀疑代码。5. 第三跳:Pod直连Service,ClusterIP的魔法5.1 ClusterIP不存在于任何网卡上,它活在规则里跨节点Pod通信解决了,下一步就是从Pod访问Service ClusterIP。第一次知道ClusterIP不在任何节点上“落地”的时候,很多人会觉得不可思议。它不绑定真实网卡,没有ARP应答,甚至你在节点上ip addr都看不到它。那数据包发到ClusterIP为什么能通?因为每个节点上的kube-proxy会实时watch API Server里的Service和Endpoints对象,把“ClusterIP → 一组后端Pod IP”的映射关系写进本机的NAT规则里。当一个Pod访问ClusterIP时,数据包先正常走出Pod的网络命名空间,到达cni0,然后进入宿主机的网络栈,在netfilter的PREROUTING/OUTPUT链上被规则匹配,执行DNAT,把目标地址从ClusterIP改写成一个具体的Pod IP。后续转发就复用跨节点/同节点Pod通信链路。ClusterIP的“IP层是假的”,但“转发链是真的”。5.2 kube-proxy三种模式,性能差异可以很大kube-proxy有userspace、iptables、ipvs三种模式,现在基本见不到userspace了,生产环境主要在iptables和ipvs之间选。iptables模式用NAT规则链实现负载均衡:一个Service对应一条KUBE-SVC链,链上挂多个KUBE-SEP链,每个KUBE-SEP负责DNAT到一个后端Pod。后端数量变大时,规则数量线性增长,最直观的副作用是规则更新慢,集群内Service频繁变动时可能出现短暂的转发断开。ipvs模式基于内核的IPVS模块,本身就是为负载均衡设计的,用哈希表维护Service和RealServer映射,后端增删只改表项,不产生海量iptables规则链条。它还支持rr、lc、sh等多种调度算法,适合大规模集群。模式规则规模负载均衡算法对大规模集群友好度userspace用户态代理轮询差,基本淘汰iptables与后端数量线性增长随机/轮询(statistic)Service多时更新慢ipvs哈希表,规则数基本固定rr、lc、sh等高,推荐5.3 动手看一条ClusterIP规则的完整链路我用一个探针Pod持续访问web-svc,在节点上抓规则。先看当前kube-proxy模式:kubectl logs -n kube-system -l k8s-app=kube-proxy | grep -i "Using"如果是iptables模式,用iptables-save把规则导出来看:iptables-save -t nat | grep -A 10 "KUBE-SVC"典型输出长这样:-A KUBE-SVC-XXXXX -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-POD1 -A KUBE-SVC-XXXXX -j KUBE-SEP-POD2 -A KUBE-SEP-POD1 -p tcp -m tcp --dport 80 -j DNAT --to-destination 172.20.1.5:80 -A KUBE-SEP-POD2 -p tcp -m tcp --dport 80 -j DNAT --to-destination 172.20.1.6:80KUBE-SVP里用了statistic --mode random实现50%概率分流,另一条规则落到第二个后端。至于为什么第一个后端显示DNAT到172.20.1.5:80,实际上KUBE-SEP里还会有MASQUERADE规则,把源IP也伪装掉,这样后端Pod回包时能正确回到宿主机网络栈,再由conntrack恢复原始连接。如果是ipvs模式,用ipvsadm看:ipvsadm -Ln | grep -A 3 "10.96.12.10"输出里能看到Tcp 10.96.12.10:80 rr,下面是两个Realserver地址。相比一长串iptables规则,ipvs的视图简洁得多。验证这一步时我最常用的命令是:在探针里curl http://web-svc:80,同时在节点上开一个对Pod IP的tcpdump,比如:tcpdump -i cni0 -nn host 172.20.1.5 and port 80 -c 10注意:抓包位置在cni0,能看到报文目标地址已经被改成了Pod IP。对比Pod eth0上的报文目标地址(原始ClusterIP),非常直观地感受到什么叫DNAT。提示:如果Pod访问ClusterIP报connection refused,先别急着怀疑规则。检查一下Service的selector有没有正确匹配到Pod,kubectl get endpoints web-svc如果为空,规则链上根本没有后端可转,直接拒连。这个是最常见的“Service不通”原因,通常不是规则坏了,而是Endpoints没生成。6. 第四跳:Service名称到ClusterIP,DNS发现链路6.1 CoreDNS在集群里的角色,说穿了就是个名字翻译器网络通了之后,应用一般不会直接用ClusterIP,而是用Service名字。集群内建的DNS服务(默认是CoreDNS)负责把Service名字翻译成ClusterIP。这件事还要结合Pod自己的resolv.conf才能完整解释。进入探针Pod,执行:cat /etc/resolv.conf输出类似:search default.svc.cluster.local svc.cluster.local cluster.local nameserver 10.96.0.10 options ndots:5nameserver指向CoreDNS的ClusterIP;search是搜索域列表;ndots:5意味着,如果域名里点的数量小于5,解析器会先把域名拼上search域依次尝试,而不是直接走FQDN查询。这个细节日常不起眼,排查DNS慢的时候它就是主角。6.2 实测DNS解析:短域名和FQDN会走完全不同的路径在探针里执行:nslookup web-svc你会看到它实际上查询的是web-svc.default.svc.cluster.local,并且返回的A记录就是ClusterIP。再加一条:dig web-svc.default.svc.cluster.local +short返回的结果一样。但用dig web-svc +short,走的就是ndots逻辑下的多轮搜索,可能返回不权威的中间结果,也可能因为search域顺序导致解析到错误目标。我建议所有验证都直接用FQDN,例如web-svc.default.svc.cluster.local。很多线上DNS“诡异超时”,原因就是应用用了短名,而search域列表较长,每层搜索都要等待查询超时后才进入下一层。特别是Pod内没有配置完整FQDN、又依赖外部短域名的情况下,一次DNS查询可能被“人为”拖到几秒。6.3 三个常见的DNS验证误区第一个误区是只看解析结果,不看解析耗时。DNS解析通不代表快,dig末尾会给出耗时,如果每次查询几百毫秒,就要检查CoreDNS性能或上游DNS配置,而不是直接跳过。第二个误区是忽略Headless Service的特殊表现。无头Service没有ClusterIP,CoreDNS返回的不是虚拟IP,而是后端Pod IP列表。所以验证时要区分普通Service和无头Service,它们的DNS行为天然不同。第三个误区是Service名字里带了不符合RFC的字符,比如下划线。某些版本不会给出友好的报错,而是直接解析失败或返回空结果,排查时很容易联想到网络策略或其他玄学问题,实际上就是DNS名字本身不合法。7. 第五跳:从集群外部访问,NodePort、LoadBalancer与Ingress7.1 NodePort完整链路:外部流量怎么进到集群内部内部链路都通了,最后验证从集群外部到Service的路径。最基础的方式是NodePort。创建Service时指定type: NodePort,集群会在每个Node上开放一个固定端口,比如30080。外部请求访问任意Node的30080端口,数据包进入宿主机的网络栈后,被iptables/ipvs规则匹配,走一遍DNAT,转发到后端Pod。如果入口Node不是后端Pod所在Node,还会发生一次跨节点转发。这也是NodePort模式容易被诟病的地方:额外一跳不仅增加延迟,还可能因为转发节点的资源争抢影响长连接服务质量。可以通过externalTrafficPolicy: Local让入口流量只转发到本Node的Pod,但副作用是后端Pod分布不均时可能出现部分Node“空转”。验证命令很简单:curl http://<nodeip>:30080在Node上抓包看链路:tcpdump -i eth0 -nn tcp port 30080 -c 10再结合iptables-save | grep KUBE-NODEPORT就能看到规则是如何从NodePort链跳转到KUBE-SVC链的。7.2 LoadBalancer和Ingress的定位,别混为一谈NodePort是基础,生产上几乎都会再套一层负载均衡或Ingress。LoadBalancer类型实际就是云厂商提供一个四层负载均衡器,把公网流量转发到各Node的NodePort(不同云厂商实现略有差异);Ingress则是集群内部的七层入口网关,根据域名和路径路由到不同的Service。很多人在云上同时开了LoadBalancer和Ingress,然后疑惑“为什么外部域名通了,但直接用NodePort访问不行”,这时候先分清两者:LB负责四层接入,Ingress负责七层路由,如果Ingress入口本身就绑在NodePort上,那么NodePort不通,Ingress一定也不通。组件工作层级转发依据适合场景NodePort四层端口测试、无LB环境LoadBalancer四层端口/IP云上对外暴露TCP/UDP服务Ingress七层域名/路径HTTP/HTTPS路由、网关7.3 我推荐的对外访问验证顺序外部访问出问题时,我习惯按这个顺序从里到外排查:在集群内Pod里直接curl ClusterIP:验证Service转发本身。在Node上curl ClusterIP:验证节点网络栈到Service正常。在Node上curl本机NodePort:验证本机NodePort规则。在另一个Node上curl NodePort:验证跨节点转发和回程路径。在集群外通过LB或Ingress域名访问:验证防火墙、安全组和入口配置。这五步每一层都有清晰的边界,能精确把问题锁定在一个很小的范围内,而不是盲目重启kube-proxy或者怀疑CNI插件。8. 这条链路里最容易翻车的5个坑,踩过才长记性8.1 坑一:kube-proxy模式与预期不一致排查问题前先确认kube-proxy到底跑的哪种模式,我吃过这个亏。一度以为集群是ipvs模式,手动改了一条iptables规则做验证,结果规则完全没生效,排查半天发现集群实际是iptables模式。确认方式:kubectl logs -n kube-system -l k8s-app=kube-proxy | grep -i "Using"如果输出Using ipvs,那看规则用ipvsadm;如果输出Using iptables,那就用iptables-save。模式和工具匹配,才能看到真实规则。这个问题在集群升级或重新安装后尤其容易出现。8.2 坑二:conntrack造成的“假连通”,坑了一下午有一次排查线上偶发超时,现场访问很稳定,但业务就是有周期性报错。最后发现问题出在conntrack:Service后端Pod滚动更新后,老连接还留在conntrack表里,继续着原来的DNAT目标,新连接却已经分配到新Pod上。表面上看“刚才还通”,实际上你验证的可能是一条已经建立的老连接。正确做法是验证连通性时强制新建连接,curl加--connect-timeout,必要时主动清一下conntrack条目:conntrack -L | grep <service-cluster-ip> conntrack -D -d <service-cluster-ip>生产环境千万别乱删conntrack,但排查问题时这招能迅速确认是不是连接跟踪在作怪。8.3 坑三:DNS缓存和search域联手制造“幽灵解析”Pod里跑的业务如果使用了系统级DNS缓存,你在Pod里nslookup看到的是新解析结果,但业务进程可能还拿着缓存的旧ClusterIP。更麻烦的是有些镜像内置了dnsmasq之类的缓存,和K8s默认的ndots:5叠加后,短域名的查询行为完全不可预测。排查时不要只看nslookup,也别只看应用的日志,两个都要看。如果Pod里解析正常、应用就是不通,进业务容器里看看/etc/resolv.conf,确认它到底走哪里的DNS,再决定是清缓存还是调整应用配置。8.4 坑四:NetworkPolicy在“默默执法”K8s默认没有NetworkPolicy时,所有Pod之间互通;一旦有人创建了NetworkPolicy,哪怕只有一个,规则也只对匹配到的标签生效。遇到过不止一次:两个命名空间之间Pod互相能ping通,但业务端口访问被拒,翻遍Service和iptables都找不到问题,最后发现是某个NetworkPolicy只放行了特定端口。验证方式:kubectl get networkpolicy -A重点看policy的podSelector和ingress规则,比对一下出问题Pod的标签。这一步虽然简单,但很容易被忽略,因为网络策略不像Service和Endpoint那么直观。8.5 坑五:安全组和防火墙对NodePort的“选择性放行”云厂商的安全组规则有时候很隐蔽。之前遇到一个现象:nc -vz nodeip 30080通了,但业务访问老是超时。排查才发现安全组只放行了特定源IP的访问,而nc正好是从放行网段里发出来的,业务流量来自另一个网段,被安全组拦在门外。做外部访问验证时,一定要用真实业务流量的源IP和目标端口来测,不要拿一个“测试环境专用”的IP测完就下结论。同时检查所有环节的放行策略:云安全组、节点防火墙、本地iptables的INPUT链。四层和七层各测一遍,才能确定问题到底在哪一段。整个链路验证到这里就算是完整走了一遍。我个人的习惯是,新集群上线前一定先做一遍这五跳验证,把每段的结果截图存档,后面业务出问题的时候可以直接对比“基准线”,省掉大量从零排查的时间。最后再分享一个小技巧:验证过程中所有抓包文件统一命名加时间戳,比如link-check-svc-20250101.cap,因为网络问题往往不是“现在坏了”,而是“某次变更之后坏了”,有历史抓包对比,才能快速确认回归点在哪一环。