Kubernetes集群跑起来之后,流量怎么进、怎么分,永远是最先冒出来的问题。我最近在一个生产环境里把Service的负载均衡方案从iptables模式切换到了IPVS,同时用External IP把集群入口统一收敛成了几个固定的虚拟IP,整个链路从“能通但不好查”变成了“规则清晰、流量分布可控”。这篇文章就把这次实践的完整思路和操作过程整理出来,涉及kube-proxy的IPVS模式切换、External IP的实现方式、以及两者如何协同工作,适合正在规划集群入口方案、或者觉得现有负载均衡链路不好排查的同学参考。
1. 为什么IPVS和External IP要放在一起讲
很多人在刚开始接触Kubernetes网络时,容易把IPVS和External IP当成两个独立的话题。IPVS是kube-proxy的一种工作模式,负责处理Service到Pod的流量转发;External IP则是Service对外暴露流量的入口地址。但从实际生产链路看,这两个层面是串在一起的:外部流量先打到External IP上,然后经过节点接入层进入Service的ClusterIP,最后再由IPVS或iptables分发到后端Pod。任何一环出问题,整个服务就不可用。
1.1 流量入口的完整路径
一次完整的外部访问,在Kubernetes集群内大致要经过下面这几跳:
- 客户端请求到达External IP(可能是MetalLB分配的VIP,也可能是手动配置的节点IP映射)。
- 流量进入节点后,由节点上的kube-proxy根据Service定义进行DNAT(目标地址转换),把目的地址从ClusterIP转换为后端Pod的IP。
- Pod处理请求并返回,kube-proxy再执行反向SNAT,把源地址重新改写,保证客户端看到的响应来源是Service的虚拟IP。
在这个链路里,External IP解决的是“流量怎么进节点”,IPVS解决的是“流量进节点之后怎么分”。两者一个管入口、一个管分发,割裂开来看问题,往往会出现“External IP通但不代表Service健康,Service健康但入口死活不通”的尴尬局面。
1.2 两个层级的技术选型
External IP这一层级,常见方案包括NodePort直接暴露、云厂商LoadBalancer、MetalLB(裸金属环境)、keepalived+VIP方案。IPVS这一层级替代的是默认的iptables模式,核心差异在于数据路径的处理方式。
我这次实践的选型组合是MetalLB的Layer2模式提供External IP,kube-proxy切换到IPVS模式做数据路径分发。这个组合的兼容性很成熟,MetalLB只负责宣告VIP并绑定到某个节点,不干预kube-proxy的数据路径;IPVS则专注于四层转发,两者各司其职,排查问题时边界非常清晰。
1.3 统一负载均衡的含义
所谓“统一”,指的是让入口层和数据路径层在策略上对齐。比如External IP通过MetalLB绑定到一个节点后,如果该节点上的IPVS规则没有正确生成,流量就会黑洞;反过来,如果IPVS的后端Pod都在其他节点,而External IP只绑定在某个特定节点上,就需要考虑跨节点转发和路由可达性。统一负载均衡的落地,是在这两层之间建立一种可观测、可配置的联动关系,而不是简单地把两个单词拼在一起。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 方案选型分析
动手之前,我把几个核心选型点拉出来做了对比,这里直接分享结论。
2.1 kube-proxy的iptables与IPVS模式对比
默认情况下,kube-proxy使用iptables模式。它把Service的负载均衡规则翻译成一组iptables链,流量进入时逐条匹配规则。这种方式实现简单、兼容性强,但随着Service数量增长,规则链越来越长,匹配效率会下降。IPVS则完全不同,它在Linux内核中直接集成了LVS(Linux Virtual Server)的能力,通过哈希表和调度算法完成流量分发,不依赖线性匹配规则。
具体对比下来,IPVS有几个明显优势:
- 单个Service可以定义多种调度算法(rr、wrr、lc、wlc、sh等),iptables模式只有随机等概率一种效果。
- IPVS的内核哈希表结构天然支持大规模Service场景,规则多了性能下降不明显。
- ipvsadm可以直接查看连接转发表,排查问题时比逐条数iptables规则直观得多。
iptables不是不能用,中小集群、Service数量几百以内,它完全够用。但如果你的集群已经上千个Service,或者你希望获得更精细的调度策略控制,IPVS基本是唯一合理的演进方向。
2.2 External IP实现方式的选择
External IP这块,我对比了三种路径:
- NodePort:最简单,但端口范围受限(默认30000-32767),多个服务都要暴露时端口管理混乱,而且无法提供稳定的虚拟IP。
- keepalived+VIP:手动维护成本和故障切换逻辑自己写,适合对网络结构有强控制要求的场景,但运维负担不轻。
- MetalLB:裸金属Kubernetes场景的标准方案。Layer2模式下通过ARP应答宣告VIP,BGP模式则对接现有路由器。它把External IP纳入Kubernetes资源管理,操作体验接近云厂商的LoadBalancer。
对于大多数自建集群,MetalLB是最省心的选择。尤其Layer2模式,不需要网络设备配合,只要节点在同一二层网络内就能跑起来。
2.3 为什么选择IPVS + External IP组合
我选择IPVS + External IP组合,核心原因是两者的可观测性和可维护性可以叠加。MetalLB的Service资源状态、IPVS的查看转发表,都能用命令直接确认,链路出问题至少能快速定位到“入口没宣告”还是“规则没生成”。另一个原因是,IPVS模式天然支持会话保持,对于状态化业务,例如WebSocket长连接场景,调度策略的可控性比iptables高很多。这个组合在实际操作中只要配置一次,后续维护基本是一劳永逸。
3. 环境准备与前置条件
开始切换之前,先把基础条件和准备工作做扎实,能少踩很多坑。
3.1 内核模块与系统参数检查
IPVS依赖内核模块,不同Linux发行版的模块名略有差异,但核心模块是一致的。我这次的操作系统是Ubuntu 22.04,内核版本5.15。需要确保以下模块已加载:
bash复制modprobe ip_vs
modprobe ip_vs_rr
modprobe ip_vs_wrr
modprobe ip_vs_lc
modprobe ip_vs_wlc
modprobe ip_vs_sh
modprobe nf_conntrack
为了开机自动加载,把模块名加入/etc/modules-load.d/ipvs.conf文件:
text复制ip_vs
ip_vs_rr
ip_vs_wrr
ip_vs_lc
ip_vs_wlc
ip_vs_sh
nf_conntrack
接下来调整内核参数。最关键的是开启IP转发,同时调整连接跟踪相关的参数,避免高并发下conntrack表溢出:
bash复制sysctl -w net.ipv4.ip_forward=1
sysctl -w net.ipv4.vs.conntrack=1
sysctl -w net.netfilter.nf_conntrack_max=131072
把这些参数持久化到/etc/sysctl.d/99-kubernetes.conf文件里:
text复制net.ipv4.ip_forward=1
net.ipv4.vs.conntrack=1
net.netfilter.nf_conntrack_max=131072
执行sysctl --system生效。注意,net.ipv4.vs.conntrack这个参数必须开启,否则IPVS模式下的Service在某些场景会出现连接跟踪异常,表现就是连接建立后数据回不来。
提示:如果之前没装过ipvsadm,先安装一下,后面的验证全靠它。“
apt install -y ipvsadm”或者对应的yum包。
3.2 kube-proxy配置调整
kube-proxy的mode切换,最稳妥的方式是通过ConfigMap修改。先查看当前kube-proxy的配置:
bash复制kubectl -n kube-system get cm kube-proxy -o yaml
在配置中找到mode字段,如果没有就添加:
yaml复制apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
mode: "ipvs"
ipvs:
scheduler: "wrr"
strictARP: true
这里的scheduler我选了wrr(加权轮询),适合后端Pod规格差异大的场景。strictARP: true是MetalLB Layer2模式稳定运行的前提,如果不开启,MetalLB的VIP宣告可能会因为kube-proxy的ARP响应冲突而不生效,这是一个很容易被忽略的联动参数。
修改完成后,滚动重启kube-proxy的所有Pod:
bash复制kubectl -n kube-system rollout restart daemonset kube-proxy
3.3 MetalLB安装
MetalLB的安装很简单,直接使用官方manifest部署:
bash复制kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.13.10/config/manifests/metallb-native.yaml
安装完成后,需要创建IP地址池。假设你的业务网段内有192.168.10.200-192.168.10.220这一段空闲IP,配置如下:
yaml复制apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: production-pool
namespace: metallb-system
spec:
addresses:
- 192.168.10.200-192.168.10.220
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: production-advertisement
namespace: metallb-system
spec:
ipAddressPools:
- production-pool
注意:IP地址池选段时,一定要避开DHCP动态分配范围,也要避开其他服务器已占用的静态IP,MetalLB不会自动检测IP冲突。
4. 实操:切换到IPVS模式的完整步骤
准备工作结束后,进入正式的切换操作。
4.1 确认当前模式与节点状态
切换前先记录当前工作模式,方便对比验证:
bash复制kubectl -n kube-system get pods -l k8s-app=kube-proxy
kubectl -n kube-system logs kube-proxy-xxxxx | grep -I mode
正常情况下,能看到Using ipvs Proxier这类日志,说明IPVS模式已经生效。如果日志显示的是Using iptables Proxier,说明ConfigMap修改没有被正确加载。
4.2 验证IPVS规则生成
创建一个测试Service,比如一个nginx服务:
yaml复制apiVersion: v1
kind: Service
metadata:
name: nginx-test
spec:
selector:
app: nginx
ports:
- port: 80
targetPort: 80
创建后,在任何节点上执行ipvsadm查看规则:
bash复制ipvsadm -ln
输出中会看到类似下面的内容:
text复制IP Virtual Server version 1.2.1
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn InActConn
TCP 10.96.0.10:80 wrr
-> 172.16.1.2:80 Masq 1 0 0
-> 172.16.1.3:80 Masq 1 0 0
LocalAddress一列就是Service的ClusterIP,RemoteAddress就是后端的Pod IP。看到这个输出,说明kube-proxy已经正确生成IPVS虚拟服务器规则。
4.3 测试负载均衡效果
用Client Pod或者直接在节点上curl测试:
bash复制curl http://10.96.0.10/
连续访问多次,观察ipvsadm -ln --stats的计数器变化。用以下命令查看详细统计:
bash复制ipvsadm -ln --stats
输出里每个后端Pod的ActiveConn会交替增长,说明轮询调度生效。如果发现某个Pod的流量始终是0,检查对应的Pod健康状况和Service的Endpoint列表:
bash复制kubectl get endpoints nginx-test
4.4 切换后常规状态检查清单
切换IPVS模式后,我每次都会快速过一遍以下检查项:
- 所有节点上的ipvsadm规则数量和Service数量一致。
- kube-proxy日志中没有任何关于模块加载失败的报错。
- DNS服务(CoreDNS)在IPVS模式下工作正常,因为CoreDNS本身也是通过ClusterIP暴露的。
- 已有StatefulSet或Stateful服务使用的Headless Service不受影响,Headless Service不经过IPVS虚拟服务器。
实操心得:切换模式不会中断已有连接,因为kube-proxy的重启是滚动进行的,每台节点独立生效。但为了保险,建议在业务低峰期操作,毕竟内核路径换了,谨慎一点没有坏处。
5. 实操:External IP的接入与统一
IPVS模式正常后,开始接入External IP。这一步的目标是让外部流量可以通过固定虚拟IP直接访问集群内的Service。
5.1 为Service分配External IP
创建LoadBalancer类型的Service:
yaml复制apiVersion: v1
kind: Service
metadata:
name: nginx-lb
spec:
type: LoadBalancer
selector:
app: nginx
ports:
- port: 80
targetPort: 80
应用后查看Service状态:
bash复制kubectl get svc nginx-lb
MetalLB会从IPAddressPool中分配一个IP,显示在EXTERNAL-IP列。如果状态显示<pending>,查看MetalLB的controller日志:
bash复制kubectl -n metallb-system logs -l component=controller
常见的pending原因有两种:IP地址池没有可分配的IP;或者L2Advertisement没有正确引用IPAddressPool。
5.2 理解External IP在IPVS模式下的转发路径
External IP分配完成后,请求访问192.168.10.200时,链路是这样的:
- MetalLB通过ARP应答告知客户端,VIP属于某个节点(Leader节点)。
- 客户端请求到达Leader节点。
- Leader节点上的kube-proxy收到目的地址为VIP的请求,执行DNAT,把目标地址转换为Service的ClusterIP
10.96.0.10。 - IPVS规则再次接管,将请求分发到后端Pod。
这里有个关键点:步骤3中VIP到ClusterIP的转换发生在进入IPVS规则之前,它由MetalLB添加的一条iptables规则完成。所以即使kube-proxy工作在IPVS模式,iptables仍然承担了小部分流量入口处理职责。这不会影响性能,因为IPVS才是主数据路径,但排查问题时脑海里要有这张完整的链路图。
5.3 从External IP到VIP的调度策略验证
验证整个链路是否通畅,推荐从集群外部发起访问,比如在客户端机器上执行:
bash复制for i in $(seq 1 10); do curl -s http://192.168.10.200/ | grep "Hostname"; done
如果你在后端Pod的所有Nginx Pod里返回了不同Pod名称,说明IPVS轮询调度和External IP入口链路都工作正常。
5.4 一个入口统一多个Service
实际生产环境中,不会为每个Service单独分配一个External IP,而是通过一个入口统一暴露多个HTTP服务。有两个常用思路:
- 思路一:使用Ingress Controller(比如ingress-nginx)承载所有HTTP流量,Ingress本身只需要一个LoadBalancer类型的Service和一个External IP。
- 思路二:多个四层Service各自分配External IP,适用于非HTTP协议,比如MySQL、Redis这些需要四层直连的场景。
在我这次实践里,HTTP服务走Ingress入口,统一绑定一个VIP;数据库和缓存服务则各自绑定独立VIP。这样在IPVS层面,每个服务都有独立的一组虚拟服务器规则,互不干扰,排查问题也清晰。
5.5 手动配置External IP的特殊场景
有些时候不方便部署MetalLB,比如需要严格指定VIP归属节点。这种情况下,可以手动在Service上配置externalIPs字段:
yaml复制apiVersion: v1
kind: Service
metadata:
name: nginx-manual
spec:
type: ClusterIP
selector:
app: nginx
ports:
- port: 80
targetPort: 80
externalIPs:
- 192.168.10.250
然后在节点上手动添加VIP和路由规则,并用keepalived保证VIP高可用。这种方案的优势是可控,但运维成本明显比MetalLB高。除非有特殊要求,否则我仍然推荐MetalLB。
6. 统一负载均衡的关键细节与最佳实践
这部分是我实际操作后总结出来的关键细节,每个都是踩过坑或优化过的点。
6.1 会话保持对状态化服务的重要性
IPVS模式的会话保持特性非常实用,我重点说一下。在kube-proxy的ConfigMap中,可以针对特定Service配置会话保持:
yaml复制apiVersion: v1
kind: Service
metadata:
name: websocket-service
spec:
sessionAffinity: ClientIP
sessionAffinityConfig:
clientIP:
timeoutSeconds: 10800
配置后,来自同一个客户端IP的请求会始终转发到同一个后端Pod,这对WebSocket、SSH这类需要保持状态的服务至关重要。底层原理是IPVS的sh调度算法会在哈希表中记录源IP到目标Pod的映射关系,后续请求直接查表分发,不再重新调度。
注意,会话保持和负载均衡是有点矛盾的。开启会话保持后,某一时刻的请求可能集中在某个Pod上,后端压力不均。所以timeoutSeconds不要设置过长,一般按业务需要来,比如WebSocket可以设长一点,普通HTTP服务不建议开启。
6.2 externalTrafficPolicy的选型
Service的externalTrafficPolicy字段是统一负载均衡方案中必须决策的参数。默认值是Cluster,含义是所有节点都有概率收到外部流量,然后通过IPVS转发到任意节点上的Pod。这个模式实现简单,但它会保留两层转发,客户端源IP在到达Pod时会丢失。
设置为Local模式后,外部流量只会到达那些有对应后端Pod的节点,IPVS不做跨节点转发,Pod能直接看到客户端真实IP。代价是容易产生负载不均,某个节点没有Pod时,VIP的流量不会路由到它。
我的建议是:如果能接受源IP丢失,用默认的Cluster模式,负载最均衡;如果业务强依赖真实客户端IP,比如需要做访问控制或地域统计,用Local模式,同时可以配合Pod反亲和性或者DaemonSet来保证每台节点都有后端Pod。
6.3 健康检查与故障转移
IPVS本身不会自动摘除故障Pod,它依赖kube-proxy周期性地同步Endpoints数据。kube-proxy会将Service的Endpoints列表转成IPVS虚拟服务器下的RealServer,如果Pod失联,对应的RealServer会从规则中移除。
为了加快故障转移速度,建议调整kube-proxy的syncPeriod和minSyncPeriod参数:
yaml复制apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
mode: "ipvs"
ipvs:
scheduler: "wrr"
strictARP: true
syncPeriod: 30s
minSyncPeriod: 5s
默认的syncPeriod可能是1分钟,对于高可用要求高的服务有点长。我调成30s后,Pod故障时最快5秒就能感知,实际业务中断时间大幅缩短。
6.4 高流量场景下的调度算法选择
IPVS支持多种调度算法,默认是wrr。如果只按默认配置跑,在流量峰谷差异大或Pod规格差异大的场景,会出现某些Pod连接数明显偏高的现象。
我的选择经验如下:
- 后端Pod规格一致,连接时长较短:用wrr,默认即可。
- 后端Pod规格差异大:用加权轮询时,给高规格Pod设置更高的
Weight,但Pod没有直接设置方式,需要先确认所有后端一致性,要不就换lc。 - 长连接为主的场景:用lc(最少连接)更好,可以把请求分发到当前活跃连接数最少的Pod上。
- 单客户端大量短连接:用sh可以保证同一个客户端始终落在同一个Pod,减少连接建立开销。
修改调度算法的位置在kube-proxy的ConfigMap中,修改后仍需重启kube-proxy生效。
注意:因为IPVS调度算法的粒度是整个kube-proxy级别,集群内所有Service共用同一个调度算法。如果你希望不同Service使用不同调度算法,需要额外引入其他方案,例如在每个节点上手动配置ipvs规则绕开kube-proxy,但这会带来管理复杂度,一般不建议。
6.5 从iptables模式临时回退的方法
如果IPVS切换后出现异常,回退方案也要提前准备。回退操作很简单:
- 将kube-proxy ConfigMap中的
mode改回空值或iptables。 kubectl -n kube-system rollout restart daemonset kube-proxy。- 验证节点上ipvs规则消失,iptables规则重新生成。
在实际运维中,回退大概率不会用到,但准备一份操作文档能避免紧急情况下的慌乱。
7. 常见问题与排查技巧实录
最后整理一下我这套方案在实际运行中遇到过的问题,以及对应的排查思路。
7.1 IPVS规则未生成
现象:Service创建后,访问ClusterIP超时,ipvsadm -ln中看不到对应规则。
排查步骤:
- 检查kube-proxy日志是否有
can't initialize ipvs之类的报错,如果模块缺失,需要先modprobe ip_vs_*。 - 检查节点上
/proc/net/ip_vs文件是否存在,不存在说明内核模块没有加载成功。 - 检查Service是否已有Endpoints,如果没有后端Pod,IPVS不会生成虚拟服务器规则。
7.2 External IP不通但ClusterIP正常
现象:访问ClusterIP通,访问External IP超时。
排查步骤:
- 先确认MetalLB的状态:
kubectl get svc -A | grep LoadBalancer,查看EXTERNAL-IP列是否已分配。 - 再确认ARP宣告:在VIP绑定的节点上执行
arping -I eth0 <VIP>测试网络。 - 检查节点防火墙(如ufw、firewalld)是否放行了Service对应端口。
- 确认
strictARP: true是否正确设置,如果未开启,MetalLB的ARP响应经常被kube-proxy的响应覆盖。
7.3 流量集中在某个节点
现象:External IP绑定节点上的Pod负载很高,其他节点空闲。
原因分析:externalTrafficPolicy: Cluster模式下,IPVS转发是全局均匀的,但如果External IP入口只有单一节点,该节点的入站流量会比其他节点高,但不应该出现长时间集中。
解决思路:
- 检查后端Pod在各节点上的分布。
- 如果用的是
Local模式,这是正常现象,可以通过调整Pod调度策略解决。 - 确认IPVS调度算法不是sh(哈希调度),sh在源IP固定时总是分发到同一后端,看起来就像流量集中。
7.4 连接数过高导致conntrack表溢出
现象:节点日志中出现nf_conntrack: table full, dropping packet。
解决思路:
- 调大
nf_conntrack_max:sysctl -w net.netfilter.nf_conntrack_max=262144。 - 缩短conntrack的超时时间,比如TCP的ESTABLISHED连接超时:
bash复制sysctl -w net.netfilter.nf_conntrack_tcp_timeout_established=3600
对于纯四层转发的高并发场景,建议同时调整net.ipv4.vs.conntrack=1的相关行为,确保IPVS连接跟踪不会成为瓶颈。
7.5 排查工具速查表
平时我排查这套环境时,最常用的是以下命令组合:
bash复制# 查看IPVS虚拟服务器和真实服务器状态
ipvsadm -ln --stats
# 查看IPVS连接跟踪表
ipvsadm -lnc
# 查看Service状态
kubectl get svc -A -o wide
# 查看Endpoints
kubectl get endpoints -A
# 查看MetalLB状态
kubectl -n metallb-system get ipaddresspool, l2advertisement
# 查看kube-proxy日志
kubectl -n kube-system logs daemonset/kube-proxy --tail=50
这套组合基本覆盖了从入口到后端的全部链路,出现问题时能快速定位到具体环节。
7.6 一个教训:升级内核后模块丢失
有一次我把节点内核从5.15升级到5.19后,发现IPVS模式下的Service全部异常。排查后发现,之前加载的ip_vs内核模块没有随新内核自动编译,模块文件丢失了。重新安装了匹配内核版本的linux-modules-extra包并重启后解决问题。
从这次经历得到的教训是:内核升级后一定要第一时间检查/proc/net/ip_vs是否存在,以及lsmod | grep ip_vs的输出。
实际操作这套方案跑下来,我最深的体会是:IPVS和External IP的组合并不是什么黑科技,它只是把Linux内核本来就有的能力和Kubernetes的Service抽象有机地结合了起来。关键在于理解每一层在链路中的职责,以及在方案设计阶段就把可观测性考虑进去。现在我的每个Service从创建到外部可访问,不到1分钟就能完成,出问题时也能在5分钟内定位到入口层还是分发层。如果你的集群还在用默认的iptables模式,又恰好面临Service规模增长或入口管理混乱的问题,这套方案值得一试。
