☰
Kubernetes Service到Pod流量路径:iptables、IPVS与conntrack全解析
2026/10/6 8:57:56 网站建设 项目流程

搞明白 Service 到 Pod 的流量路径,是每一个被"kubectl get svc 能看到 ClusterIP,Pod 也全是 Running"却依然访问不通的人,迟早要过的坎。我处理线上问题这些年,Service 相关的坑见得太多了:明明 endpoints 是满的,curl 就是超时;明明有两个副本,流量却全压在同一个 Pod 上。这些问题的根源,几乎都落在同一条链路上——用户请求到达 Service 的虚拟 IP 之后,内核里到底发生了什么,数据包才精准落进那一个 Pod。

这篇文章不聊抽象架构图,我直接把 iptables 的规则现场、IPVS 的调度表、以及数据包在节点网卡和 Pod 网卡之间的走向全部摊开来讲。适合刚学完 k8s 基础、对本机网络细节还犯迷糊的人,也适合经常被 Service 访问故障折磨的运维和开发。你不需要提前把整个 CNI 源码读完,只需要带着一个疑问进来:用户访问 k8s 的 svc 后,流量是怎么打到具体的某一个 pod 上的?

1. 先搭好实验场景,再拆流量链路

1.1 一个最小场景:Deployment 两个副本 + ClusterIP Service

先把场景固定下来,后面所有规则和抓包才有参照物。假设我用一份很普通的 Deployment 起了两个 Nginx 副本,再创建一个 ClusterIP 类型的 Service,yaml 大概长这样:

apiVersion: apps/v1 kind: Deployment metadata: name: nginx-demo spec: replicas: 2 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.27 --- apiVersion: v1 kind: Service metadata: name: nginx-svc spec: selector: app: nginx ports: - port: 80 targetPort: 80

部署完之后,kubectl get svc nginx-svc会看到类似10.96.154.25:80的 ClusterIP,kubectl get endpoints nginx-svc会看到两个 Pod IP,比如10.244.1.5:80和10.244.1.6:80。到这里一切都很"正常",但正常只是表象,真正的问题在于:ClusterIP 这个地址,你在任何一台节点上用ip addr都找不到它对应的网卡,它只是写在 etcd 里的一条资源记录。那用户访问它的时候,操作系统凭什么知道要把包往哪里送?

答案的起点是三个角色:EndpointSlice 控制器负责维护 Pod 名单,kube-proxy 负责把名单翻译成内核规则,内核的 netfilter/IPVS 负责真正干活。这三者是一条流水线,任何一个环节卡住,Service 就会"看起来正常,用起来难受"。

1.2 三种常见的访问入口,路径有什么不同

Service 的流量入口大致分三种:集群内部的 ClusterIP、对外的 NodePort、云厂商或自建负载均衡器的 LoadBalancer。很多人把这三种当成三套完全不同的机制,其实它们的后半段是共用的,区别只在数据包到达节点的过程中怎么被"接住"。

入口类型数据包最开始去哪Service 参与方式典型场景
ClusterIP集群内 Pod 或节点直接访问虚拟 IP节点上的 NAT 规则直接处理服务间调用、测试验证
NodePort任意节点的 30000-32767 端口节点收到后转到 ClusterIP 规则无 LB 环境的外网访问
LoadBalancer外部 LB 先把流量转发到某节点 NodePort最终仍走 NodePort 后半段云上生产环境对外服务

也就是说,NodePort 和 LoadBalancer 本质上都是"多一层入口包装",真正的分发逻辑还是 ClusterIP 那套。这也是为什么排查问题时,不管用户从哪进来的,最后都要回到ClusterIP -> Endpoints -> Pod这条主线上去看。搞清楚这条主线,剩下的都是旁支。

2. Service 是虚拟的承诺,kube-proxy 才是执行者

2.1 Service 与 Endpoints:一份被持续维护的"花名册"

Service 本身不存任何 Pod 信息,它只有两样东西:一个不变的虚拟 IP,以及一个 selector 标签选择器。真正把 Service 和 Pod 绑在一起的,是 Endpoints 这个资源,以及它背后的 EndpointSlice。在比较新的 Kubernetes 版本里,kube-controller-manager 里的 EndpointSlice 控制器会一直盯着符合条件的 Pod,一旦 Pod 的 IP 变化、标签变化或者 Ready 状态变化,它就会立刻增删 EndpointSlice 里的地址条目。

