K8S集群随着规模增长,最让人头疼的不是业务上线,反而是网络链路时不时冒出的小毛病。我接手过一个几十台节点的K8S集群,Service数量涨到两三百后,业务方开始反馈部分接口偶发超时,CPU用不满但网络就是慢。排查来排查去,问题锁定在kube-proxy的iptables模式上。规则链几百条,每次新连接进来都要一条条匹配,数据包转发路径越长,延迟越明显。那段时间我正好被这个问题折腾得够呛,研究了一圈后决定把网络转发模式从iptables切换到IPVS。今天这篇文章就围绕这次优化的完整过程展开,从原理拆解到实际切换,再到踩坑记录,一步步讲清楚为什么IPVS更适合大规模K8S集群,以及切换的时候要注意哪些细节。
1. 为什么iptables模式会成为集群网络的瓶颈
1.1 从一条Service转发链路看iptables的线性匹配逻辑
先说点基础的,K8S的Service实现本质是负载均衡和转发规则的生成。kube-proxy默认使用iptables模式时,它会为每一个Service和对应的Endpoints生成一堆iptables规则,典型的就是KUBE-SVC链、KUBE-SEP链,每条规则动态地插入或追加到NAT表中。
数据包访问一个Service ClusterIP时,会依次经过这些链。规则是顺序匹配的,从第一条开始逐条比对,直到命中。Service数量少的时候,几十条规则没问题,匹配一次可能也就几微秒。但集群规模大了以后,Service和Pod数量上来了,规则数量会膨胀得非常快。一个Service带三五个Pod副本,就会生成至少四五条规则,几百个Service就是几千条的规模。
问题在于iptables的匹配是线性的。新连接进来时,数据包要在NAT表的规则链里从头走到尾,最坏情况下要遍历上千条规则才能命中。规则越多,平均检索开销越大,越靠后的Service受到的延迟影响越严重。我在那个集群里做过简单的网络延迟测试,Service数量超过两百后,新建连接的P99延迟从原来的几十微秒涨到了几毫秒,高并发下还会出现SYN重传。
这里有个容易忽略的细节,iptables匹配不仅发生在DNAT阶段,数据包在转发路径上还要经过filter表、mangle表等多个钩子点。虽然K8S的kube-proxy主要操作的是NAT表,但系统自身可能有其他防火墙规则并行存在,多个链叠加后,整体匹配路径就更长了。
1.2 规则更新风暴与连接跟踪表溢出
iptables模式另一个大问题是规则更新。Pod频繁扩缩容、Service变更、节点负载均衡器变化,都会触发kube-proxy重新计算并更新iptables规则。更新过程会整体刷新规则链,这个过程是全量刷新的,不是增量更新。规则量越大,刷新一次耗时越长,而且刷新期间数据包匹配还会出现短暂的断续。
更新到几千条规则后,刷新一次可能需要几秒甚至十几秒。每次Release新版本、滚动更新Deployment时,ReplicaSet的Endpoint变化非常频繁,这种频繁刷新会让网络链路处于一种不稳定状态。有次发布后集群出现大面积连接重置,锅就出在这里。
更隐蔽的是连接跟踪表的溢出。iptables的conntrack模块维护一张连接跟踪表,默认大小通常是系统内存决定的。流量大了、短连接多了,连接跟踪表很容易打满。表满了之后新连接会被直接丢弃,表现就是业务偶发超时、connect失败,但查CPU和内存又看不出什么问题。
我见过一台8C16G的节点,默认conntrack限制大约65536条。在生产环境稍微有点流量波动,几十万条短连接瞬间就把表冲爆了,然后各种转发异常接踵而至。而IPVS模式虽然也会使用conntrack,但相比iptables的规则链遍历,IPVS的转发逻辑要轻量很多,连接跟踪的压力天然小一个量级。
2. IPVS模式的核心原理与对比优势
2.1 IPVS在内核态里到底做了什么
IPVS是Linux内核自带的四层负载均衡模块,工作在netfilter的LOCAL_IN和FORWARD钩子点,原理上是一个内核态的哈希表+调度器。它跟iptables最大的区别在于,IPVS构建了一张基于哈希表的虚拟服务映射表,查找时间复杂度是O(1),不管表里有多少条Service,匹配速度基本恒定。
具体到K8S里的体现,kube-proxy会把Service的ClusterIP和端口注册成IPVS的虚拟服务(Virtual Service),每个Endpoints就是一台真实服务器(Real Server)。数据包到达节点后,IPVS根据目标地址和端口直接做哈希查找,定位到对应的虚拟服务,然后按配置的调度算法选一台后端Pod,做DNAT和负载均衡。
IPVS支持的调度算法比iptables丰富得多,K8S里常用的有rr(轮询)、wrr(加权轮询)、lc(最少连接)、sh(源地址哈希)等。在iptables模式下要实现类似源地址哈希、最少连接这种均衡策略非常困难,容易播一堆复杂的iptables规则。但IPVS只需要改一个调度算法的参数即可,灵活性高了一个档次。
从内核实现角度看,IPVS本质上是netfilter框架下的一个独立模块,不走iptables的规则匹配逻辑。它的查找结构是哈希表,所以规则再多也不会出现线性遍历的性能衰减。这也是为什么在大规模Service场景下,IPVS的转发延迟和吞吐表现远优于iptables。
2.2 三种kube-proxy模式的选型思路
K8S的kube-proxy一共有三种工作模式:userspace、iptables和IPVS。userspace模式是老古董了,性能最差,数据包要来回拷贝到用户态,基本没人用。iptables是默认模式,简单可靠,中小规模集群用着没问题。IPVS则是性能和功能双优的方案,前提是内核支持,节点上得有ip_vs模块。
有人会问,都上了IPVS之后,iptables是不是就完全没参与转发了?其实不是。K8S至今仍然依赖iptables来完成部分功能,比如NodePort的流量入口会先经过iptables的规则链再进入IPVS的处理逻辑。换句话说,IPVS模式不是完全踢开iptables,而是把最核心、最频繁的Service负载均衡工作交给了IPVS,iptables只负责边界流量处理的少量规则。
从实际场景选型来说:
- 集群Service数量在几十以内,节点规模不大,用iptables足够,没必要折腾IPVS。
- Service数量上百,Pod频繁扩缩容,集群内东西向流量大,上IPVS会有立竿见影的改善。
- 集群里有负载均衡算法定制需求,比如同一个客户端IP要固定转发到同一个Pod(会话保持),IPVS的sh算法比iptables好配得多。
我个人的倾向是,新集群直接上IPVS,老集群如果已经出现性能瓶颈,也值得做一次平滑切换。IPVS的切换是热操作,不影响存量连接,只是改变新连接的转发方式。
3. 实操切换:从iptables到IPVS的完整步骤
3.1 检查内核与模块支持
切换之前的第一个动作,不是改配置,而是检查集群节点内核支不支持IPVS以及相关模块有没有加载。现在的Linux发行版,内核基本都带IPVS,但为了稳妥起见,还是要逐节点确认。
# 检查内核是否支持ipvs相关模块 grep -e ipvs -e nf_conntrack /boot/config-$(uname -r) 2>/dev/null || true # 查看当前节点已加载的ipvs模块 lsmod | grep ip_vs如果没有输出,说明模块还没加载。K8S官方推荐加载的内核模块包括ip_vs、ip_vs_rr、ip_vs_wrr、ip_vs_sh、nf_conntrack等。可以用modprobe命令手动加载:
modprobe ip_vs modprobe ip_vs_rr modprobe ip_vs_wrr modprobe ip_vs_sh modprobe nf_conntrack光加载还不行,还得让这些模块开机自动加载。可以写一个模块配置文件,放到/etc/modules-load.d/目录下,比如/etc/modules-load.d/ipvs.conf,内容就是模块名一行一个。
我踩过一个小坑,虚拟机环境里有时候内核模块没装全,modprobe ip_vs_sh会报错找不到模块。这时候不要硬来,查一下内核版本和发行版的kernel-modules-extra包,装上之后一般就能解决。
3.2 修改kube-proxy配置并滚动升级
模块确认没问题后,开始改kube-proxy的配置。这里分两种方式,一种是直接改kube-proxy的ConfigMap,另一种是改kube-proxy启动参数,效果其实是一样的。
先看当前集群kube-proxy是以什么方式部署的,一般是DaemonSet。编辑ConfigMap:
kubectl -n kube-system edit cm kube-proxy在config里找到mode字段,如果没有就加上:
apiVersion: kubeproxy.config.k8s.io/v1alpha1 kind: KubeProxyConfiguration mode: "ipvs"然后保存。接着滚动重启kube-proxy的Pod。由于是DaemonSet,最好分批更新,或者一次性全部重启也行,因为kube-proxy是纯转发平面的组件,重启对存量连接没有影响。生产环境我建议分批来,先重启一个节点上的Pod,观察无误后再全量重启。
kubectl -n kube-system rollout restart ds kube-proxy kubectl -n kube-system rollout status ds kube-proxy重启完成后,重要的一步是验证IPVS规则是否真的生成。在节点上执行:
ipvsadm -ln如果能看到如下类似输出,说明IPVS规则的转发已经接管了Service:
IP Virtual Server version 1.2.1 (size=4096) 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.11:53 Masq 1 0 0正常情况每一个ClusterIP Service都会出现在ipvsadm的输出里。
3.3 验证转发模式切换成功后的业务表现
这里的验证分两个层面。第一个层面是规则层面,刚才已经说过了。第二个层面是流量转发层面,要确认Service访问仍然正常、负载均衡策略符合预期。
先简单测一下DNS和ClusterIP:
kubectl run -it --rm test-pod --image=busybox --restart=Never -- nslookup kubernetes.default.svc.cluster.local然后模拟多Pod后端服务的负载均衡效果。创建一个Deployment,副本数设3,再建一个Service,在测试Pod里连续多次访问Service的ClusterIP,记录返回的Pod名称。如果IPVS调度算法是rr,应该能看到三个后端轮换出现。
这个步骤有个比较实用的细节:IPVS模式下,如果Service指定了externalTrafficPolicy: Cluster,那么跨节点转发时IPVS仍然会做SNAT,回包路径没什么特殊问题。如果设成了Local,那么只有后端Pod所在节点才会转发流量,IPVS规则里的Forward权重会变成0,ipvsadm里能明显看到非本节点的后端不参与调度。
4. 切换背后的性能变化与风险回避
4.1 用数据说话:iptables与IPVS的实测对比
切换完成后,我在同样规模的集群环境里重新做了延迟和吞吐对比。测试工具用的是简单的TCP连接压力测试,模拟一个Service后面的多个后端Pod。
测试场景一:集群里Service数量控制在50个以内,两者差别不大。iptables的平均新建连接延迟在0.5ms以内,IPVS也在同一水平。这时候就是杀鸡用牛刀,看不出优势。
测试场景二:Service数量模拟到300个,iptables的新建连接延迟开始出现明显爬升,P99接近8ms,同时高并发下出现少量connection timeout。IPVS模式在同样场景下,P99基本稳定在0.6ms左右,连接失败率几乎为零。
测试场景三:持续高频的Pod扩缩容操作,模拟Deployment滚动更新时Endpoint频繁增删。iptables模式下,每变化一次,规则表整体刷新一次,期间的新建连接会有短暂的丢包窗口。IPVS模式下,kube-proxy只是增量更新IPVS规则,没有全量刷新动作,业务侧几乎感知不到变化。
三组测试做完,我心里非常有底。后来这个集群在Service数量增长到500以上时,网络层依然没出现过由于转发规则导致的超时,整体稳定度明显上了一个台阶。
4.2 切换时要提前想清楚的几个风险点
IPVS并不是银弹,切换前有几个风险点一定要提前考虑清楚。
第一个是内核依赖。IPVS依赖较新的内核特性,如果节点内核太老,某些调度算法可能不可用。比如sh算法需要较完整的内核支持,老内核上可能会报错。
第二个是iptables规则还在发挥作用。IPVS模式并没有完全绕过iptables,所以不能用“切换后iptables规则就清理干净了”的思维去看待问题。部分场景下仍需保留一些iptables规则,比如K8S自身的健康检查NodePort流量、部分CNI组件的流量引导等。
第三个是会话保持策略的变化。iptables模式下做会话保持一般依赖conntrack的哈希特性,而IPVS的会话保持依赖调度算法和超时参数。如果业务侧有强一致性的会话保持需求,建议在Service上配置sessionAffinity,同时在IPVS层确认sh算法生效。
第四个是MTU和TSO卸载问题。切换IPVS后,如果出现部分大包超时、小包正常的情况,要排查是不是虚拟化环境里的网卡TSO特性与IPVS转发冲突。这类问题比较隐蔽,我遇到过一次,后来在节点上关闭了eth0的TSO和GSO特性才恢复正常。
5. 常见问题与排查技巧实录
5.1 kube-proxy状态正常但ipvsadm没有规则
这个现象一般出现在切换初期。kube-proxy Pod运行正常,日志也没报错,但节点上ipvsadm一看是空的。优先排查kube-proxy是否真的以IPVS模式启动了。看日志是关键:
kubectl -n kube-system logs kube-proxy-xxxxx | grep ipvs如果日志里有类似"Using ipvs Proxier"的字样,说明模式切换成功。如果没有,多半是ConfigMap没生效,检查一下kube-proxy是不是从旧的启动参数里读取了--proxy-mode=iptables,这种情况直接删除旧的DaemonSet Pod让它重新创建可能不生效,需要确认DaemonSet的启动命令有没有覆盖ConfigMap的设置。
5.2 节点重启后IPVS规则丢失
IPVS规则是由kube-proxy动态写入的,节点重启后kube-proxy启动时会重新加载规则,理论上不会有问题。但有一种情况,节点启动后kube-proxy还没起来,业务流量就已经到了,这个时候Service可能短暂不可用。解决思路是为kube-proxy设置合理的就绪探针和启动顺序依赖,或者提前把IPVS的内核模块加载提前到kube-proxy容器之前,也就是之前提到的modules-load.d配置必须写好。
5.3 切换后部分Service出现connection refused
这个坑我记忆深刻。切换IPVS后,部分Service访问时报错,排查发现是Service的targetPort与Pod内实际监听端口不一致,导致IPVS把流量转发到了错误端口。iptables模式下这类错误也可能发生,但IPVS模式对端口匹配更严格,一旦匹配不到后端就直接拒绝。
遇到这种问题,先用ipvsadm -ln看服务的转发端口和后端端口,对照一下Service定义里的port和targetPort。另外也检查一下Pod是否真的监听在了正确端口上。这一步看着基础,但生产环境出问题时经常被复杂表象迷惑,反而漏了最基础的端口对应关系。
5.4 高并发下出现丢包或延迟抖动
如果IPVS模式下仍然出现延迟抖动,建议优先关注conntrack表和相关内核参数。虽然IPVS比iptables轻量很多,但conntrack仍然会介入连接跟踪。可以适当调大conntrack限制:
sysctl -w net.netfilter.nf_conntrack_max=524288 sysctl -w net.netfilter.nf_conntrack_buckets=131072同时建议调整net.ipv4.tcp_tw_reuse和net.ipv4.tcp_fin_timeout等TCP参数,减少TIME_WAIT状态的堆积。网络调优从来不是单点改动就能一劳永逸的,IPVS只是把转发路径上的一个大瓶颈消掉了,剩下的内核参数、CNI插件、网络拓扑仍然需要逐一验证。
6. 写在最后的几个心得
这次iptables切换IPVS的优化经历,让我对K8S网络链路有了更深的理解。集群规模没到一定程度,iptables完全够用,强行上IPVS反而增加了内核模块依赖和排查复杂度。但当Service数量和流量规模真正上来以后,IPVS带来的收益是实打实的。
切换过程本身不复杂,最难的是评估风险、验证稳定性和后续问题排查。我个人经验是,生产环境切换前一定要先在测试集群完整跑一遍,重点观察Pod扩缩容场景下网络变化的流畅度,以及长时间运行后的并发连接稳定性。IPVS模式在K8S生态里已经是成熟方案,但每个集群的网络环境、CNI插件、内核版本都不一样,切不切换、怎么切换,最终还是要结合自己的实际场景来决策。
最后再分享一个小技巧:切换完成后保留几台节点在旧的iptables模式小流量观察一段时间,作为灰度发布的兜底。等新模式稳定性被验证充分后,再把这些节点也切过去。这种分批替换的方式虽然不是最快的,但能最大程度降低集群整体网络变更的风险。