kube-proxy 从 iptables 模式切到 IPVS 模式,这件事在集群规模小的时候几乎感觉不到差别,但只要节点上 Service 数量过了几百、Pod 副本数上千,iptables 那条链的线性匹配开销就会开始咬人。我最早动这个念头,是因为一个中等规模的业务集群里 Service 已经逼近两千,节点上iptables-save的输出动辄十几万行,规则刷新时 CPU 尖刺明显,新建连接的延迟抖动也跟着上来了。IPVS 基于哈希表做转发决策,时间复杂度是常数级,天然适合这种场景。这篇内容就是把我自己从选型、改造、验证到踩坑的完整过程摊开讲清楚,适合已经对 Kubernetes 网络模型有基本概念、正在被大规模 Service 转发性能困扰、或者单纯想把集群网络底座升级一档的运维和平台开发同学参考。哪怕你只是刚接触 kube-proxy 的调度模式,文中的原理拆解和操作步骤也能让你照着走一遍。
1. 为什么要把调度方式换成 IPVS
1.1 iptables 模式在大规模集群下的真实瓶颈
要理解这次改造的价值,得先把 iptables 模式的工作机制说透。kube-proxy 在 iptables 模式下,会为每一个 Service 生成一组规则,ClusterIP 的访问靠KUBE-SERVICES链做匹配,再跳到对应的KUBE-SVC-xxx链,最后分发到KUBE-SEP-xxx端点链。问题在于,iptables 的规则匹配本质上是顺序遍历,一条链上的规则越多,一个数据包要走到目标规则需要经过的比对次数就越多。
打个生活化的比方:iptables 像一本没有目录的纸质电话簿,你要找一个联系人,只能从第一页开始一页页翻。Service 有一千个,平均每个包要翻五百页才能找到对应规则。而 IPVS 更像一个哈希索引的通讯录,你报出名字,它直接算出该翻到哪一页,一步到位。这个差异在小集群里被缓存和规则命中率掩盖了,但规模一上来,差距是指数级拉开的。
具体到实测数据,我在一个 3 节点、每节点约 1500 个 Service 的环境里做过对比:iptables 模式下iptables-restore单次全量同步耗时接近 4 秒,期间 kube-proxy 的 CPU 占用能冲到单核 80% 以上;切到 IPVS 后,同样的 Service 规模,规则同步耗时降到 300 毫秒以内,CPU 峰值不到 20%。这个量级的差异,直接决定了集群在频繁扩缩容时网络平面的稳定性。
1.2 IPVS 的哈希转发原理与适用边界
IPVS(IP Virtual Server)是内核里的一套四层负载均衡实现,它把 Service 的 ClusterIP 和端口注册成一个虚拟服务(virtual service),把后端 Pod 注册成真实服务器(real server),转发时通过哈希表直接定位。kube-proxy 在 IPVS 模式下不再写海量 iptables 规则,而是通过 netlink 接口调用 IPVS 的内核模块来维护转发表。
这里有个关键点很多人会忽略:IPVS 模式并不是完全抛弃 iptables。kube-proxy 仍然会用少量 iptables 规则处理 SNAT、masquerade、NodePort 的某些边界情况,以及 IPVS 不支持的场景(比如某些特殊协议的端口映射)。所以切过去之后你依然能看到 iptables 规则,只是数量从十几万行骤降到几百行,这才是性能提升的来源。
IPVS 的适用边界也要说清楚。它擅长的是四层转发,对七层特性无感;它依赖内核模块ip_vs、ip_vs_rr、ip_vs_wrr、ip_vs_sh、nf_conntrack等,内核版本太低或者裁剪过的内核可能缺模块。另外 IPVS 的调度算法(rr、wrr、lc、sh 等)决定了后端分发的均衡策略,kube-proxy 默认用 rr(轮询),这个默认值在多数场景够用,但如果你有长连接业务,可能需要考虑会话保持相关的配置。
1.3 改造前必须确认的三个前提
动手之前,有三件事必须先确认,否则改到一半会卡住。
第一是内核模块可用性。在目标节点上执行lsmod | grep ip_vs看模块是否已加载,没有的话modprobe ip_vs试加载,再确认ip_vs_rr、ip_vs_wrr、ip_vs_sh、nf_conntrack这几个依赖模块都在。有些精简版系统镜像会裁掉这些模块,那就得先换内核或装对应模块包,这一步不通过后面全是空谈。
第二是kube-proxy 的部署形态。是以 DaemonSet 跑在集群里,还是以系统服务跑在宿主机上?前者改配置相对简单,改 ConfigMap 重启 Pod 即可;后者要改节点上的配置文件并重启服务,回滚成本更高,操作要更谨慎。两种形态的配置路径和生效方式完全不同,搞混了会出现"改了没生效"的迷惑现象。
第三是集群里是否有依赖 iptables 规则做特殊拦截的组件。有些网络插件或者安全组件会往 iptables 里插自定义链,IPVS 模式下这些链的挂载点可能变化,需要提前梳理。我遇到过某监控组件依赖KUBE-SERVICES链做流量统计,切 IPVS 后统计归零,后来改成从 IPVS 的统计接口取数才恢复。
2. 改造方案设计与关键参数取舍
2.1 整体切换思路:先灰度后全量
我的做法不是一把梭全集群切换,而是先单节点灰度、再分批推进。原因很直接:kube-proxy 的模式切换会影响该节点上所有 Pod 的 Service 访问,一旦出问题,影响面是整个节点而不是单个服务。灰度能把这个爆炸半径控制住。
具体节奏是:挑一个非核心业务的节点,把它上面的 kube-proxy 切成 IPVS,观察至少 24 小时,重点看 Service 访问成功率、新建连接延迟、kube-proxy 自身资源占用、以及 IPVS 转发表的条目数是否与预期 Service 数吻合。确认稳定后,再按批次(比如每次 20% 节点)推进,每批之间留出观察窗口。这个节奏看起来慢,但比全量切换后回滚要快得多——回滚一次全集群的代价,远比多等几天大。
2.2 调度算法与转发模式的参数选择
kube-proxy 的 IPVS 配置里有几个参数值得掰开说。ipvs.scheduler决定调度算法,默认rr(轮询)。如果你的后端 Pod 性能不均,wrr(加权轮询)配合权重更合适;如果是需要会话保持的长连接场景,sh(源地址哈希)能让同一源 IP 的请求固定落到同一后端。但要注意,sh在 NAT 环境下可能因为源地址被改写而失效,用之前得确认网络路径。
ipvs.strictARP这个参数控制是否开启严格 ARP 模式,默认false。当 ClusterIP 和后端 Pod 在同一网段、且存在 ARP 冲突风险时,建议设为true,避免节点响应不属于自己的 ARP 请求导致流量错乱。这个坑我在早期项目里踩过,表现为偶发的 Service 访问超时,抓包才定位到是 ARP 应答抢答。
ipvs.excludeCIDRs用来排除某些 CIDR 不走 IPVS 转发,典型场景是节点本地服务或者需要直连的网段。这个参数不设的话,某些本地回环访问可能被错误地卷进 IPVS 转发,出现访问不通的怪现象。
| 参数 | 默认值 | 推荐场景 | 注意事项 |
|---|---|---|---|
| ipvs.scheduler | rr | 通用场景 | 长连接考虑 sh,性能不均考虑 wrr |
| ipvs.strictARP | false | 同网段部署建议 true | 避免 ARP 抢答导致流量错乱 |
| ipvs.excludeCIDRs | 空 | 有本地直连需求时配置 | 排除后该网段不走 IPVS |
| ipvs.tcpTimeout | 0(沿用内核默认) | 长连接业务可调大 | 过大会占用 conntrack 表 |
| ipvs.udpTimeout | 0(沿用内核默认) | UDP 业务按需调整 | UDP 无连接状态,超时影响回收 |
2.3 回滚预案的设计
任何改造都要想好怎么退。IPVS 切回 iptables 的操作本身不复杂——把模式改回去、重启 kube-proxy 即可,但回滚的时机和触发条件要提前定好。我给自己定的红线是:灰度节点上 Service 访问错误率超过 0.5% 持续 5 分钟,或者出现大面积连接超时,立即回滚该节点。
回滚前要保留现场:ipvsadm -Ln的输出、iptables-save的快照、kube-proxy 的日志,这些是事后定位问题的关键。我习惯在切换前先跑一遍ipvsadm -Ln > /tmp/ipvs-before.txt和iptables-save > /tmp/iptables-before.txt,回滚后对比差异,能快速判断是配置问题还是内核问题。
3. 实操过程:从配置修改到验证落地
3.1 环境预检与内核模块加载
正式动手前,先在目标节点上做一轮预检。这一步不能省,我见过太多人直接改配置重启,结果 kube-proxy 起不来,日志里报can't find ip_vs module,回头再补模块,中间服务已经抖了一轮。
预检命令按顺序执行:
# 检查内核版本,建议 4.19 以上 uname -r # 检查 IPVS 相关模块是否已加载 lsmod | grep -E "ip_vs|nf_conntrack" # 若未加载,手动加载并验证 modprobe ip_vs modprobe ip_vs_rr modprobe ip_vs_wrr modprobe ip_vs_sh modprobe nf_conntrack # 再次确认模块状态 lsmod | grep ip_vs # 检查 ipvsadm 工具是否可用,用于后续验证 which ipvsadm || echo "ipvsadm not installed"如果modprobe报模块找不到,说明内核没编进这些模块,需要装kernel-modules-extra之类的包或者换内核。这一步过了,再确认ipvsadm工具在,后面验证全靠它。
注意:模块加载是临时的,重启后会丢失。要持久化,得把模块名写进
/etc/modules-load.d/ipvs.conf,每行一个模块名。别只modprobe完就以为万事大吉,节点一重启又回到解放前。
3.2 修改 kube-proxy 配置的两种路径
配置修改分两种部署形态,路径完全不同,先确认你的集群是哪种。
DaemonSet 形态:kube-proxy 以 Pod 形式跑在kube-system命名空间。配置在 ConfigMapkube-proxy里,改mode字段为ipvs,并按需补上ipvs段。改完 ConfigMap 后,需要重启 kube-proxy 的 Pod 让配置生效——注意是重启 Pod,不是滚动更新 Deployment,因为 kube-proxy 是 DaemonSet,直接kubectl rollout restart daemonset/kube-proxy -n kube-system即可。
apiVersion: v1 kind: ConfigMap metadata: name: kube-proxy namespace: kube-system data: config.conf: |- apiVersion: kubeproxy.config.k8s.io/v1alpha1 kind: KubeProxyConfiguration mode: "ipvs" ipvs: scheduler: "rr" strictARP: true excludeCIDRs: [] # 其余字段保持原样系统服务形态:kube-proxy 以 systemd 服务跑在宿主机,配置文件通常在/var/lib/kube-proxy/config.conf或/etc/kubernetes/kube-proxy-config.yaml,具体路径看你的部署方式。改完mode: "ipvs"后,systemctl restart kube-proxy重启服务。这种形态下没有 ConfigMap 兜底,改之前务必先备份原文件,cp config.conf config.conf.bak,回滚时直接覆盖回去。
两种形态改完后,都要确认 kube-proxy 日志里出现类似Using ipvs Proxier的字样,这是模式生效的直接证据。如果日志还是Using iptables Proxier,说明配置没被读到,检查路径和字段名是否写对。
3.3 验证 IPVS 转发表与 Service 连通性
配置生效后,验证分三层:转发表、连通性、性能。
第一层看转发表。ipvsadm -Ln会列出所有 virtual service 和对应的 real server。正常情况下,集群里有多少个 Service(含 ClusterIP 类型),这里就该有多少条 virtual service 记录,每条下面挂着对应的后端 Pod 地址和端口。如果条目数明显偏少,可能是 kube-proxy 还没同步完,等几十秒再看;如果一直偏少,查 kube-proxy 日志有没有同步报错。
# 查看 IPVS 转发表 ipvsadm -Ln # 统计 virtual service 数量,与集群 Service 数对比 ipvsadm -Ln | grep -c "TCP\|UDP" # 查看某个具体 Service 的后端分布 ipvsadm -Ln | grep -A 5 "10.96.0.10"第二层看连通性。在节点上直接访问一个 ClusterIP 的 Service,确认能通;再从 Pod 内部访问,确认跨节点也通。我习惯用curl打一个已知的 HTTP 服务,配合-w输出耗时,顺便看延迟是否正常。如果访问不通,先ipvsadm -Ln看该 Service 有没有对应条目,再看后端 Pod 是否 Ready,逐层排除。
第三层看性能。对比切换前后的新建连接延迟和 kube-proxy 资源占用。我用的方法是ab或wrk对某个 Service 压一轮,记录 P99 延迟;同时top看 kube-proxy 进程的 CPU。正常情况下 IPVS 模式下 kube-proxy 的 CPU 应该明显低于 iptables 模式,延迟抖动也更小。
3.4 灰度推进与全量切换的节奏控制
单节点验证通过后,进入灰度推进。我的做法是按节点分批,每批之间留观察窗口。具体操作上,如果是 DaemonSet 形态,可以通过给节点打标签配合 nodeSelector 控制哪些节点先跑新配置,或者直接分批重启不同节点上的 kube-proxy Pod(用kubectl delete pod指定节点上的那个 Pod,让它重建时读到新 ConfigMap)。
每批推进后,重点盯三个指标:Service 访问错误率、节点上ipvsadm -Ln的条目数是否与预期一致、kube-proxy 日志有无异常。这三个指标稳定 24 小时,再推下一批。全量切换完成后,别急着收工,再观察一周,因为有些问题(比如长连接超时、conntrack 表满)需要时间才暴露。
提示:灰度期间如果发现某节点异常,回滚只需把该节点的 kube-proxy 配置改回
iptables并重启,不影响其他已切换节点。这种"按节点独立回滚"的能力,是灰度策略最大的价值,别为了省事跳过灰度。
4. 常见问题与排查技巧实录
4.1 切换后 Service 访问不通的排查路径
这是最高频的问题,排查要按层走,别一上来就瞎改。我的排查顺序是:先看 IPVS 表有没有该 Service 的条目,再看后端 real server 是否健康,最后看网络路径和 conntrack。
如果ipvsadm -Ln里压根没有目标 Service 的条目,问题在 kube-proxy 同步层——查 kube-proxy 日志有没有Failed to sync之类的报错,常见原因是 Service 的某些字段 IPVS 不支持(比如某些特殊协议的端口名),或者 kube-proxy 权限不足调不了 netlink。
如果条目在但后端为空,说明 Endpoints 没同步过来,查 Service 的 selector 是否匹配到 Pod、Pod 是否 Ready。如果条目和后端都在但访问不通,就要看 conntrack 和网络路径了,conntrack -L | grep <ClusterIP>看有没有异常状态的连接,tcpdump抓包看包到底走到了哪。
| 现象 | 可能原因 | 排查命令 | 处理方式 |
|---|---|---|---|
| IPVS 表无该 Service | kube-proxy 同步失败 | 查 kube-proxy 日志 | 修复配置或权限后重启 |
| 有条目但后端为空 | Endpoints 未同步 | kubectl get endpoints | 检查 selector 和 Pod 状态 |
| 条目后端正常但访问超时 | conntrack 或网络路径问题 | conntrack -L、tcpdump | 清理 conntrack 或查路由 |
| 偶发访问失败 | ARP 冲突或 strictARP 未开 | arping、查 strictARP 配置 | 开启 strictARP |
| 长连接频繁断开 | tcpTimeout 过短 | 查 ipvs.tcpTimeout | 调大超时值 |
4.2 conntrack 表满导致的连接丢弃
IPVS 依赖 conntrack 做连接跟踪,表满时会直接丢新连接,表现为"偶发访问失败"或"新建连接超时"。这个问题在 iptables 模式下也存在,但 IPVS 模式下因为转发路径变化,暴露得更明显。
排查方法是conntrack -C看当前条目数,sysctl net.netfilter.nf_conntrack_max看上限。如果当前数逼近上限,就是表满了。处理分两步:短期调大nf_conntrack_max,长期优化nf_conntrack_tcp_timeout_established等超时参数,让空闲连接更快回收。
# 查看当前 conntrack 条目数和上限 conntrack -C sysctl net.netfilter.nf_conntrack_max # 临时调大上限(重启失效) sysctl -w net.netfilter.nf_conntrack_max=1048576 # 持久化写入 echo "net.netfilter.nf_conntrack_max=1048576" >> /etc/sysctl.conf注意:调大 conntrack 上限会占用更多内存,按节点内存量力而行。一般每 1 万条目约占用几 MB,百万级条目要预留几百 MB 内存,别在小内存节点上盲目调大。
4.3 内核模块缺失与版本不匹配
模块缺失的表现是 kube-proxy 起不来,日志报can't find ip_vs或failed to load ip_vs。前面预检章节已经讲了怎么加载,这里补充一个容易忽略的点:模块版本要和内核版本匹配。有些节点升级过内核但没重启,uname -r显示的是新内核,实际跑的还是旧内核,modprobe加载的模块和运行内核不匹配,会出现加载成功但功能异常的情况。
确认方法是对比uname -r和uname -a的输出,以及/lib/modules/$(uname -r)目录是否存在。如果目录不存在,说明当前运行内核没有对应模块,需要重启到新内核或者装匹配的模块包。这个坑比较隐蔽,因为modprobe可能不报错,但 IPVS 功能就是不对。
4.4 从 IPVS 回退 iptables 的注意事项
回退操作本身简单,但有几个细节要注意。第一,回退前先ipvsadm -C清空 IPVS 表,避免残留条目干扰;第二,回退后 iptables 规则会重新全量生成,大规模集群下这个同步过程可能耗时几秒到几十秒,期间 Service 访问会有短暂抖动,要选低峰期操作;第三,回退后确认iptables-save的规则数恢复到切换前的量级,如果明显偏少,说明 kube-proxy 还没同步完,再等等。
回退后别急着删 IPVS 模块,留着不影响 iptables 模式运行,万一还要再切回去省得重新加载。真正要清理的是那些为 IPVS 临时加的 sysctl 参数,如果确认不再用 IPVS,再考虑回滚这些参数。
5. 我踩过的坑与实操心得
5.1 strictARP 没开导致的偶发超时
这个坑值得单独说。早期一次切换后,Service 访问出现偶发超时,概率大概千分之几,抓包发现是节点对不属于自己的 ClusterIP 做了 ARP 应答,导致流量被引到错误节点。根因是strictARP没开,节点在 ARP 层面"抢答"了。
开启strictARP: true后问题消失。这个参数的作用是让节点只对属于自己的 IP 做 ARP 应答,避免同网段下的 ARP 冲突。如果你的集群节点和 Pod 在同一网段,或者用了某些会广播 ARP 的网络方案,强烈建议开启。代价是几乎没有,收益是消除一类隐蔽的偶发故障。
5.2 调度算法选错导致的长连接抖动
默认的rr轮询在短连接场景很均衡,但长连接场景下,如果后端 Pod 数量变化(扩缩容),轮询会把新连接分到新 Pod,而老连接还挂在老 Pod 上,导致负载不均。我遇到过一次扩容后新 Pod 几乎没流量、老 Pod 被打满的情况,就是rr在长连接下的典型表现。
后来对长连接业务单独用sh(源地址哈希),同一源 IP 固定落到同一后端,扩缩容时影响面可控。但sh也有代价——后端 Pod 数量变化时,哈希映射会重排,部分连接会重新分布,所以扩缩容要配合优雅下线,让老连接自然结束。
5.3 监控指标要跟着换
切 IPVS 后,原来基于 iptables 规则数做的监控会失效,因为规则数骤降不代表异常。要补上 IPVS 维度的监控:ipvsadm -Ln --stats能拿到每个 virtual service 的连接数、包量、字节量,这些才是判断转发是否正常的依据。我习惯把ipvsadm -Ln --stats的输出定期采集,对比 Service 数量和连接分布,异常时能快速定位是哪个 Service 出了问题。
另外 kube-proxy 自身的 metrics 里,IPVS 模式下的kubeproxy_sync_proxy_rules_duration_seconds和kubeproxy_sync_proxy_rules_last_timestamp_seconds依然有效,可以用来判断同步是否及时。这两个指标在切换后要重点盯,同步延迟变大往往意味着 kube-proxy 压力上来了。
5.4 节点重启后的模块持久化
前面提过模块加载是临时的,这里再强调一次。节点重启后如果模块没持久化,kube-proxy 起不来,整个节点的 Service 访问全挂。持久化方法是在/etc/modules-load.d/下建配置文件,把ip_vs、ip_vs_rr、ip_vs_wrr、ip_vs_sh、nf_conntrack都写进去,每行一个。这个文件在系统启动早期被读取,确保模块在 kube-proxy 启动前就加载好。
我现在的习惯是,任何涉及内核模块的改造,第一件事就是把持久化配置写好,再谈其他。因为节点重启是迟早的事,临时加载的模块撑不过一次重启,这个坑踩一次就够记一辈子。
6. 改造后的效果与后续可扩展方向
全量切换并稳定运行一个月后,我复盘了几个关键数据:kube-proxy 的 CPU 占用从平均单核 40% 降到 8% 左右,规则同步耗时从秒级降到百毫秒级,Service 访问的 P99 延迟抖动明显收敛。这些收益在 Service 规模继续增长时会进一步放大,因为 IPVS 的转发开销不随 Service 数量线性增长。
后续可扩展的方向,一个是把 IPVS 的调度算法按业务类型做精细化配置,比如在线业务用rr、长连接用sh、批处理用wrr,通过 kube-proxy 的配置分段或者按命名空间做差异化;另一个是结合 IPVS 的统计接口做更细粒度的流量观测,把每个 Service 的连接数、包量纳入监控大盘,替代原来基于 iptables 规则的观测方式。这两件事都不难,难的是先把模式切换这步走稳,后面的优化才有基础。
我个人在实际操作中的体会是,kube-proxy 换 IPVS 这件事,技术难度不高,难在节奏控制和细节确认。预检做扎实、灰度不跳过、回滚预案想清楚,这三件事做到位,切换过程基本不会出大问题。真正容易翻车的地方,往往是那些看起来不起眼的参数(比如 strictARP)和持久化配置(比如模块加载),它们平时不显山露水,出问题时却能让整个节点网络瘫痪。把这些细节提前想到、做到,比事后救火省心得多。