
前阵子帮朋友排查一个 Kubernetes 集群的问题三四百个 Service 的测试环境压测一上来kube-proxy 所在节点的 CPU 直接冲到 70% 以上业务倒是没挂但 NodePort 转发延迟肉眼可见地变高。我上节点用iptables-save一看KUBE-SERVICES 链后面挂了几千行规则瞬间就想起那个被问过无数次的问题kube-proxy 的 iptables 代理模式和 IPVS 代理模式到底差在哪什么时候该切换先说清楚这里讨论的代理模式指的都是 kube-proxy 处理 Service 流量的方式。Service 是 Kubernetes 里给一组 Pod 提供稳定访问入口的抽象kube-proxy 负责把发往 ClusterIP、NodePort、LoadBalancer 的流量转发到后端 Pod 上。转发怎么实现目前最主流的就是 iptables 和 IPVS 两条路另外还有已经被边缘化的 userspace 模式以及在新版本里正在试验的 nftables 模式。这篇文章把 Service 流量从进入节点到送达 Pod 的完整路径拆开讲对比两种模式的原理差异、性能边界、故障表现再给出一套我实际用过的切换和排障方案。1. 先搞清楚kube-proxy 的代理模式解决的是什么问题1.1 Service 的流量转发难题在没有 Service 的世界里Pod 的 IP 是不断变化的扩缩容、重建、调度都会让 IP 失效。Service 提供了一个稳定的虚拟 IPClusterIP和端口用户访问 Service 时kube-proxy 需要在节点上做一次流量搬运把目标地址从 ClusterIP 改写成某个后端 Pod 的 IP再把回包原路带回来。这个搬运动作听着简单但里面有三个麻烦事第一个后端列表是动态的。Pod 随时可能被替换每增加一个副本kube-proxy 都要立刻感知并调整自己的转发规则。第二个负载均衡。如果有 5 个后端流量不能全打到一个 Pod 上。第三个回包问题。客户端访问的是 ClusterIP但实际收到流量的是后端 Pod后端 Pod 回包时源 IP 是自己的 Pod IP客户端根本不认识这个包必须被改回去。这里就涉及到 SNAT/MASQUERADE 的配合。1.2 为什么 kube-proxy 的转发方式成为性能关键kube-proxy 并不是数据面的流量转发者它其实只是个规则维护者。真正的转发动作发生在 Linux 内核里kube-proxy 只负责把 Service 和 Endpoint 的变化翻译成内核能理解的规则。问题就在翻译这两个字上。iptables 模式和 IPVS 模式本质上是两套完全不同的内核数据路径iptables 模式把 Service 翻译成一条条 iptables NAT 规则流量进入节点后在 PREROUTING 链里被一条条规则扫描匹配。IPVS 模式把 Service 翻译成内核里的一张虚拟服务器哈希表流量进入节点后在内核态直接查表决定转发目标。单看这个差别你大概能猜出性能分水岭在哪了。下面两章分别拆细节。2. iptables 模式把负载均衡这件事翻译成防火墙规则2.1 一条请求在 iptables 模式下到底走了哪些链假设集群里有一个 ServiceClusterIP 是 10.96.0.10端口 80后端有三个 PodPod IP 分别是 10.244.1.2、10.244.2.3、10.244.3.4。客户端在集群内访问 10.96.0.10:80数据包在节点上的路径大概是这样的数据包进入节点被 nat 表的 PREROUTING 链接管。PREROUTING 跳转到 KUBE-SERVICES 自定义链。KUBE-SERVICES 链里有一条规则专门匹配这个 Service目标地址是 10.96.0.10 且端口 80匹配则跳到该 Service 对应的 KUBE-SVC-XXX 链。KUBE-SVC-XXX 链是负载均衡链里面有若干条规则按概率把流量分发给不同的 KUBE-SEP-XXX 链。KUBE-SEP-XXX 链里执行真正的 DNAT把目标 IP 改成某个 Pod IP端口改成 Pod 端口。在后端 Pod 返回数据包时因为 conntrack 记录了这次 DNAT 的原始映射回复包会自动做反向转换源 IP 变回 ClusterIP客户端无感知。用命令看直观得多。在节点上执行iptables-save -t nat能看到类似这样的规则片段-A KUBE-SERVICES -d 10.96.0.10/32 -p tcp -m tcp --dport 80 -j KUBE-SVC-XYZ -A KUBE-SVC-XYZ -m statistic --mode random --probability 0.5000000000 -j KUBE-SEP-A -A KUBE-SVC-XYZ -m statistic --mode random --probability 0.3333333333 -j KUBE-SEP-B -A KUBE-SVC-XYZ -j KUBE-SEP-C -A KUBE-SEP-A -p tcp -m tcp -j DNAT --to-destination 10.244.1.2:8080 -A KUBE-SEP-B -p tcp -m tcp -j DNAT --to-destination 10.244.2.3:8080 -A KUBE-SEP-C -p tcp -m tcp -j DNAT --to-destination 10.244.3.4:8080注意概率规则的顺序。三个后端的场景下第一条规则的概率是 0.5不是 0.333这是因为 iptables 的 statistic 模块是命中即终止第一条命中了 50% 的流量剩下的 50% 继续往下走第二条的概率要按剩余流量里的 50%来算也就是全局的 33% 左右最后一条兜底。2.2 链式匹配的时间复杂度规则越多代价越高iptables 的核心数据结构是链表数据包从链头开始逐条规则匹配命中后执行动作没命中就继续往下走。这是一个典型的线性扫描过程。当集群里只有几十个 Service 时这条链很短扫描代价可以忽略。但当 Service 数量涨到几百上千KUBE-SERVICES 链会变得非常长每个包都要从头扫到尾。更麻烦的是每个 Service 的负载均衡链 KUBE-SVC-XXX 本身也是一条链如果后端数量很多这条链也要逐条扫。我在那个三四百个 Service 的测试环境里见过 KUBE-SERVICES 链后面跟了超过 3000 条规则。K8s 社区和不少性能测试都提到过iptables 模式在规则量达到几千条后转发延迟和 CPU 消耗会明显上升。这不是某条命令慢的问题而是数据路径上每个包都要付出更多匹配成本属于结构性的瓶颈。2.3 规则同步也是隐藏的痛点kube-proxy 每次感知到 Service 或 Endpoint 变化都要重新计算整套 iptables 规则然后通过iptables-restore一次性灌进内核。问题在于规则量越大这次灌入的时间就越长。有一个很容易被忽略的联动效应后端 Pod 在频繁扩缩容或滚动更新的集群里Endpoint 会持续变化kube-proxy 需要不断做整表替换。在这个过程中iptables 规则处于新旧交替的不稳定窗口期极少数新建连接可能会被转发到已经不再就绪的后端。虽然 kube-proxy 会尽量把变化做成原子操作但规则量的增长会让这个窗口越来越明显。2.4 隐藏的 conntrack 依赖iptables 模式做 DNAT必须依赖 conntrack连接跟踪记录原始目标地址到新目标地址的映射否则回包来了不知道该怎么还原。每个经过 DNAT 的连接都会占用一条 conntrack 表项。正常情况下这没什么问题但高并发短连接场景会很伤。压测时大量短连接快速建立、快速关闭conntrack 表项虽然会老化回收但瞬时并发一高表很容易被打满。表满之后新的连接会被内核直接丢弃表现就是间歇性超时。这个坑我在第 5 章的排障实录里会细讲它表面上是系统参数问题根子却在 kube-proxy 的转发模式选择上。3. IPVS 模式内核态四层负载均衡器接管转发3.1 IPVS 是什么从 LVS 走进 KubernetesIPVSIP Virtual Server不是 Kubernetes 发明的它是 Linux 内核自带的四层负载均衡框架最初由 LVS 项目贡献给内核在数据中心里被用了很多年。它的核心能力是把发往某个虚拟 IP 的流量按调度算法转发到一组真实服务器上。kube-proxy 在 iptables 之外选择了 IPVS相当于把负载均衡这个专业活交还给了内核里专门干这件事的模块而不是让防火墙模块来客串。Kubernetes 从 1.6 左右开始把 IPVS 模式作为 alpha 引入经历几个版本的演进后逐步成熟现在已经是很稳定的生产方式。3.2 虚拟服务器与真实服务器一张哈希表完成查找IPVS 模式下kube-proxy 会把每个 Service 的 ClusterIP:Port 注册成一个虚拟服务器Virtual Server把每个后端 Pod 注册成真实服务器Real Server通过 netlink 接口写入内核。数据包进来后IPVS 先查虚拟服务器表再看调度算法选哪个后端然后做地址转换并转发。在节点上执行ipvsadm -L -n可以看到类似这样的输出IP Virtual Server version 1.2.1 (size4096) Prot LocalAddress:Port Scheduler Flags - RemoteAddress:Port Forward Weight ActiveConn InActConn TCP 10.96.0.10:80 rr - 10.244.1.2:8080 Masq 1 0 0 - 10.244.2.3:8080 Masq 1 0 0 - 10.244.3.4:8080 Masq 1 0 0IPVS 内部用哈希表组织虚拟服务器数据包根据目标 IP 和端口计算哈希直接定位到对应的虚拟服务器不需要像 iptables 那样一条条规则扫描。查找时间复杂度接近 O(1)这也是 IPVS 在大规模 Service 场景下性能更平稳的最根本原因。3.3 调度算法给负载均衡带来的自由度iptables 模式做负载均衡就是靠概率规则相当于随机分发。IPVS 则自带一整套调度算法kube-proxy 允许你通过配置选择rr轮询请求依次分给每个后端是 kube-proxy 的默认算法。wrr加权轮询适合后端配置不均衡的场景。lc最少连接把新连接分给当前活跃连接数最少的后端。wlc加权最少连接。sh源地址哈希同一个来源 IP 的连接会被分到同一个后端天然支持会话保持。dh目标地址哈希。sed、nq 等在特殊场景下才会用到。r 和 wrr 在日常业务里基本够用如果后端 Pod 的处理能力有明显差异或者你想针对某个来源 IP 做会话保持IPVS 提供的选择就比 iptables 丰富得多。3.4 IPVS 模式里 iptables 并没有完全退休很多人有个误解以为切到 IPVS 模式后 iptables 就完全没有存在感了。实际上 IPVS 只负责看门把流量分发给正确的后端但集群里还有一些场景仍然需要 iptables 参与比如某些需要被强制丢弃或者做特殊标记的流量。涉及 MASQUERADE 的场景。部分插件比如网络策略相关的实现也会依赖 iptables。所以切到 IPVS 之后iptables-save里依然能看到规则只是数量会比纯 iptables 模式少很多。kube-proxy 日志里也会明确显示当前用的代理方式第 6 章会写验证方法。4. 两种模式的分水岭性能、故障表现与运维习惯4.1 小规模集群看不出差距规模上来后差异明显如果你的集群只有几十个 Service后端 Pod 数量也不多iptables 模式完全够用换不换 IPVS体感几乎为零。我自己在小型测试集群里做过粗略对比几十个 Service 时两种模式的 CPU 和延迟差异都在误差范围内。但规模一旦上来差距就出来了。我自己的运维体感是Service 数量到几百个节点上 KUBE-SERVICES 相关规则膨胀到几千条之后iptables 模式会出现两个明显的征兆——kube-proxy 同步规则变慢压测时节点 CPU 中软中断和 netfilter 相关的开销上升。IPVS 模式在这两个维度上的表现都平稳得多因为它不需要维护超长规则链。4.2 故障模式对比规则同步、连接跟踪、滚动更新故障表现是最能体现两种模式差异的地方。iptables 模式的问题在于线性扫描 整表替换。规则多的时候转发延迟受规则量影响规则同步存在时间窗口连接跟踪表容易被高并发短连接打爆。它做的负载均衡本质是概率后端数量骤增骤减时规则重建期间的连接失败率会略有上升。IPVS 模式的问题在于查表快但有些问题藏得更深。如果内核没加载对应模块kube-proxy 会静默回退到 iptables日志里只有一行警告不细心的人可能根本发现不了。另外 IPVS 依赖内核模块和 ipvsadm 工具链排查问题要会看 ipvsadm 的计数器和统计信息这对习惯看 iptables 规则的人来说有一个适应期。4.3 可观测性差异iptables-save vs ipvsadm排查 Service 转发问题时两种模式的排查入口完全不同。iptables 模式的思路是iptables-save -t nat找到 KUBE-SERVICES 链然后顺着 SVC 链和 SEP 链找 DNAT 规则用iptables -t nat -L -n -v看命中计数器定位流量有没有走到预期的链上。IPVS 模式的思路是ipvsadm -L -n看虚拟服务器和真实服务器列表ipvsadm -L -n --stats看累计连接数和字节数ipvsadm -l -n --rate看实时速率。如果某个后端 Pod 的 ActiveConn 和 InActConn 异常再回到 Endpoint 层面核对 Pod 状态。坦白说如果团队里所有人都只会iptables-save那切到 IPVS 之前一定要先补一下 ipvsadm 的使用。工具是小事但排障思路完全不同生产环境卡在排查这一步会非常难受。4.4 一张表看清主要差异对比项iptables 模式IPVS 模式底层实现NAT 链式规则匹配内核哈希表 调度算法查找复杂度随规则量线性增长近似 O(1)负载均衡方式statistic random 概率分发支持 rr、wrr、lc、sh 等算法内核模块依赖无额外依赖需要 ip_vs 等模块连接跟踪依赖强依赖 conntrack依赖较低使用自己的连接表规则更新方式iptables-restore 整表替换netlink 增量更新大规模下表现规则膨胀后延迟和 CPU 上升性能相对平稳运维工具iptables-save / iptablesipvsadm回退风险kube-proxy 默认选项无回退风险模块未加载时可能静默回退 iptables5. 我的一次排障实录conntrack 表满引发 NodePort 持续超时5.1 现象压测一到高峰期连接开始间歇性失败有次在一个测试集群里帮人压测集群用的是 iptables 模式业务方发现只要 QPS 冲到一定量级NodePort 访问就开始间歇性超时。业务 Pod 的 CPU、内存都正常Service 的 Endpoint 列表没问题后端副本数也没变。一开始我怀疑是应用层的问题但压测工具报的是 TCP 连接超时不是 HTTP 5xx这说明数据包可能压根没到后端或者到了之后回包丢了。5.2 排查链路从 Endpoint 一路查到内核日志我的排查顺序是这样的第一步确认 Service 和后端状态正常kubectl get endpoints service-name kubectl get pods -o wide | grep service-nameEndpoints 正常三个副本都是 Ready。第二步看 kube-proxy 有没有报错kubectl -n kube-system logs kube-proxy-pod --tail50没有明显异常。第三步看内核的 netfilter 日志。这一步是转折点dmesg | grep -i nf_conntrack输出里出现了很扎眼的nf_conntrack: table full, dropping packet。这说明连接跟踪表满了新连接被内核直接丢弃。第四步用conntrack -S看统计信息确认表项是不是持续在涨同时看两个关键内核参数conntrack -S sysctl net.netfilter.nf_conntrack_max sysctl net.netfilter.nf_conntrack_count结果很清晰nf_conntrack_max是默认的 65536而nf_conntrack_count已经顶到了上限附近。5.3 根因确认DNAT 与高并发短连接压垮了连接跟踪表根因其实不复杂iptables 模式做 DNAT每个连接都要在 conntrack 里占一条表项用于记录回包时怎么把源地址还原成 ClusterIP。压测业务大量使用短连接每个请求都新建连接TIME_WAIT 状态的连接表项会在短时间内堆积。表一满新的连接进不来表现就是间歇性超时。这里有个容易被忽略的点即使后端返回很快连接关闭之后 conntrack 表项也不会立刻被清掉而是会保持一段时间等待超时回收。高并发场景下表项的回收速度赶不上新增速度表满几乎是必然的。5.4 临时止血与后续调整临时解决先把表调大sysctl -w net.netfilter.nf_conntrack_max262144 sysctl -w net.netfilter.nf_conntrack_buckets65536把配置持久化到/etc/sysctl.d/99-conntrack.conf。表调大之后压测问题立刻缓解。但这只能止血。如果服务和流量继续增长iptables 模式下还会有新的瓶颈而且 conntrack 表调太大会占用太多内存不是无脑调大的方案。那次之后我建议业务方把集群的 kube-proxy 切到了 IPVS 模式。IPVS 对 conntrack 的依赖低很多连接跟踪压力明显下降同样的压测场景就没再出现表满丢包的问题。6. 切换到 IPVS 模式的完整操作与回退方案6.1 内核模块与依赖检查切换前先确认节点内核模块。IPVS 需要ip_vs核心模块以及你计划用到的调度算法模块。至少保证下面几个存在modprobe ip_vs modprobe ip_vs_rr modprobe ip_vs_wrr modprobe ip_vs_sh为了保险最好把这几个模块写入系统启动加载配置cat /etc/modules-load.d/ipvs.conf EOF ip_vs ip_vs_rr ip_vs_wrr ip_vs_sh EOF不同节点的内核可能不一样尤其是混部集群最好把每个节点都检查一遍。我遇到过某个节点没加载ip_vs_wrrkube-proxy 日志里报调度算法不可用然后整个节点回退到 iptables 模式的情况。6.2 修改 kube-proxy 配置并滚动重启kubeadm 部署的集群kube-proxy 是 DaemonSet配置存在 kube-system 命名空间下的 ConfigMap 里。直接改kubectl -n kube-system edit configmap kube-proxy把mode字段改成ipvs关键片段类似这样kind: KubeProxyConfiguration mode: ipvs ipvs: scheduler: rr strictARP: true不同版本的 apiVersion 可能不同但mode字段一直存在。strictARP我建议打开尤其是集群里有 LoadBalancer 相关组件时否则可能出现 ARP 响应异常的问题。保存后滚动重启kubectl -n kube-system rollout restart daemonset/kube-proxy6.3 验证切换是否真的生效重启不能只凭感觉要看实际证据。先看日志kubectl -n kube-system logs kube-proxy-pod | grep Using输出是Using ipvs Proxier.才说明切换成功。如果输出Using iptables Proxier说明 kube-proxy 回退到了 iptables需要检查模块和 ipvsadm 是否就绪。然后看节点上的 IPVS 规则ipvsadm -L -n能看到一堆 ClusterIP 和 NodePort 对应的虚拟服务器以及各自的 Real Server。再对比一下 iptables 规则数量iptables-save -t nat | grep KUBE-SERVICES | wc -l规则数量会比重启前少很多。最后做功能回归集群内的 ClusterIP 访问、NodePort 访问、如果有 LoadBalancer 也一起测。要注意的是切换代理模式的过程中已有连接可能会被中断建议选在低峰期操作并通知业务方重试或重启存量连接。6.4 回退 iptables 的注意事项切换有风险回退姿势也要提前想好。回退就是把 ConfigMap 里的mode改回iptables再滚动重启 kube-proxykubectl -n kube-system edit configmap kube-proxy # mode 改回 iptables kubectl -n kube-system rollout restart daemonset/kube-proxy回退后同样看日志确认Using iptables Proxier。有一点要注意IPVS 模式下已经建立的连接在回退到 iptables 后可能因为规则变化而断开属于正常现象。如果业务对连接中断特别敏感回退窗口要再谨慎一些。7. 聊聊我怎么给现网集群做最终选型跑完上面这套对比和排障我个人的选择逻辑其实很清晰。如果你在维护一个 200 个 Service 以内、后端 Pod 变化不频繁的集群iptables 模式完全够用不需要折腾内核模块排查工具也直观这是它的舒适区。反过来如果集群规模会持续增长或者业务有大量短连接、高并发访问 Service 的场景IPVS 模式能从底层减轻很多压力值得早点切过去。我那个被压测打爆 conntrack 的集群切到 IPVS 之后同类场景下的连接跟踪问题基本消失了。最后提一个容易忽略的细节每次切换模式都要回到底层确认不要只看配置。配置写了 ipvs但内核模块没加载、ipvsadm 缺失、或者节点内核太老kube-proxy 都会悄悄退回 iptables。用日志和ipvsadm -L -n双重确认才能保证你预想中的代理模式真的在生效。