用一句话概括:Service 是"招聘启事",EndpointSlice 是"入职名单"。招聘启事上写的是要求(selector),入职名单上写的是现有员工(Pod IP)。kube-proxy 不会自己去问 Pod 在不在,它只看名单。所以当你发现 Service 访问不通时,第一步永远是kubectl get endpointslices -l kubernetes.io/service-name=nginx-svc,看看名单是不是空的、IP 是不是落后于实际 Pod IP。这一步能过滤掉至少三成问题。

2.2 kube-proxy 的本职工作:把清单翻译成内核规则

kube-proxy 是运行在每个节点上的 DaemonSet,它不是数据面的转发器,而是一个"规则翻译器"。它通过 API Server 监听 Service 和 EndpointSlice 的变化,然后把变化翻译成节点上实际生效的转发规则。翻译成的规则有两种主流形态:iptables 规则(nat 表)或者 IPVS 虚拟服务器。

这里要澄清一个常见误解:数据包不是"流经" kube-proxy 的,kube-proxy 甚至可以停掉,已经写好的内核规则照样会继续转发流量。真正让数据包改道的是内核里的 netfilter 钩子,kube-proxy 只是往这些钩子上挂规则。理解这一点特别重要——排错的时候你去看 kube-proxy 日志,往往什么发现都没有,因为问题根本不在它身上,而在它写出来的规则和内核行为上。

2.3 为什么 ClusterIP 不是绑在某块网卡上的地址

我刚开始学 k8s 的时候干过一件傻事:去节点上ip addr找 ClusterIP,想着找到那块"虚拟网卡"就能理解它了,结果自然是什么都没有。ClusterIP 从诞生起就不属于任何网卡,它只存在于内核的 NAT 规则里。数据包到达节点之后,会在 PREROUTING 或 OUTPUT 链里被匹配到这个虚拟 IP,然后直接被改写目的地,压根走不到"路由到某张网卡"这一步。

这就像公司前台有一个"总机号码",拨进去之后前台把电话转接给具体员工,总机号码本身不连接任何一根真实的电话线。这个设定有一个让人意外的副作用:你在节点上ping ClusterIP通常是通的(内核会响应),但telnet ClusterIP 80才有意义,因为只有匹配到端口和协议,规则才会触发。

3. 负载均衡发生在哪:iptables 与 IPVS 的两种答案

3.1 iptables 模式下的抽签逻辑

kube-proxy 默认的代理模式是 iptables(部分发行版和云厂商会改成 ipvs),在这种模式下,负载均衡完全靠 nat 表里一串链的跳转来完成。用优雅一点的说法叫"概率抽签",说难听点就是"掷骰子"。我先用命令把现场抓出来,你感受一下:

iptables -t nat -L KUBE-SERVICES -n --line-numbers

输出里能看到一条匹配 ClusterIP 的规则,大意是:目的地址是10.96.154.25且目的端口是 80 的包,跳到 KUBE-SVC 开头的一条链。接着把这条 SVC 链打出来:

iptables -t nat -L KUBE-SVC-XXXX -n -v

假设有两个后端 Pod,你会看到类似这样的两条规则:

Chain KUBE-SVC-XXXX (1 references) target prot opt source destination KUBE-SEP-A 0.0.0.0/0 0.0.0.0/0 statistic mode random probability 0.50000000000 KUBE-SEP-B 0.0.0.0/0 0.0.0.0/0

第一条规则带着statistic mode random probability 0.5,意思是"一半概率跳到后端 A,另一半概率落到第二条,而第二条无条件跳到后端 B"。如果后端有三个,概率会变成 1/3 和 1/2,保证最终每个后端分到约 1/3 的流量。再往下看 KUBE-SEP 链,它才是真正动手的地方:

Chain KUBE-SEP-A (1 references) target prot opt source destination DNAT tcp 0.0.0.0/0 10.96.154.25 tcp dpt:80 to:10.244.1.5:80

这行 DNAT 规则把目的 IP 从 ClusterIP 改写成了 Pod IP。到这里,负载均衡其实已经结束了——后面的路由转发是内核自己的事。

