K8S集群跑大半年之后,该到的坎一个都不会少。Service数量一上来,kube-proxy的iptables模式就开始拖后腿,高频新建连接时CPU毛刺明显,规则更新时整个集群的网络通路跟着抖一下。身边不止一个同行在集群规模到几百个Service之后,第一件动刀的事就是把kube-proxy从iptables切到IPVS。这篇文章就把我这边实操过的切换过程、原理、坑点和验证手段完整梳理一遍,给正准备做同样优化的团队一个可落地的参考。
1. 先搞清楚K8S三种转发模式,别稀里糊涂就切
1.1 kube-proxy的三种工作模式
kube-proxy是K8S里负责Service网络转发实现的核心组件,它的职责说白了就是两件事:监听API Server里Service和Endpoints的变化,然后把这些变化转成宿主机上实际生效的转发规则。这个转发规则落在哪里、以什么形式落地,就对应了三种不同模式。
userspace模式是老古董了,流量要先经过kube-proxy进程本身,再通过用户态和内核态的切换转发出去,性能和吞吐量都很差,现在已经基本没人用,只作为一个历史选项留着。
iptables模式是K8S的默认模式,工作在内核态netfilter框架上。它为每个Service生成一系列iptables规则,利用NAT机制把访问ClusterIP的流量DNAT到后端的Pod IP。这个模式的好处是成熟、稳定、零额外组件依赖,绝大多数集群开箱即用。但它的短板也明显:规则是链式结构,流量进来之后要沿着规则链一条一条匹配。
IPVS模式同样是内核态的转发方案,但它不是基于netfilter的规则链,而是基于内核的IPVS模块。IPVS本身就是为负载均衡设计的,使用哈希表来存储转发规则,时间复杂度是O(1)级别的查找,不像iptables那样要线性遍历。
注意:切换IPVS模式并不等于完全抛弃iptables。K8S里ClusterIP、NodePort、ExternalIP这些Service类型,还有LoadBalancer的实现,都会有一部分功能仍然依赖iptables规则兜底。所以别抱着"切到IPVS之后iptables规则就清零"的预期,正确的理解是:核心的负载均衡和转发逻辑从iptables换成了IPVS,iptables退居二线,只做辅助性的包处理。
1.2 默认的iptables模式为什么在大规模下会难受
iptables的转发靠PREROUTING、INPUT、FORWARD、OUTPUT、POSTROUTING这些链,K8S的kube-proxy在NAT表里为每个Service创建KUBE-SVC-XXX系列链,每个Service下有若干条规则对应后端Pod。当集群里有几百个Service、每个Service又有多个后端副本时,规则数量是Service数量乘以后端数量,轻松上千条甚至几千条。
流量访问Service时,内核要按顺序匹配这些规则。虽然内核的iptables匹配有缓存优化,但在连接新建速率很高或者规则规模很大的场景下,CPU的开销依然很可观。我见过一个集群在2000多条iptables规则的情况下,高并发压测时节点上sys CPU占比明显飙升,网络延迟和P99也同步跟着变差。
更头疼的问题是规则更新的抖动。每当有Service或Endpoints变更,kube-proxy会把整个规则集重新计算一遍,然后一次性刷入内核,这个过程是全量更新。更新期间,新连接可能因为规则处于不一致状态而失败或重试,表现就是服务调用出现偶发的connection reset、超时,业务方会来问"集群是不是又抖了"。
1.3 什么情况下值得切换到IPVS
不是所有集群都需要切。小规模集群、Service数量几十个以内、流量模型又简单的话,iptables完全够用,切不切没太大差别。但如果你遇到下面这些信号,就该认真考虑IPVS了:
- Service和Endpoints总量达到几百甚至上千级别;
- 高频短连接场景多,转发路径的CPU开销已经能通过监控观测到;
- 集群里经常发布服务,Endpoints频繁变动,iptables全量刷新带来的抖动开始影响业务;
- 需要更丰富的负载均衡调度策略,比如加权轮询、最少连接、源地址哈希等,iptables模式下这些很难实现。
2. IPVS赢在哪里:哈希查找、调度算法、内核原生
2.1 哈希表匹配和规则链的差距,是本质性的
iptables规则存储本质上是链表结构,每个数据包要沿着链逐个匹配直到命中。内核虽然有一些匹配优化机制,比如规则集版本号、跳转预判之类,但整体上仍避免不了线性匹配的固有成本。
IPVS则在初始化时为每个转发目标建立哈希表项,数据包到达时直接对目标地址做哈希计算并定位到对应条目,不需要逐个扫描。从数学上看,iptables是O(n),IPVS是O(1),在n变大之后两者差异是指数级的。实测数据上,我们在一个400个Service、1500多个后端Pod的集群里做过对比压测,相同QPS下,iptables模式节点CPU的sys开销比IPVS模式高出30%~50%,连接建立的平均延迟也有明显劣化。
2.2 一个内核模块,内置十种调度算法
IPVS本身是内核里的一个模块,做负载均衡已经很多年了。它原生支持很多调度算法,不需要额外部署任何东西:
| 算法 | 全称 | 说明 |
|---|---|---|
| rr | Round Robin | 轮询,默认算法,适合后端能力均等场景 |
| wrr | Weighted Round Robin | 加权轮询,按权重分配连接 |
| lc | Least Connections | 最少连接,分给当前连接数最少的后端 |
| wlc | Weighted Least Connections | 加权最少连接 |
| lblc | Locality-Based Least Connections | 基于局部性的最少连接,目标是同一目的IP的请求尽量转发到同一服务器 |
| lblcr | Locality-Based Least Connections with Replication | 带复制的局部性最少连接 |
| sh | Source Hashing | 源地址哈希,实现会话保持 |
| dh | Destination Hashing | 目的地址哈希 |
| sed | Shortest Expected Delay | 最短期望延迟 |
| nq | Never Queue | 永不排队,有闲置服务器就优先分配 |
K8S里kube-proxy默认会让IPVS使用rr轮询,你可以通过kube-proxy的配置项修改调度算法。比如有些业务希望同一个源IP的请求都打到同一台后端,就可以在kube-proxy里配sh算法,效果上类似会话保持。
2.3 为什么说IPVS的连接转发更"干净"
iptables做负载均衡的实现方式是DNAT:把目标ClusterIP改写为后端Pod IP,再配套SNAT规则保证回包能回来。每个连接都要经过目标地址重写和源地址重写两次操作,路径比较长。
IPVS直接在内核态维护一个转发表,数据包进来后根据目标地址查表,按调度算法选中一台后端,然后做对应的NAT处理。整个转发逻辑集中在IPVS子系统内部,不像iptables那样散落在多条链路上。这对新连接的处理效率提升非常明显,因为新建连接在内核里要做的事情更少、路径更短。
另外IPVS天然是设计来承担大规模负载均衡的,它支持更大规模的连接表,也支持更细粒度的连接跟踪,不会像iptables的规则链那样在规则数量增长之后出现无谓的开销。
3. 切换前的检查清单,这两步没做必踩坑
3.1 内核模块到底装了没有
IPVS功能不是默认加载的,要先用modprobe把相关模块和调度算法模块拉起来。这里有个很容易被忽视的坑:很多发行版内核里虽然带了IPVS支持,但模块没启用,直接切模式会导致kube-proxy启动失败,或者启动了但转发规则不生效,问题还特别隐蔽。
建议在集群的每个节点上都执行一遍下面这套检查:
# 检查是否已有IPVS模块加载 lsmod | grep ip_vs # 结果为空的话,手动加载核心模块 modprobe ip_vs modprobe ip_vs_rr modprobe ip_vs_wrr modprobe ip_vs_sh modprobe nf_conntrack_ipv4加载完成后,确认模块挂载成功:
lsmod | grep ip_vs应该能看见ip_vs、ip_vs_rr、ip_vs_wrr、ip_vs_sh这一组模块。
注意:如果是云上运行的节点,有些云厂商的镜像和自定义操作系统默认会打开一些安全加固选项,可能需要显式允许加载IPVS相关模块。另外如果集群里后续要启用IPVS的加权轮询等算法,记得把对应算法模块也一起加载,不然配置了不支持的算法会报错。
3.2 确认集群当前的kube-proxy模式
切换之前,先确认现在kube-proxy是什么模式。新装的K8S集群默认就是iptables模式,但也不排除有人之前已经切到IPVS后来又切回来的情况。
直接看kube-proxy的ConfigMap:
kubectl -n kube-system get cm kube-proxy -o yaml重点看config.yaml里的mode字段。没有mode字段或者mode为空,就表示使用默认的iptables模式。想快速确认一个节点上实际生效的模式,可以看kube-proxy日志,启动时它会打一行日志告诉你当前使用什么模式。
3.3 检查内核版本和官方支持情况
IPVS模式在K8S里已经GA很久了,正常情况下内核版本不是大问题。但如果你用的是比较老的内核(比如3.x时代的存量集群),就需要确认内核的IPVS模块能力和K8S要求的匹配度。另外iptables模式切换为IPVS后,Service的ClusterIP转发会依赖netfilter的conntrack,所以确认nf_conntrack相关模块也加载了就很重要。
把上面三步都确认完后,再开始动配置。千万不要跳过模块检查,这是我在实际项目里反复强调的,但是永远有人漏掉。
4. 手把手把kube-proxy切换到IPVS模式
4.1 全流程速览:五步切换法
切换过程本身不复杂,核心操作是这几步:
- 在所有节点加载IPVS相关内核模块;
- 修改kube-proxy的ConfigMap,把mode改成ipvs;
- 滚动重启kube-proxy的Pod;
- 观察每个节点上的IPVS规则是否生成;
- 用业务流量做连通性验证。
4.2 修改ConfigMap的实操细节
先编辑kube-proxy的配置:
kubectl -n kube-system edit cm kube-proxy在data.config.yaml里找到mode字段,如果没有就手动加一行:
apiVersion: kubeproxy.config.k8s.io/v1alpha1 kind: KubeProxyConfiguration mode: "ipvs" ipvs: scheduler: "rr"保存退出后,kube-proxy的DaemonSet不会自动更新ConfigMap对应的Pod,需要手动触发滚动重启:
kubectl -n kube-system rollout restart daemonset/kube-proxy等滚动完成:
kubectl -n kube-system rollout status daemonset/kube-proxy这里解释一下为什么需要滚动重启。kube-proxy的配置是启动时读取ConfigMap的,运行中不会自动热加载,所以改了配置一定要把Pod全量重建一遍。滚动策略是DaemonSet默认的,会逐个节点重建,不用太担心整个集群同时断网。
4.3 为什么说有条件的话先在测试集群演练一遍
我一直建议团队在灰度集群或测试环境里先完整演练一遍切换流程,原因很简单:IPVS模式下,Service的转发行为在某些边界场景和iptables模式存在细微差别,这些差别在业务流量跑起来之后才会暴露。
比如有些旧版本在IPVS模式下对externalTrafficPolicy为Local的NodePort类型Service处理得不够好,会导致源IP保留行为不一致。又比如某些网络插件和IPVS模式存在兼容性问题,需要额外配置才能正常工作。这些在测试集群里验证过,到了生产集群才不会手忙脚乱。
4.4 切换后第一时间确认的几件事
第一,确认kube-proxy日志无异常
kubectl -n kube-system logs -l k8s-app=kube-proxy --tail=50能看到类似这样的日志就说明模式切换生效了:
Using ipvs Proxier.第二,看节点上的IPVS规则是否生成
ipvsadm -Ln正常情况下应该能看到每条Service对应一条或几条IPVS转发记录,比如:
IP Virtual Server version 1.2.1 Prot LocalAddress:Port Scheduler Flags -> RemoteAddress:Port Forward Weight ActiveConn InActConn TCP 10.96.0.1:443 rr -> 192.168.1.10:6443 Masq 1 0 0 TCP 10.96.0.10:53 rr -> 192.168.1.21:53 Masq 1 0 0 -> 192.168.1.22:53 Masq 1 0 0第三,看看iptables规则是否大幅减少
iptables-save | grep KUBE-SVC | wc -l切到IPVS模式后,KUBE-SVC链的规则数量会显著下降,虽然不会完全清空,但规模比之前小了一个数量级。
5. 切换后必踩的坑:五条高频问题与排查方法
5.1 问题一:kube-proxy反复崩溃,日志里报IPVS错误
这是我见过最多的故障。签发条件很简单:节点上没加载IPVS内核模块,kube-proxy却已经以IPVS模式启动了。
报错日志通常是:
Error: IPVS proxier can't be used because the following required modules are not loaded: ip_vs, ip_vs_rr排查和解决也比较直接:
# 查看kube-proxy日志确认错误 kubectl -n kube-system logs <kube-proxy-pod-name> | grep -i ipvs # 在所有节点加载模块 modprobe ip_vs modprobe ip_vs_rr modprobe ip_vs_wrr modprobe ip_vs_sh这里有个细节:如果节点是云主机,重启之后模块可能不会自动加载。所以不要只在当前会话加载完就完了,最好把模块加载命令写入系统启动配置,比如在/etc/modules-load.d/目录下创建一个ipvs.conf文件,把要加载的模块名逐行写进去:
ip_vs ip_vs_rr ip_vs_wrr ip_vs_sh这样节点重启后模块会自动加载,不用每次手工救火。
5.2 问题二:某些Service只有ClusterIP,没有IPVS规则
切换IPVS之后,你用ipvsadm -Ln看规则,发现有的Service没出现在IPVS规则表里。这种情况需要检查这个Service的Type和字段配置。
IPVS模式下,kube-proxy只处理特定类型的Service:
- ClusterIP类型:IPVS会为clusterIP+port生成规则;
- NodePort类型:会额外为nodePort生成规则;
- LoadBalancer类型:会为externalIP和LB地址生成规则;
- Headless Service:没有ClusterIP,IPVS不会为其生成转发规则,因为这种Service本身就是直接返回Endpoint IP的。
所以如果你看到某个Service没生成IPVS规则,先看看它是不是Headless,再检查它是否有ClusterIP字段。排除掉这些之后还没规则,就去看kube-proxy日志里有没有同步错误。
5.3 问题三:会话保持失效,同一客户端的请求打到不同后端
iptables模式下,同样的源和目标组合在conntrack里是保持同一条连接的,新连接也能通过conntrack的reuse逻辑保持到同一个后端。但在IPVS模式下,连接的新建行为取决于调度算法。默认的rr是轮询,每个新连接都可能被分到不同后端。
如果业务需要同一来源IP的请求尽量落在同一台后端,要做两件事: 一是把kube-proxy的IPVS调度算法改成sh或lblc,sh是源地址哈希,lblc也是尽量保持同一个源IP到同一台后端。 二是确认Service的sessionAffinity配置,如果Service设置了sessionAffinity为ClientIP,kube-proxy本身也会做一层亲和性逻辑。
调度的改法还是编辑kube-proxy的ConfigMap:
ipvs: scheduler: "sh"改完照样滚动重启。注意一点:sh算法对后端数量变化比较敏感,如果Pod经常扩缩容,哈希映射会变化,可能需要业务容忍一定比例的连接重新分布。
5.4 问题四:NodePort访问正常,但ClusterIP访问超时
这种问题在切换IPVS后偶发出现,整体症状是集群内部通过ClusterIP访问某些服务不稳定,时好时坏,NodePort却正常。
排查思路分几步走:
先确认IPVS规则是否存在:
ipvsadm -Ln | grep <clusterIP>然后确认conntrack表是否异常:
conntrack -L | grep <clusterIP>如果conntrack出现大量TIME_WAIT或异常状态,有可能是连接跟踪表溢出或冲突。IPVS模式对conntrack的依赖比iptables模式更重,因为需要维护转发状态的关联关系。临时调整conntrack大小可以缓解:
sysctl net.netfilter.nf_conntrack_max=262144 sysctl net.netfilter.nf_conntrack_buckets=65536当然这只解决眼前问题,根子上还是要优化连接复用,比如客户端侧开启连接池、HTTP keep-alive,减少短连接数量。
5.5 问题五:切换后网络插件不兼容,Pod之间通信受限
部分CNI插件在IPVS模式下需要额外配置。特别是那些依赖iptables规则来放行或操作流量的插件,可能会和IPVS的转发逻辑产生冲突。
遇到这类问题,先确认CNI插件版本是否支持IPVS模式。很多主流CNI插件在较新版本里都对IPVS做了兼容适配。如果不兼容,要么升级CNI插件版本,要么在切换IPVS模式时把CNI相关的iptables规则保留白名单。
建议切换前查一下自己用的CNI插件的官方兼容性说明,别等切完了才发现不兼容,来回折腾很痛苦。
6. 切换之后的性能验证和持续监控
6.1 怎么证明切换真的有效
切换完了不能只凭感觉说"好像变快了"。要做对比验证,建议至少准备两次压测:一次切之前的iptables基线数据,一次切之后的IPVS数据。
压测关注几个指标:
- 节点CPU的sys占比;
- 连接建立速率;
- P99/P999延迟;
- 每秒新建连接数。
以我们当时的压测数据为例,同一套业务应用、同样的压测工具和参数,iptables模式下,单节点在4000 QPS左右时sys CPU已经到40%附近;切到IPVS后,相同QPS下sys CPU掉到25%左右,P99延迟从88毫秒降到60毫秒上下,新建连接速率的上限也明显提升。
压测期间建议用监控工具实时盯节点指标,不要等压测完了再去翻数据,不然出了问题不好定位是压测工具的问题还是集群的瓶颈。
6.2 日常监控IPVS状态的三个手段
第一,看IPVS的统计信息
ipvsadm -Ln --stats可以看到每个Service的活跃连接数、非活跃连接数、入包出包数、出入字节数。这是排查流量分布是否均匀的第一手数据。
第二,接入Prometheus监控
kube-proxy暴露/metrics接口,里面有ipvs相关的指标,比如:
- kubeproxy_sync_proxy_rules_duration_seconds;
- kubeproxy_network_programming_duration_seconds。
通过这些指标可以观察规则同步耗时,判断是否存在同步瓶颈。
第三,观察节点conntrack状态
IPVS模式下conntrack仍然很关键。持续监控net.netfilter.nf_conntrack_count和nf_conntrack_max的比率,如果长期超过70%,就该考虑扩大容量或者优化连接复用。
| 监控项 | 命令/指标 | 预警阈值 |
|---|---|---|
| conntrack使用率 | cat /proc/sys/net/netfilter/nf_conntrack_countcat /proc/sys/net/netfilter/nf_conntrack_max | 超过70% |
| IPVS连接数 | ipvsadm -Ln --stats 的ActiveConn | 观察变化趋势 |
| kube-proxy同步耗时 | kubeproxy_sync_proxy_rules_duration_seconds | 持续升高需关注 |
| 节点sys CPU | node_cpu_seconds_total{mode="system"} | 同比上升超过20% |
6.3 切完IPVS之后的大规模集群,还能往哪一步优化
IPVS解决的是kube-proxy转发路径上的性能问题,但K8S的网络性能优化是个持续迭代的过程。集群规模继续增长之后,几个方向值得探索:
一是把部分关键业务的Service改用更底层的网络加速方案,比如通过负载均衡器直挂Pod,跳过kube-proxy这一层转发。
二是关注eBPF技术在数据面上的应用,目前已经有一些基于eBPF的CNI插件可以实现更细粒度的流量观测和转发控制,同时在转发性能上做了不小的优化。这个方向未来可能会和IPVS模式形成竞争关系,但目前主流生产集群里IPVS依然是性价比很高的选择。
三是加强集群网络的可观测性建设,比如在Service层面建立黄金指标监控,连接状态下钻,这样才能在流量异常时快速定位是转发层的问题还是业务自身的问题。
7. 切换IPVS的后续扩展和一些想法
7.1 调度算法选择:一包方案不是万能的
按我们使用经验,如果业务对会话保持没有硬性要求,默认的rr算法就行;如果业务要求同一客户端多次请求尽量打到同一台后端,sh算法是最常见的判断;如果后端处理能力差异很大,wrr或者wlc更合适。
但调度算法也不是一劳永逸的。Pod频繁缩扩容时要重新评估算法是否还适合。我们曾有一个状态类服务,一开始用rr很均衡,后来后端Pods调整了副本数量,部分节点负载明显不均匀,换到wlc之后才好很多。所以切完IPVS不代表就完事大吉,持续观察调度效果还是必要的。
7.2 保存现场:切换后的审计与回滚预案
任何生产变更都要有回滚方案。IPVS切换的回滚很简单:把ConfigMap里的mode改回iptables,重启kube-proxy就行。但如果已经跑了很久,回滚后iptables规则要重新全量生成,期间可能有一次短暂的连接中断,业务要能接受这个窗口。
建议切换前把kube-proxy的当前配置和iptables规则备份一下:
kubectl -n kube-system get cm kube-proxy -o yaml > kube-proxy-cm-backup.yaml iptables-save > iptables-backup-$(date +%Y%m%d%H%M%S).rules真出问题想回滚时,至少有一份恢复现场的数据,不用凭记忆重建规则。
7.3 结尾补一个实操心得
我实际切换的时候遇到过一个小问题,值得单独拿出来说。滚动重启kube-proxy DaemonSet之后,部分节点的IPVS规则迟迟没生成,查了半天发现是那几台节点的时钟有偏差,导致kube-proxy从API Server同步Service信息时出了故障。把节点时间同步修好后规则立即正常生成。所以切换IPVS之前最好全节点做一个时间同步检查,NTP服务别出幺蛾子,不然排查起来真的很费时间。
IPVS替代iptables这件事,单看切换动作确实不大,但涉及的细节很多。内核模块、配置项、调度算法、CNI兼容性、conntrack状态,哪一环没考虑到,切换后都有可能冒问题。希望这篇实操梳理能帮你少走一些弯路,真正把集群转发性能提上去之后,再回头看那些监控曲线,你会觉得这几小时的切换工作是值得的。