首页 / 资讯中心 / 文章详情

Kubernetes网络链路验证:从Pod到Service的五跳排查指南

Kubernetes网络链路验证:从Pod到Service的五跳排查指南 ★ FEATURED ARTICLE
拿到一个新搭好的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.5Pod和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再创建对应的ServiceapiVersion: 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你会看到很多vethxxxifN这样的设备其中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如果想亲眼看到这个链路最有效的抓包位置是cni0tcpdump -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 BGPunderlay路由直通无封装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直连ServiceClusterIP的魔法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与后端数量线性增长随机/轮询statisticService多时更新慢ipvs哈希表规则数基本固定rr、lc、sh等高推荐5.3 动手看一条ClusterIP规则的完整链路我用一个探针Pod持续访问web-svc在节点上抓规则。先看当前kube-proxy模式kubectl logs -n kube-system -l k8s-appkube-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有没有正确匹配到Podkubectl get endpoints web-svc如果为空规则链上根本没有后端可转直接拒连。这个是最常见的“Service不通”原因通常不是规则坏了而是Endpoints没生成。6. 第四跳Service名称到ClusterIPDNS发现链路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的ClusterIPsearch是搜索域列表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没有ClusterIPCoreDNS返回的不是虚拟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-appkube-proxy | grep -i Using如果输出Using ipvs那看规则用ipvsadm如果输出Using iptables那就用iptables-save。模式和工具匹配才能看到真实规则。这个问题在集群升级或重新安装后尤其容易出现。8.2 坑二conntrack造成的“假连通”坑了一下午有一次排查线上偶发超时现场访问很稳定但业务就是有周期性报错。最后发现问题出在conntrackService后端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因为网络问题往往不是“现在坏了”而是“某次变更之后坏了”有历史抓包对比才能快速确认回归点在哪一环。
阅读完成 · 觉得有帮助?
咨询建站