值得留意的是,iptables 模式的"随机"是每个新连接独立抽签的。它不会记住某个客户端上次分到了哪个 Pod,所以同一客户端的两个不同连接可能被分到不同后端。如果你需要会话保持,就得给 Service 配sessionAffinity: ClientIP,kube-proxy 会额外生成一条按源 IP 哈希的规则。

3.2 IPVS 模式:把负载均衡交给内核模块

iptables 模式在规则数量很大的时候性能会明显下降,因为链表是线性匹配的。所以生产环境里我更喜欢把 kube-proxy 切到 IPVS 模式,在 kube-proxy 的 ConfigMap 里把mode: "ipvs"配上就行。IPVS 是内核里专门的 L4 负载均衡器,它维护一张哈希表,直接用内核线程做调度,规则再多也不会像 iptables 那样一条条遍历。

接线完成之后,在节点上执行:

ipvsadm -Ln

会看到这样一张表:

IP Virtual Server version 1.2.1 Prot LocalAddress:Port Scheduler Flags -> RemoteAddress:Port Forward Weight ActiveConn InActConn TCP 10.96.154.25:80 rr -> 10.244.1.5:80 Masq 1 0 0 -> 10.244.1.6:80 Masq 1 0 0

LocalAddress 是 Service 的 ClusterIP,RemoteAddress 就是两个 Pod IP,调度算法默认是 rr(轮询)。每来一个新连接,IPVS 内核模块按照调度算法直接选一个 RealServer,然后用Masq方式做 DNAT,把包转到 Pod。整个过程不经过用户态,也没有链表遍历,性能比 iptables 模式高一个量级。

iptables 和 IPVS 的选择,说白了就是"灵活但线性"和"高效但简单"之间的权衡。iptables 模式胜在匹配条件全、规则透明,适合小集群和排查学习;IPVS 模式胜在性能和调度算法丰富,适合后端数量多、连接密度高的生产集群。我个人的经验是:先在小集群里把 iptables 模式玩明白,再切 IPVS,因为你只有看得懂规则,才能在出问题时快速定位而不是瞎猜。

3.3 DNAT、SNAT、MASQUERADE:数据包的改头换面术

前面已经提到 DNAT 把目的地址改成了 Pod IP,但流量能回来,靠的不只是 DNAT,还有 SNAT 和 conntrack。这里用生活化的方式解释一下这三兄弟的分工。

DNAT 是"进入时的改名换姓":包到达节点时,目的地址还是 ClusterIP,DNAT 把它改写为 Pod IP,相当于快递到了前台,前台把收件人改成具体员工的名字再往后送。SNAT/MASQUERADE 是"出门时的伪装":当数据包来自集群外部(比如通过 NodePort 进来的),如果不伪装源地址,Pod 回包时会把包发给原始客户端,而不是经过节点中转,这样客户端收到的源 IP 是 Pod IP,而不是它访问的节点 IP,连接直接断掉。所以 MASQUERADE 会把源 IP 改成节点网卡 IP,回包先回到节点,节点再根据 conntrack 记录把包转回客户端。

conntrack 是这个链路里的"记事本"。DNAT 发生的时候,内核会在连接跟踪表里记一笔:这条连接原始目的是10.96.154.25:80,被改写成了10.244.1.5:80。回包到达节点时,conntrack 看到这是刚才那条连接的回复,就反向做一次 UN-DNAT,把源地址再改回 ClusterIP,这样客户端浑然不知自己其实跟 Pod 直接对话过。

这里有一个我在实战中踩过很多次的坑:conntrack 表是有上限的,默认值在某些内核和环境下并不高。如果集群里短连接特别多,conntrack 表被撑满,新连接就会随机丢包,表现是"Service 时通时不通"。排查时可以用conntrack -S看 insert_failed 计数,如果一直在涨,就该考虑调大nf_conntrack_max,或者从应用层改造连接模式了。

4. 最后一跃:从节点网卡到 Pod 内网卡

4.1 节点路由表和 CNI 插件的分工

数据包被 DNAT 成10.244.1.5:80之后,内核需要重新做一次路由决策:这个目的 IP 到底从哪张网卡出去?这一步不再由 Service 管,而是由 CNI 网络插件负责。不同的 CNI 方案在这层的表现完全不同,我以最常见的 Flannel 和 Calico 为例。

