Kubernetes Service 底层 IPVS 流量转发原理:容器跨集群节点负载分配路径优化实践
Kubernetes Service 底层 IPVS 流量转发原理:容器跨集群节点负载分配路径优化实践

前言
"沛涵,我们线上有个 Service 延迟突然飙升了,节点间的请求分布很不均匀,感觉流量全打到一个节点上了。"
周一早上我刚把 coffee 放到桌上,运维同事小周就急冲冲地跑过来。我打开 Grafana 看了一眼,确实,三个 worker 节点中有一个的 CPU 和网络吞吐明显比其他两个高出一截。
"你用的是 iptables 模式吧?"我问。
"对啊,默认的。"
"走,给你换成 IPVS 模式,顺便讲讲这里面的门道。"
其实很多 K8s 使用者都知道 Service 能做负载均衡,但真正理解底层流量怎么走、为什么 iptables 在集群规模变大后会成为性能瓶颈、IPVS 又是怎么解决这些问题的——能说清楚的人并不多。今天我们就从底层原理到实战优化,把这件事彻底聊透。
一、底层原理:IPVS 如何替代 iptables 成为更优解
1.1 iptables 的痛点
先回顾一下默认的 iptables 模式。kube-proxy 通过 watch API Server 监听 Service 和 Endpoint 的变化,然后写入 iptables 规则。每条规则是一个链式匹配:
flowchart TD
A[Pod 发出请求\n访问 ClusterIP] --> B[PREROUTING 链]
B --> C[KUBE-SERVICES 链\n匹配目标 ClusterIP]
C --> D{命中 Service?}
D -->|是| E[KUBE-SVC-XXXX 链\n负载均衡判断]
D -->|否| F[正常路由]
E --> G{随机概率匹配\n每个 Endpoint 对应一条规则}
G -->|prob 33.3%| H[KUBE-SEP-1\n→ Pod A]
G -->|prob 33.3%| I[KUBE-SEP-2\n→ Pod B]
G -->|prob 33.3%| J[KUBE-SEP-3\n→ Pod C]
H --> K[DNAT → 目标 Pod IP]
I --> K
J --> K
K --> L[路由到目标节点/容器]
iptables 的匹配是线性遍历的。当集群中有几千个 Service、上万个 Endpoint 时,iptables 规则数会膨胀到数万条。每条新连接的匹配都需要遍历整个链,CPU 开销随着规则数线性增长。更麻烦的是,当 Service 或 Pod 发生变化时,iptables 规则需要全量刷新,这会导致短暂的连接断开。
1.2 IPVS 的设计哲学
IPVS(IP Virtual Server)是 Linux 内核内置的第4层负载均衡器,它使用 hash table 而不是链表来做匹配。时间复杂度从 O(n) 降到了 O(1),无论你有100条规则还是10000条规则,匹配速度恒定。
| 对比维度 | iptables 模式 | IPVS 模式 |
|---|---|---|
| 数据查找方式 | 链表遍历,O(n) | 哈希表查找,O(1) |
| 规则更新方式 | 全量刷新,iptables-restore | 增量更新,ipvsadm |
| 10,000 条规则内存开销 | ~20 MB | ~5 MB |
| 大规模集群性能 | 明显下降 | 基本恒定 |
| 负载均衡算法支持 | 仅 random(概率匹配) | rr/wrr/lc/lblc/sed/nq 等 8+ 种 |
| 健康检查 | 不支持原生健康检查 | 支持后端权重和实时调度 |
| DNAT 开销 | 通过 iptables 额外处理 | IPVS 内置 NAT 模式 |
1.3 IPVS 的三种转发模式
IPVS 在 Linux 内核层面支持三种转发模式,但在 K8s 中最常用的是 NAT 模式:
flowchart LR
subgraph NAT模式
C1[客户端] -->|请求 VIP:Port| N[IPVS NAT\n修改目标IP为RS IP]
N -->|源IP也改成LVS IP| RS1[RealServer]
RS1 -->|响应回给LVS| N
N -->|改回VIP:Port| C1
end
subgraph DR模式
C2[客户端] -->|请求 VIP:Port| D[IPVS DR\n不改MAC,只改目标MAC]
D -->|目标MAC改为RS| RS2[RealServer]
RS2 -->|直接回复客户端\nbypass LVS| C2
end
subgraph Tunneling模式
C3[客户端] -->|请求 VIP:Port| T[IPVS Tun\nIPIP封装]
T -->|内层IP不变\n外层IP改为RS| RS3[RealServer]
RS3 -->|解封装后处理\n直接回复| C3
end
| 转发模式 | 原理 | 优点 | 缺点 | K8s 中使用 |
|---|---|---|---|---|
| NAT | 修改目标 IP 和端口,响应经由 LVS 返回 | 后端无需特殊配置,RS 可跨网段 | 响应流量走 LVS,可能成为瓶颈 | K8s 默认使用的模式 |
| Direct Routing | 修改目标 MAC,后端直接回复客户端 | 性能最高,响应不经过 LVS | RS 需要配置 VIP 到 lo 接口,需同网段 | 不适用 |
| Tunneling | IPIP 封装报文 | RS 可跨网段,响应直回 | 需要 IPIP 内核模块,额外头部开销 | 不适用 |
K8s 选择 IPVS NAT 模式是因为集群节点通常不在同一二层网络,且不能要求每个节点额外配置 lo 接口的 VIP。
二、快速上手:在 K8s 中开启 IPVS 模式
2.1 确认内核支持
首先确保节点内核加载了 IPVS 相关模块:
# 确认内核模块是否加载
lsmod | grep ip_vs
# 如果没有,加载所有 IPVS 模块
cat <<EOF | sudo tee /etc/modules-load.d/ipvs.conf
ip_vs
ip_vs_rr
ip_vs_wrr
ip_vs_sh
nf_conntrack
EOF
sudo systemctl restart systemd-modules-load.service
# 确认加载成功
lsmod | grep -E "ip_vs|nf_conntrack"
2.2 修改 kube-proxy 配置
将 kube-proxy 的 mode 从 iptables 改为 ipvs:
# 编辑 kube-proxy ConfigMap
kubectl edit configmap kube-proxy -n kube-system
修改关键字段:
apiVersion: v1
kind: ConfigMap
metadata:
name: kube-proxy
namespace: kube-system
data:
kube-proxy.yaml: |
apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
mode: "ipvs" # 从 iptables 切换为 ipvs
ipvs:
scheduler: "rr" # 调度算法:轮询
strictARP: true # 严格 ARP,避免 IP 冲突
tcpFinTimeout: 10s
tcpTimeout: 120s
udpTimeout: 10s
iptables:
masqueradeAll: true # 所有流量做 SNAT
masqueradeBit: 14
clusterCIDR: "10.96.0.0/12"
2.3 重建 kube-proxy Pod
# 重建 kube-proxy Pod 使配置生效
kubectl rollout restart daemonset kube-proxy -n kube-system
# 观察 Pod 状态
kubectl get pods -n kube-system -l k8s-app=kube-proxy -w
# 确认 kube-proxy 已使用 IPVS 模式
kubectl logs -n kube-system -l k8s-app=kube-proxy --tail=50 | grep "ipvs"
# 预期输出: "Using ipvs Proxier"
2.4 验证 IPVS 规则
# 查看 IPVS 规则表
sudo ipvsadm -Ln
# 输出示例
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.20:53 Masq 1 2 5
-> 192.168.1.21:53 Masq 1 1 3
TCP 10.96.0.10:9153 rr
-> 192.168.1.20:9153 Masq 1 0 0
-> 192.168.1.21:9153 Masq 1 0 1
可以看到每个 ClusterIP:Port 对应一个 Virtual Service,后面的 RealServer 就是真正的 Pod IP。Masq 表示 NAT 模式(K8s 中对应 iptables masquerade),Weight 是权重,ActiveConn 是当前活跃连接数。
三、核心 API 与深水区:掌控 IPVS 流量调度
3.1 IPVS 支持的调度算法
kube-proxy 支持通过 ipvs.scheduler 参数指定调度算法:
| 算法 | 全称 | 行为 | 适用场景 |
|---|---|---|---|
rr |
Round Robin | 轮询分发 | 后端能力均等的通用场景 |
wrr |
Weighted RR | 按权重轮询 | 后端规格不一时 |
lc |
Least Connection | 发到当前连接数最少的 RS | 长连接服务 |
wlc |
Weighted LC | 加权最少连接 | 后端规格不同 + 长连接 |
lblc |
Locality-Based LC | 基于局部性的最少连接 | Cache 类服务 |
lblcr |
LBLCR with Replication | 带复制的局部性 LC | Cache 集群 |
dh |
Destination Hashing | 按目标 IP 哈希 | 同源同目的地 |
sh |
Source Hashing | 按源 IP 哈希 | 同源同后端 |
3.2 Go 中操作 IPVS 规则
如果你想在 Operator 或自定义控制器中管理 IPVS 规则,可以使用 github.com/moby/ipvs 库:
package main
import (
"fmt"
"net"
"github.com/moby/ipvs"
)
func main() {
handle, err := ipvs.New()
if err != nil {
panic(err)
}
defer handle.Close()
// 获取所有 IPVS 服务规则
services, err := handle.GetServices()
if err != nil {
panic(err)
}
for _, svc := range services {
fmt.Printf("VIP: %s:%d, Scheduler: %s, Protocol: %d\n",
net.IP(svc.Address).String(),
svc.Port,
svc.SchedName,
svc.Protocol,
)
// 获取每个 Service 的后端 RealServer
dests, _ := handle.GetDestinations(svc)
for _, dest := range dests {
fmt.Printf(" -> RS: %s:%d, Weight: %d, ActiveConn: %d\n",
net.IP(dest.Address).String(),
dest.Port,
dest.Weight,
dest.ActiveConnections,
)
}
}
}
3.3 连接跟踪与 conntrack 调优
IPVS 本身不做连接跟踪,它依赖 nf_conntrack 来维护 NAT 映射关系。当集群流量较大时,conntrack 表可能会满,导致丢包:
# 查看 conntrack 使用情况
cat /proc/sys/net/netfilter/nf_conntrack_count
cat /proc/sys/net/netfilter/nf_conntrack_max
# 调优 conntrack 参数
cat <<EOF | sudo tee /etc/sysctl.d/99-conntrack.conf
net.netfilter.nf_conntrack_max = 2097152
net.netfilter.nf_conntrack_buckets = 524288
net.netfilter.nf_conntrack_tcp_timeout_established = 3600
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 60
net.netfilter.nf_conntrack_tcp_timeout_close_wait = 60
net.ipv4.conf.all.rp_filter = 0
net.ipv4.conf.default.rp_filter = 0
EOF
sudo sysctl -p /etc/sysctl.d/99-conntrack.conf
四、实战演练:跨集群节点负载分配路径优化
4.1 场景描述
我们有一个 3 节点的 K8s 集群,部署了一个 6 副本的 Web 服务,需要通过 Service 对外暴露。我们希望流量能均匀分布到所有节点上的 Pod。
4.2 完整配置
---
# 部署 6 副本的 Web 服务
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app
namespace: production
spec:
replicas: 6
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- web
topologyKey: kubernetes.io/hostname
containers:
- name: nginx
image: nginx:alpine
ports:
- containerPort: 80
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
---
apiVersion: v1
kind: Service
metadata:
name: web-svc
namespace: production
annotations:
service.kubernetes.io/topology-aware-hints: "Auto"
spec:
type: ClusterIP
selector:
app: web
ports:
- port: 80
targetPort: 80
sessionAffinity: None # 关闭会话保持,让 IPVS rr 算法自由调度
4.3 验证流量分布
部署后,用压测工具验证流量是否均匀:
# 创建一个测试 Pod
kubectl run test-pod --image=busybox -it --rm --restart=Never -- sh
# 在 test-pod 中持续发送请求
while true; do wget -q -O- http://web-svc.production.svc.cluster.local; sleep 0.1; done
# 在另一个终端观察 IPVS 统计
watch -n 2 'sudo ipvsadm -Ln | grep -A 10 "web-svc"'
# 输出示例(观察 ActiveConn 在各 RS 间是否接近)
TCP 10.96.0.20:80 rr
-> 10.244.1.2:80 Masq 1 45 50
-> 10.244.1.3:80 Masq 1 48 47
-> 10.244.2.2:80 Masq 1 44 51
-> 10.244.2.3:80 Masq 1 46 49
-> 10.244.3.2:80 Masq 1 47 48
-> 10.244.3.3:80 Masq 1 45 50
4.4 外部流量接入:同节点优先转发
对于从外部进入集群的流量(通过 NodePort 或 LoadBalancer),我们希望尽量把请求转发到本节点的 Pod,减少跨节点开销。这需要结合 externalTrafficPolicy: Local:
apiVersion: v1
kind: Service
metadata:
name: web-svc-lb
namespace: production
spec:
type: LoadBalancer
selector:
app: web
ports:
- port: 80
targetPort: 80
externalTrafficPolicy: Local # 保留客户端源 IP,且只转发到本节点 Pod
注意:使用 externalTrafficPolicy: Local 时,如果某个节点上没有运行 Pod,该节点就不会接收外部流量,需要做好 Pod 分布策略(如上面配置的 podAntiAffinity + 合适的副本数)。
4.5 路径优化对比
flowchart LR
subgraph 优化前_iptables
IN1[外部请求\n→ Node A:30080] --> IPT1[iptables DNAT\n概率匹配]
IPT1 -->|1/3概率| N1A[Node A Pod]
IPT1 -->|1/3概率| N1B[Node B Pod]
IPT1 -->|1/3概率| N1C[Node C Pod]
N1B -->|跨节点网络开销| N1A_Resp[响应经 Node A 返回]
end
subgraph 优化后_IPVS_+_Local
IN2[外部请求\n→ Node A:30080] --> IPV2[IPVS rr 调度]
IPV2 -->|Local 策略| N2A[仅转发到\n本节点 Pod]
IPV2 --> N2A_Resp[响应直连返回\n无跨节点开销]
end
五、避坑指南
问题 1:切换 IPVS 模式后 Service DNS 解析失败
现象:切换为 IPVS 后,部分 Service 的 DNS 解析超时。
原因:IPVS 模式下,kube-proxy 对 ClusterIP 的应答会做 SNAT,但如果 nf_conntrack 模块未加载或参数不合理,UDP 的 conntrack 条目可能被过早回收,导致 DNS 应答包无法正确路由回请求方。
解决方案:
# 确保 nf_conntrack 模块加载
sudo modprobe nf_conntrack
sudo modprobe nf_conntrack_ipv4
# 调整 UDP 超时参数
sudo sysctl -w net.netfilter.nf_conntrack_udp_timeout=30
sudo sysctl -w net.netfilter.nf_conntrack_udp_timeout_stream=120
问题 2:IPVS 模式下 kube-proxy 日志大量报错 "can't add service"
现象:切换后 kube-proxy 日志持续输出类似 can't add service: Address already in use 的错误。
原因:kube-proxy 的 strictARP: true 导致它接管了节点的 ARP 应答,但如果节点上已有其他服务占用了相同 IP(如 keepalived 的 VIP),就会冲突。
解决方案:
# 修改 kube-proxy ConfigMap
ipvs:
strictARP: false # 没有非必要不要开启
# 或者确保没有其他服务占用相同 IP 段
如果确实需要 strictARP(如使用 MetalLB),则需要规划好 IP 段,避免冲突。
问题 3:大量短连接导致 conntrack 表溢出
现象:集群规模较大时,节点 dmesg 出现 nf_conntrack: table full, dropping packet。
原因:IPVS 模式依赖 conntrack 做 NAT 映射,而 Service Mesh 或高频微服务调用会产生大量短连接,迅速撑满默认的 conntrack 表(通常 65536 条)。
解决方案:
# 增大 conntrack 容量
sudo sysctl -w net.netfilter.nf_conntrack_max=2097152
sudo sysctl -w net.netfilter.nf_conntrack_buckets=524288
# 如果使用 EKS / 托管 K8s,考虑改用 AWS VPC CNI 的直通模式
# 减少 conntrack 整体负载
六、总结
回到开头的场景,我们把集群切换到 IPVS 模式后,又配合 externalTrafficPolicy: Local 做了同节点优先转发,节点间的流量分布曲线肉眼可见地变得均匀了。之前那个 CPU 偏高节点的负载直接降了 35%。
回顾一下今天聊的核心要点:
- iptables 的线性匹配在大规模集群中会成为瓶颈,而 IPVS 的哈希查找将时间复杂度从 O(n) 降到了 O(1)
- IPVS 在 K8s 中使用 NAT 模式,通过修改目标 IP 做流量转发,响应流量也经过 LVS 节点返回
- 支持 rr、wrr、lc、sh 等 8 种调度算法,根据业务场景灵活选择
- 跨集群节点负载优化的关键是合理的 Pod 分布策略 + externalTrafficPolicy: Local + 正确的 conntrack 调参
写这篇稿子的时候,Ping 又跳到键盘上来了,踩了一串乱码进去。我把它抱开,它一脸不服气地蹲在显示器旁边看我敲 ipvsadm -Ln。可能它也对那个哈希表挺感兴趣的吧。
下篇预告:K8s CNI 网络插件深度对比:Calico vs Cilium vs Flannel,eBPF 正在如何改变网络转发面——感兴趣的朋友可以先关注起来。
如果你在实际迁移 IPVS 过程中遇到什么坑,欢迎在评论区交流。
更多推荐

所有评论(0)