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 过程中遇到什么坑,欢迎在评论区交流。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