Flannel 的 VXLAN 模式下,每个节点上有一个叫cni0的桥接网卡,所有本节点的 Pod 都通过 veth 虚拟网线挂在这座桥上。节点路由表里会有这样的条目:10.244.1.0/24 dev cni0,表示目的网段是本节点的,直接桥接;10.244.2.0/24 via 10.244.2.1 dev flannel.1,表示目的网段在别的节点,走 VXLAN 隧道。Calico 则更直接,它通过 BGP 在各节点间宣告 Pod 网段路由,数据包直接走节点物理网卡转发,没有隧道封装,性能更好但依赖底层网络的连通性。

不管哪种方案,数据包最终都要通过 veth 对进入 Pod 的网络命名空间。veth 就像一根虚拟网线,一头插在宿主的 cni0 或 cali 接口上,另一头插在 Pod 的 eth0 上。包走到宿主这一头,就等同于一脚迈进了 Pod 的门,剩下的就是 Pod 里协议栈的正常处理了。

4.2 回包是另一条路,但 conntrack 让它看起来像同一跳

很多人以为数据包从哪来就沿原路回去,其实完全不是。Pod 收到请求后,Nginx 回包的源地址是10.244.1.5:80,目的地址是客户端 IP(如果经过了 MASQUERADE,这个"客户端 IP"其实是发出请求的节点 IP)。回包走的路径是:Pod 的 eth0 -> veth -> 节点路由表 -> 按源目地址查 conntrack,发现这条连接是之前 DNAT 过的,于是做反向转换,把源地址改回 ClusterIP,如果之前有 SNAT,再把目的地址改回客户端原始 IP。这一套转折全部发生在内核里,对应用层完全透明。

理解了回包路径,就理解了为什么kubectl logs里看到的客户端 IP 有时候是节点 IP 而不是真实用户 IP。默认 externalTrafficPolicy 是 Cluster 模式,NodePort 流量经过节点时会做 SNAT,真实源 IP 被抹掉了。想要保留真实源 IP,得把 Service 的externalTrafficPolicy改成 Local,但这又要求流量只转发到本节点的 Pod,跨节点转发会被放弃,流量分布不均匀。这是一个取舍问题,没有两头都占的方案。

4.3 完整生命周期:一个 HTTP 请求的 7 个阶段

把上面的知识串起来,一次完整的请求大概是这样的。我拿集群外用户通过 NodePort 访问来举例,因为它覆盖了最全的路径:

  1. 用户请求到达某个节点的 NodePort 端口,比如10.0.0.5:30080。
  2. 节点上的 iptables 或 IPVS 规则匹配到 NodePort,把它交给对应的 ClusterIP Service 规则继续处理。
  3. 在 KUBE-SVC 链或 IPVS 调度表中,按概率或轮询选中一个后端 Pod,比如10.244.1.5。
  4. 做 DNAT:目的地址从 ClusterIP 或 NodePort 对应的虚拟地址改成10.244.1.5:80;同时 MASQUERADE 把源地址改成节点 IP。
  5. 内核重新路由,根据 CNI 的路由表把包从正确网卡发出,经过 veth 进入 Pod。
  6. Nginx 处理完请求,回包到节点。conntrack 识别出这是已记录的连接,做反向 NAT。
  7. 节点把回包交给用户,用户看到的仍然是他访问的那个地址,整个过程无感知。

这 7 个阶段里,最容易出问题的集中在 2、3、5。前两个属于 kube-proxy 规则层的范畴,最后一个属于 CNI 路由层的范畴,排查时先把这两大块分开,别在 Pod 里面瞎抓包。

5. 排查实录:从"不通"到"通了"的常见套路

5.1 访问 ClusterIP 超时,但 Pod 明明健康

现象是 Pod 状态都是 Running,kubectl get svc也有 ClusterIP,但进 Pod 里curl http://10.96.154.25就是卡住或拒绝。这种问题八成出在 EndpointSlice 和 kube-proxy 规则之间的同步上,按下面的顺序查,能省掉很多冤枉路。

先看名单有没有更新:kubectl get endpointslices -l kubernetes.io/service-name=nginx-svc -o yaml,确认地址是实际 Pod IP。再看 kube-proxy 有没有把规则写进去:节点上执行iptables -t nat -L KUBE-SERVICES -n | grep 10.96.154.25,如果在 IPVS 模式下就ipvsadm -Ln | grep 10.96.154.25。规则如果不存在,要么是 EndpointSlice 没更新,要么是 kube-proxy 没监听到,后者去查 kube-proxy 日志里有没有连不上 API Server 的记录。

