一个 Service 建好之后,ClusterIP 在集群里随手就能 curl 通,可你有没有想过:这一瞬间流量到底经过了什么?我在排查线上故障的时候,十次有八次最后都落到了网络层,而网络层的问题里,又有不少跟 kube-proxy 有关。这组件平时不声不响,一出问题就是大面积访问异常,而且现象还很像“应用挂了”,特别容易误判。
先给结论:kube-proxy 本身不负责转发数据包,它只负责往节点上“写规则”。真正在数据通路上搬砖的,是 Linux 内核里的 netfilter 框架,也就是 iptables、ipvs、nftables 这些具体实现。kube-proxy 的角色更像一个“配置下发器”,它监听 API Server 里 Service 和 EndpointSlice 的变化,然后把变化翻译成宿主机上的规则。理解了这个边界,再看它的各种模式和启动参数,就不会被绕晕。
这篇文章写给三类人:想彻底搞懂 Service 底层原理的,集群网络出问题不知道从哪下手的,以及准备 K8s 面试想聊点深度的。我会从工作原理、iptables 模式、IPVS 模式、线上排障、性能调优五个方向拆开讲,全程带命令、带原理、带实际案例。
1. 先搞懂 kube-proxy 在集群里到底干的是什么活
1.1 一个 Service 从创建到访问,背后发生了什么
假设你创建了一个类型为 ClusterIP 的 Service,selector 选中了三个 Pod。Kubernetes 控制面会做两件事:一是给 Service 分配一个虚拟 IP(ClusterIP),二是持续维护这个 Service 对应的 EndpointSlice 对象,里面记录着三个 Pod 的 IP 和端口。
接下来才是 kube-proxy 的主场。每个节点上的 kube-proxy 进程都在 watch Service 和 EndpointSlice 的变更事件,一旦发现有新的 Service 或后端 Pod 变化,它就会在自己的节点上执行一组操作,把这组“逻辑配置”翻译成内核能理解的数据结构。用什么数据结构,取决于你给 kube-proxy 配了什么模式:iptables 模式就写 iptables 规则链,IPVS 模式就创建 IPVS 虚拟服务器,userspace 模式则直接起一个用户态代理进程。
这里面有个很容易被忽略的细节:kube-proxy 是 DaemonSet,每个节点上都有一个独立实例。也就是说,每个节点都维护着整套 Service 规则,不管这个节点上有没有跑对应的 Pod。这样做的好处是:任何节点上的 Pod 访问 ClusterIP 都能立刻命中本地规则,不需要跨节点转发到“中心节点”,避免单点瓶颈。
1.2 kube-proxy 不是转发平面:它只是控制平面的一部分
很多人把 kube-proxy 理解成类似 Nginx 那样的代理进程,这是最大的误解。Nginx 是数据面,流量真的从它进程里过。而 kube-proxy 写完规则之后就没事了,线上数据包根本不经过它的进程。
咱们用一句话概括:kube-proxy 只负责让内核“认识”Service,数据流量的实际转发由 netfilter 在内核态完成。这也是为什么 IPVS 模式性能远好于 userspace 模式——userspace 模式下数据包要从内核态拷贝到用户态,处理好再拷回去,绕一大圈;而 IPVS 和 iptables 模式数据全程在内核态流动,用户态进程只做规则维护。
理解了这一点,排障思路就清晰了:Service 不通,先查规则在不在,再查规则对不对,最后才查数据包实际走到哪了。kube-proxy 挂了不一定影响已有流量(规则还在内核里),但会影响后续变更,比如新 Service 创建后没规则、Pod 扩缩容后后端列表不更新。
1.3 三种模式演进:从用户态代理到内核哈希表
kube-proxy 发展到现在,主流模式是 iptables 和 IPVS,最早还有一个 userspace 模式,现在基本只能在老文档和考古文章里见到了。
userspace 模式是 K8s v1.0 时代的方案,kube-proxy 进程监听一个随机端口,把 ClusterIP 的流量通过 iptables REDIRECT 转发到用户态进程,进程再负载均衡到后端 Pod。这个模式灵活但性能极差,因为每个数据包都要在用户态和内核态之间往返一次,流量一大 CPU 就顶不住。
iptables 模式从 v1.1 开始成为默认,一直沿用到今天。它直接利用内核的 netfilter 框架写规则,没有用户态拷贝,性能比 userspace 好了几个数量级。但随着集群规模变大,iptables 的 O(n) 规则匹配问题开始暴露,规则越多性能越差,这才催生了 IPVS 模式。
IPVS 模式从 v1.8 开始进入 beta,v1.10 左右基本成熟。它利用 Linux 内核自带的 LVS 模块,用哈希表做转发决策,复杂度 O(1),不管后端有多少 Pod,匹配速度几乎恒定。同时 IPVS 还支持多种调度算法,比如 rr、wrr、lc、sh,比 iptables 的随机转发更灵活。现在的生产集群,只要内核支持,我基本都是推荐 IPVS。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心实现原理拆解:iptables 模式到底怎么转发流量
2.1 一条 ClusterIP 规则是怎么组装起来的
iptables 模式下,kube-proxy 会在 NAT 表中写入一批链。以 Service A 为例,它会在 KUBE-SERVICES 链里加一条规则,匹配目标 IP 是 ClusterIP、目标端口是 Service 端口的包,然后跳转到一条专门为该 Service 创建的链 KUBE-SVC-XXX。
KUBE-SVC-XXX 链是负载均衡入口。如果这个 Service 后面有三个后端 Pod,这条链里就会有三条规则,每条规则用 statistic 模块的 random 模式设置概率,比如 probability 0.3333 表示三分之一的流量。命中之后跳转到对应的 KUBE-SEP-XXX 链。
KUBE-SEP-XXX 链处理的是具体后端 Pod 的转发逻辑。它先做 DNAT,把目标地址改成 Pod IP、端口改成 Pod 端口,然后交给 POSTROUTING 链做 SNAT 和 masquerade,最后数据包从节点网卡发往 Pod。如果后端 Pod 跟客户端在同一个节点,还有可能走本地回环路径,绕开真实网卡。
手动模拟一条规则出来看(可以在测试集群里执行 iptables -t nat -L KUBE-SVC-xxx -n -v),你会发现每条规则背后还有一条 KUBE-MARK-MASQ 匹配,作用是给需要做 SNAT 的包打上标记。这些细节平时用不到,但排障的时候能看到这些链,就不会一脸懵。
2.2 负载均衡与会话保持的实现细节
iptables 模式的负载均衡很简单粗暴:每个后端一条规则,用 statistic mode random probability 控制比例,让流量大致均匀地分配到各后端。这是纯概率行为,跟 Dubbo、gRPC 那种客户端主动选的负载均衡完全不同。
说到会话保持,这里有个关键参数 kube-proxy --masquerade-all 和 Service 注解里的 service.spec.sessionAffinity。当设置 sessionAffinity: ClientIP 时,kube-proxy 会启用 recent 模块,在 KUBE-SVC-XXX 链里加一条规则,记录来源 IP,并在一段时间内(默认 10800 秒)把同一个客户端 IP 的请求固定转发到同一个后端。
这功能听着好用,但在实际生产里我一般不建议开。一是 recent 模块在大流量下会占用内存,二是它会破坏负载均衡的均匀性,某些客户端 IP 如果特别活跃,对应后端压力会明显偏高。尤其是短连接场景,根本没有必要做会话保持。
2.3 iptables 模式的性能瓶颈和规则膨胀问题
iptables 模式最大的问题不是转发本身慢,而是规则数量膨胀后,整体匹配成本线性上升。每条规则在链里是顺序匹配的,数据包每经过一条链,要逐条比对,直到命中为止。一个集群如果有 1000 个 Service、平均每个 5 个后端,光 KUBE-SERVICES 链里就有 1000+ 条规则,KUBE-SVC 和 KUBE-SEP 链加起来几千条。数据包经过这些链,走一遍匹配,延迟就上来了。
更麻烦的是规则更新开销。Pod 频繁扩缩容、Service 频繁创建删除时,kube-proxy 会重新计算整个规则集然后全量下发。我用 time iptables-restore 测过一个有 2000 条规则的节点,一次全量恢复耗时接近秒级,这还是在规则量不算特别夸张的情况下。高频变更场景下,用户会明显感觉到网络抖动。
另外 iptables 模式没有真正的健康检查。后端 Pod 如果连续失败,kube-proxy 并不会主动把规则摘掉,它只是根据 EndpointSlice 的状态来调整后端列表。也就是说,如果某个后端 Pod 已经假死(进程还在但不再响应请求),只要 Endpoint 还是 Ready 状态,流量依然会打过去。这需要业务层配合做重试或熔断。
2.4 排障必会:怎么读 KUBE-SVC 和 KUBE-SEP 链
线上 Service 不通,第一步是看节点上有多少条 KUBE- 开头的链:
bash复制iptables -t nat -L KUBE-SERVICES -n -v --line-numbers
这条命令能看到所有 Service 对应的规则入口。如果你能找到目标 ClusterIP 和端口,说明 kube-proxy 至少把 Service 纳管了。接下来跳到对应的 KUBE-SVC-XXX 链,看后端数量是否符合预期:
bash复制iptables -t nat -L KUBE-SVC-XXXX -n -v --line-numbers
如果 KUBE-SVC 链里只有一条规则且概率是 1.0,说明这个 Service 只有一个后端。如果有多条但概率加起来不是 1,说明 kube-proxy 还在调整中(比如正在处理滚动更新),可以等一下再查。
最后看 KUBE-SEP-XXX 链,确认 DNAT 的目标地址是不是正确的 Pod IP 和端口。如果 Pod IP 变了但规则没更新,多半是 kube-proxy 的 watch 出了问题,或者 kubelet 上报状态异常。这种场景,第一反应是看 kube-proxy 日志有没有报错,而不是重启节点。
3. IPVS 模式深度解析:从 iptables 到 ipvs 的迁移和调优
3.1 为什么 IPVS 更快:O(1) 哈希和 O(n) 链表的差距
IPVS 的原理是直接在 netfilter 框架的 INPUT/OUTPUT 链路上挂一个虚拟服务器,通过内核态的哈希表查找目标 Service,再根据调度算法选择后端。它的核心优势有两个:一是查找复杂度是 O(1),不管集群里有多少 Service,查一条哈希表的时间基本不变;二是后端调度放到内核里,支持 rr、wrr、lc、sh 等算法,不需要像 iptables 那样用概率模拟。
我做过一次测试:同一个节点上挂 500 个 Service,iptables 模式压测时 p99 延迟大约比 IPVS 模式高 30% 左右;当 Service 数量涨到 2000 时,iptables 延迟继续爬升,而 IPVS 几乎是一条直线。规则更新上,IPVS 是增量下发(通过 netlink 逐个操作 VirtualServer/RealServer),不像 iptables 动不动全量刷新,Pod 频繁变更时对集群网络的影响小得多。
当然 IPVS 也不是没有代价。它需要内核加载 ip_vs 模块,而且每个节点上的 kube-proxy 需要拥有创建 IPVS 规则的能力,特权模式是跑不掉的。另外 IPVS 的调试依赖 ipvsadm 工具,不像 iptables 那么普及,排障时需要额外安装。
3.2 内核模块检查:IPVS 模式启动前必做的准备工作
迁移到 IPVS 模式之前,先确认节点内核是否加载了相关模块。默认的 kube-proxy 容器镜像会尝试加载这些模块,但如果你用的是自定义内核或容器运行时权限受限,可能会加载失败。
bash复制lsmod | grep ip_vs
如果输出为空,说明模块没有加载,需要手动加载:
bash复制modprobe ip_vs
modprobe ip_vs_rr
modprobe ip_vs_wrr
modprobe ip_vs_sh
modprobe nf_conntrack
生产环境最好把这些模块写进 /etc/modules-load.d/ 或者节点启动脚本里,否则节点重启后 kube-proxy 会因为没有 IPVS 支持而自动回退到 iptables 模式。回退本身不会造成流量中断,但会让你以为在跑 IPVS 实际却在跑 iptables,性能问题和排查方向全偏了。
3.3 关键启动参数解读:从 scheduler 到 excludeCIDRs
kube-proxy 的启动参数很多,--proxy-mode=ipvs 只是最基本的。实际调优时,我更关心这几个:--ipvs-scheduler 决定后端选择算法,默认是 rr 轮询,如果后端处理能力差异大,可以改成 wrr 并配置权重;--ipvs-exclude-cidrs 用来排除不需要做 IPVS 转发的网段,比如集群 Pod 网段和 Service 网段有时候需要同时排除,避免误匹配;--cluster-cidr 如果设置了,kube-proxy 会对进出集群的流量做 masquerade,这样外部流量到 NodePort 后回包路径才正确。
还有个参数容易踩坑:--ipvs-strict-arp。这个参数在 MetalLB 这类把 kube-proxy 用于 LoadBalancer 服务的场景里必须开启,它会开启 arp_ignore 和 arp_announce,防止节点对虚拟 IP 的 ARP 请求做出响应,避免多个节点同时响应导致流量走错节点。但普通集群里如果没这个需求,开了反而可能引发一些小问题,所以按需配置。
3.4 如何验证 IPVS 规则:ipvsadm 实战
模式切换后,怎么确认规则确实进了 IPVS 而不是还在 iptables?直接查:
bash复制ipvsadm -Ln
输出会类似这样:
code复制IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn
TCP 10.96.0.1:443 rr
-> 192.168.1.10:6443 Masq 1 0
TCP 10.96.0.10:53 rr
-> 192.168.1.11:53 Masq 1 2
-> 192.168.1.12:53 Masq 1 1
看到 LocalAddress 那一列是 ClusterIP,RemoteAddress 是 Pod IP,说明 IPVS 规则已经生效。注意 Forward 列是 Masq,表示做 SNAT,也可以是 Tunnel 或 DirectRoute,kube-proxy 默认用的是 Masq。
跟 iptables 模式有个明显区别:IPVS 模式下 iptables NAT 表里不会再出现一大堆 KUBE-SVC 链,只会有少量辅助规则(比如必须保留的 KUBE-MARK-MASQ)。如果你同时看到大量 KUBE-SVC 链和 IPVS 规则,可能是有其他组件(比如某些 CNI 插件)也写了 iptables,要留个心眼。
4. 线上故障排查实战:Service 不通,先查这几样
4.1 第一刀切在哪里:从 Pod 到 Service 还是从外部到 NodePort
Service 流量路径分两段:Pod 访问 ClusterIP 是内部路径,外部访问 NodePort 是外部路径。排查前先确认用户报告的是哪一段,排查思路完全不同。内部路径大概率是 ClusterIP 或者 DNS 解析出问题;外部路径则要看 NodePort 是否监听、节点防火墙是否放行、云平台安全组策略。
我先说内部路径。在问题 Pod 所在节点上执行:
bash复制curl -v http://10.96.0.10:53
如果通,说明本节点的 kube-proxy 规则正常,问题可能在目标 Service 的后端 Pod 上。如果不通,下一步确认目标 Service 是否存在:
bash复制kubectl get svc -n <namespace>
然后确认 EndpointSlice 列表:
bash复制kubectl get endpointslices -n <namespace>
如果 EndpointSlice 里确实有 Ready 地址,但 curl 不通,重点检查节点上的 iptables/IPVS 规则是否和 EndpointSlice 一致。如果规则缺失,重启 kube-proxy Pod 通常能触发重新同步,但要先确认 API Server 连不上还是 watch 出问题。
4.2 高频故障速查表
我在线上踩过不少坑,把高频问题整理成一张表,照着顺序排查能省不少时间。
| 故障现象 | 优先排查项 | 常用命令 |
|---|---|---|
| 从 Pod curl ClusterIP 超时 | 本机 kube-proxy 规则是否存在 | iptables -t nat -L KUBE-SERVICES 或 ipvsadm -Ln |
| 从 Pod curl ClusterIP 间歇性失败 | conntrack 表是否溢出 | dmesg 查 nf_conntrack: table full |
| Service 有 Endpoint 但部分后端不通 | Endpoint 是否 Ready,Pod 是否假死 | kubectl get endpointslices -o yaml |
| 外部访问 NodePort 不通 | 节点防火墙、云安全组 | ss -lntp 查监听端口 |
| 规则存在但流量走到错误后端 | IPVS 下检查调度算法,iptables 下检查概率配置 | ipvsadm -Ln --stats |
| kube-proxy 日志有弃用/错误信息 | 版本兼容性 | kubectl logs -n kube-system kube-proxy-xxx |
| 修改 Service 注解不生效 | kube-proxy 需要重启?不一定,看具体版本 | 手动查注解和实际规则对比 |
4.3 真实案例:conntrack 表溢出导致的间歇性丢包
有一次客户报障,说一个生产服务“偶尔访问超时”,频率不高但很稳定,大约每几分钟出现一次。直接查应用层日志什么都看不出来,CPU 内存都正常,Pod 也没有重启。按经验怀疑是网络层丢包。
在节点上用 dmesg 查内核日志,发现了关键线索:
code复制kernel: nf_conntrack: table full, dropping packet.
这是 conntrack 表满了。访问高并发时每一条连接都会占用一条 conntrack 记录,如果表空间不足,内核会直接丢弃新连接的 SYN 包,表现为“间歇性超时”。为什么跟 kube-proxy 有关?因为不管 iptables 还是 IPVS,只要开启了 masquerade(SNAT),就必须维护连接跟踪表。Service 越多、连接越频繁,conntrack 表的压力就越大。
解决办法分两步:一是调大 conntrack 表容量,二是缩短超时时间,及时回收无效记录。
bash复制sysctl -w net.netfilter.nf_conntrack_max=131072
sysctl -w net.netfilter.nf_conntrack_tcp_timeout_established=86400
上面这个是临时调优,生产环境要写到 /etc/sysctl.d/ 持久化。另外还要看节点规格,nf_conntrack_max 是根据内存自动算出来的,如果你开得过大,内存占用也会很高,需要权衡。这个案例最后把 max 从 65536 调到 262144,超时保持默认,问题就消失了,并且加了告警监控该指标。
4.4 会话保持失效,问题不一定在 kube-proxy
还有一个案例挺有意思。业务方配置了 sessionAffinity: ClientIP,但测试发现会话总能保持几十秒就断了。一开始以为是 kube-proxy 的 recent 模块或 IPVS 的持久性配置没生效,查了老半天,规则没问题,IPVS 配置也能看到 persistent 标记。
后来才发现是客户端从两个不同的出口 IP 发请求。客户端是一个反向代理集群,两个节点各自出公网,服务端看到的源 IP 不固定,会话自然保持不住。这个和 kube-proxy 半毛钱关系都没有,完全是链路层面的问题。所以排查时先确认“同一个 IP”这个前提是否成立,不然会在错误的方向上白费很多时间。
5. 日常变更、升级和性能优化的实操经验
5.1 kube-proxy 版本升级怎么做到不掉流量
kube-proxy 是 DaemonSet,升级方式比普通应用要小心一点。直接删掉所有 Pod 让新版本全部重建,风险在于滚动过程可能产生空窗,毕竟每个节点都有一个实例,同时重建会导致集群网络规则短暂缺失。
我推荐用滚动更新策略,每次只更新一部分节点,观察新版本的日志、规则生成情况,确认没问题再继续。可以通过 kubectl rollout restart daemonset/kube-proxy -n kube-system 触发,也可以用第三方工具做分批灰度。更谨慎一点,可以先手动挑一个边缘节点,用 kubectl delete pod kube-proxy-xxx 触发重建,验证新版本在单个节点上工作正常,再放量。
升级前一定要做一件事:备份当前节点的规则状态。iptables 模式可以用 iptables-save > /tmp/iptables.bak,IPVS 用 ipvsadm -Sn > /tmp/ipvs.bak。虽然 kube-proxy 启动后会重新同步规则,但万一 API Server 有问题、同步失败,备份就是最好的回退手段。
5.2 大规模集群的 kube-proxy 调优建议
集群规模大了之后,kube-proxy 本身也可能成为瓶颈,尤其是控制面事件处理这一侧。几百个 Service 看不出问题,到几千上万个 Service 时,watch 事件量、规则计算量、内核 netlink 通信量都会明显上升。此时通常需要做几件事:一是给 kube-proxy 分配更多 CPU 资源,避免被限制在单核上;二是调整 --conntrack-max-per-core 参数,让 conntrack 表按 CPU 核数扩展,充分发挥多核能力;三是关注 kube-proxy 内存占用,EndpointSlice 在大量 Pod 场景下会占用不少内存,如果节点内存紧张,规则同步可能会变慢甚至 OOM。
网络层面也有个优化点:net.netfilter.nf_conntrack_max 和 net.core.somaxconn 这种内核参数,建议通过系统级配置统一管理,而不是每次在 kube-proxy 容器里 setting,否则节点重启后就丢了。另外 IPVS 模式下不要忘记 ip_vs_conn_reuse_mode=0 这个内核参数,默认值在某些内核版本下会导致新连接重用到旧连接的端口,引发 STATUS 异常,这算是一个比较经典的老坑。
5.3 容易被忽视的几个 kube-proxy 细节
第一个细节:kube-proxy 的 --cluster-cidr 不设置也可以工作,但外部访问 NodePort 时,如果后端 Pod 和访问者不在同一个节点的网络命名空间,回包路径可能有问题。生产环境我都会配置这个参数,让 kube-proxy 对跨节点流量做 masquerade,确保回包能正确回到客户端。
第二个细节:kube-proxy 和 CNI 插件在 NAT 表里是共存的,如果 CNI 插件也在 NAT 表里写规则,规则顺序会影响转发结果。排查时不要只看 kube-proxy 的链,要把整个 NAT 表的链路顺序打印出来:iptables -t nat -L -n -v | head -100。有些奇怪的网络问题,其实是 CNI 的 POSTROUTING 规则和 kube-proxy 的 KUBE-POSTROUTING 链顺序冲突导致的。
第三个细节:kube-proxy 的日志级别。默认日志不多,但遇到诡异问题要开调试日志,kube-proxy --v=4 可以看到详细的同步过程。注意这会让日志量爆炸,只适合在单个节点上临时开启,不要整个集群都调。
5.4 从 iptables 平滑迁移到 IPVS 的操作步骤
如果你现在还是 iptables 模式,想迁移到 IPVS,建议按这个节奏来。先在测试集群改模式,跑几天看稳定性,然后逐个节点灰度。修改方式是在 kube-proxy 的 ConfigMap 里把 mode 改为 ipvs,然后重启 kube-proxy Pod。重启后立刻用 ipvsadm -Ln 确认规则生成,同时观察业务访问是否正常。
迁移前最好做一个规则对比:在迁移前把某个 Service 在 iptables 模式下的规则结构记录下来,迁移后在 IPVS 下核对同一个 Service,确保后端列表一致。另外确认目标内核的 IPVS 模块版本,如果模块太老,某些新特性(比如 ipvs-sh 算法)不生效,需要升级内核。
我自己踩过一次坑:集群上千个 Service,直接全量切换 IPVS,切换时 kube-proxy 重建规则导致 kubelet 和其他组件的健康检查连接中断了一下,引起集群短暂抖动。后来改成按节点分批迁移,每个节点迁移前先 cordon,排空流量,再操作,就没有再出过问题。后续如果再遇到类似场景,这个教训一直提醒我:控制面组件,永远不要图快直接全量动。
结尾
最后再说一点个人心得。kube-proxy 这个组件,表面上看就是写写规则,但深入进去你会发现它牵涉到 Linux 网络协议栈、iptables/IPVS 内核模块、K8s 控制面事件机制、CNI 网络插件等多个层面的交叉。排查问题的时候,我习惯先用“规则在不在、规则对不对、流量走哪了”三步走定位,大部分问题都能在两三分钟内找到方向。
平时没事的时候,我建议在测试集群里手动创建几个 Service,然后用 iptables -t nat -L -n -v 和 ipvsadm -Ln 对比两种模式的规则形态。多折腾几次,你对 Service 网络的直觉会比看文档深刻得多。后面如果再遇到类似的问题,至少不会两眼一抹黑,直接先去看 Pod 了。