我遇到过一个特别隐蔽的情况:Pod 是新版,但 Elastic IP 老挂在旧 Pod 上,EndpointSlice 里显示的 IP 在节点上根本不存在。原因是有个 StatefulSet 的 headless Service 配合 network 相关资源时,EndpointSlice 控制器更新延迟,而 kube-proxy 又还没来得及删旧规则。这种问题用kubectl get endpointslices一眼就能看见地址列表里有幽灵 IP。

5.2 流量总是打到同一个 Pod 上

另一个高频问题是:两个副本,但压测时其中一个 Pod 的流量占比接近 90%。很多人第一时间怀疑负载均衡算法坏了,其实大概率是 conntrack 在"帮忙"。

iptables 的概率抽签是针对"新连接"的,但同一个 TCP 连接里的所有包,conntrack 都会命中同一条记录,一直送到同一个 Pod。如果你的压测脚本用了长连接复用,或者数据库连接池一直保持着连接,那流量天然就固定在一个后端上。这不算 bug,而是很多中间件连接的正常表现。要验证这一点,用短连接方式压测,比如每个请求新建一个 TCP 连接,再看分布,通常会恢复均衡。

另一个原因是 Service 配置了sessionAffinity: ClientIP,这会让同一个源 IP 的所有连接都打到一个 Pod 上。排查时kubectl get svc nginx-svc -o yaml看一眼 clientIP 字段就知道。生产环境如果需要均匀分布,确认 sessionAffinity 不是 ClientIP,或者根据业务需要把 hash 键改成sourceIP,destinationIP这样的组合。

5.3 排查命令速查表

最后把这些年积累下来最常用的排查命令整理成一个速查表,按从上到下的顺序执行,基本能把 Service 流量问题定位到具体环节。

排查目的命令关键看点
查看服务与端口kubectl get svc nginx-svc -o yamlClusterIP、port、sessionAffinity
查看后端名单kubectl get endpoints nginx-svcAddresses 是否与实际 Pod IP 一致
查看新版名单kubectl get endpointslices -l kubernetes.io/service-name=nginx-svcready 地址数量
看 kube-proxy 模式kubectl get cm -n kube-system kube-proxy -o yamlconfig.conf 里的 mode 字段
iptables 规则总入口iptables -t nat -L KUBE-SERVICES -n -vClusterIP 是否出现在链里
定位 SVC 链iptables -t nat -L KUBE-SVC-XXXX -n -v概率规则和跳转目标
看最终 DNATiptables -t nat -L KUBE-SEP-XXXX -n -vto: 后面的 Pod IP 是否正确
IPVS 调度表ipvsadm -Ln --statsVS/RS 对应关系与连接数
看 conntrack 是否丢表conntrack -Sinsert_failed 是否持续增长
看节点路由ip route show table mainPod 网段走哪个接口/下一跳
进 Pod 网络空间抓包nsenter -t <pid> -n tcpdump -i eth0 port 80数据包是否真的进入 Pod

这套命令走下来,绝大多数流量打不到 Pod 的问题都能定位到"名单没更新、规则没写入、路由不通"这三层里的某一层。剩下的少数疑难杂症,就要靠抓包和对 CNI 插件的深入理解了,那是另一个话题。

我个人在实际操作中的体会是,Service 到 Pod 的流量链路并不复杂,复杂的是它跨越了多个层次:控制面有 kube-controller-manager 和 kube-proxy,数据面有 netfilter、路由表和 CNI。平时写 yaml 的时候永远记得一句话:规则是 kube-proxy 写的,路由是 CNI 管的,名单是控制器维护的,三者各司其职,互不越界。把这条边界焊死在脑子里,排障速度至少能快一倍。最后再分享一个小技巧:每次改完 Service 或者扩缩容之后,顺手在节点上把iptables -t nat -L KUBE-SERVICES -n -v和ipvsadm -Ln翻一遍,你见规则见得越多,对这条链路就越有手感,哪天它真的出问题的时候,你闭着眼都能猜到它断在哪一截。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询