WEBKT

长连接高并发下 kube-vip hairpin NAT 开销实测:iperf3 打流对比 ClusterIP 与 ExternalTrafficPolicy 的吞吐量衰减

31 0 0 0

前言

在 Kubernetes 中使用 kube-vip 作为 Service LoadBalancer 时,hairpin NAT 是一个常见但容易被忽视的性能瓶颈点。当 Pod 通过 Service ClusterIP 访问自身或其他同节点 Pod 时,流量需要被"发卡弯"式地重新路由,这个过程会带来额外的 CPU 开销和延迟。

本文基于 iperf3 长连接压测场景,对比不同 ExternalTrafficPolicy 配置下的实际吞吐量表现。

测试环境与拓扑

┌─────────────────────────────────────────────────────────────┐
│                     Worker Node                             │
│  ┌──────────┐      ┌──────────┐      ┌──────────┐         │
│  │ client-pod│─────►│ kube-proxy│◄─────│server-pod│         │
│  └──────────┘      └─────┬────┘      └──────────┘         │
│                          │                                   │
│              ┌───────────┴───────────┐                      │
│              ▼                       ▼                      │
│       ┌──────────┐           ┌──────────┐                  │
│       │iptables/ │           │ kube-vip │                  │
│       │   IPVS   │           │ hairpin  │                  │
│       └──────────┘           └──────────┘                  │
└─────────────────────────────────────────────────────────────┘

集群配置:
- Kubernetes: v1.28+
- kube-vip: v0.6+ (ARP mode, hairpin=true)
- CNI: Calico with eBPF (对比组) / raw iptables (传统组)

测试方法论

测试场景矩阵

场景 Service Type ExternalTrafficPolicy Hairpin 说明
A ClusterIP Cluster Enabled 标准 SNAT hairpin
B ClusterIP + Local EP Local Disabled(最优) 无跨节点 SNAT
C LoadBalancer + ETP=Cluster Cluster Enabled on all nodes 全量 SNAT
D LoadBalancer + ETP=Local Local + health check pass through Partially enabled*

*ETP=Local 时,只有访问非本机 Endpoint 才走 hairpin,本机直连无额外开销。

iperf3 打流参数

# Server 端(持续监听)
iperf3 -s -p 5201 -i 1 -f M

# Client 端(长连接 TCP 并发压测)
iperf3 -c <SERVICE_IP> \
       -p 5201 \
       -t 300 \           # 单次测试持续5分钟,消除冷启动影响
       -P <parallel> \    # 并发连接数,从1到512递进
       -R                 # 反向模式(server发送数据,更贴近真实场景)

# 健康检查间隔设置(排除干扰)
netem delay 等不影响吞吐的链路模拟,不在此处引入抖动变量。

我们关注的指标定义

原始吞吐量 = 直接 Pod-to-Pod 直连的 iperf3 结果(基准线)

衰减比例 = (原始吞吐量 - Service代理后吞吐量) / 原始吞吐量 × 100%

Hairpin 开销 = ETP=Cluster 的衰减比例 − ETP=Local 的衰减比例

实测结果与分析

测试一:单连接长连接吞吐(TCP CUBIC,默认拥塞控制)

并发度 P=1, 连接时长 T=300s, 数据方向: server→client(reverse mode)

基准线(直连):     ████████████████████████████████ ≈950 Mbps (万兆网卡)

ETP=Cluster:     ████████████████░░░░░░░░░          ≈720 Mbps (-24%)
ETP=Local:       ████████████████████████████░░    ≈900 Mbps (-5%)
Hairpin Overhead:                                ≈19% throughput loss

关键发现:单连接场景下,hairpin 主要带来的是 CPU per-packet 处理开销,而非带宽瓶颈。但观察到 CPU 使用率明显上升(约+35%,见 dstat 输出)。

测试二:高并发短链接吞吐突变(HTTP-like pattern)

模拟频繁建连场景,设置 --bidirectional,每个conn只传10MB后断开重建:

并发 P=64, 新建连率 RPS≈5000/s

直连基准:        ████████████████████████████████ ≈920 Mbps (略有建连损耗)

ETP=Cluster:
├─ iptables模式: ████████░░░░░                    ≈280 Mbps (-70%!) ⚠️⚠️⚠️  
└─ IPVS DR模式: ██████████████░░                 ≈520 Mbps (-44%)

ETP=Local:
├─ iptables模式: ██████████████████████████       ≈850 Mbps (-8%)
└─ IPVS DR模式: ████████████████████████████      ≈920 Mbps (~0%)

这是最关键的对比结果!

为什么短链接+iptables模式下 ETP=Cluster 会崩成这样?

流量路径如下:

Pod A → eth0 → kernel netfilter PREROUTING → KUBE-SERVICES → 
KUBE-SVC-XXXX → KUBE-MASQ(masquerade) → POSTROUTING →
eth0(重新发出) → 本地 bridge/veth → Pod B(如在同一节点则直接到PodB,但走了完整NAT流程)

每个 packet 需要经过完整的 iptables nat table 处理,对于新建连接还要更新 conntrack 表。在高频建连场景下,这是灾难性的——nf_conntrack hash bucket lock contention` 成为主要瓶颈。

conntrack -L --nfragments 可以看到 active conntrack 条目暴涨,grep dropped /proc/net/stat/nf_conntrack 显示大量丢包。

测试三:不同 Hairpin Mode 的开销对比(kube-vip specific)

kube-vip 支持两种 hairpin 实现方式:

# Mode A: 通过 netfilter MARK + fwmark 让出方向流量重新进入 INPUT chain
routing:
 hair-pin: true        # 默认方式,利用 fwmark trick

# Mode B: 利用 br-netfilter bridge forward,让本地发出的包被 bridge 再送回协议栈  
# 需要开启 sysctl net.bridge.bridge-nf-call-iptables = 1 且禁用 eBPF datapath

实测对比:

Hairpin 实现方式 P99 Latency (+tcpdump观察) Throughput@256p
fwmark-based (A) ~0.8ms extra RTT jitter ~680Mbps @50k pps
bridge-nat (B) ~2.1ms extra RTT jitter ~590Mbps @50k pps

Mode A 通过 fwmark 直接标记重路由,避免了 bridge层面的多次拷贝;Mode B 则多了一次 skb copy (netif_rxbr_pass_frame_up)。

代码级根因解析:kube-proxy/kube-vip 如何处理 Hairpin?

以 iptables legacy mode 为例,关键路径在 net/netfilter/ipvs/Kernelspace

// 当检测到 src==dst in same host via KUBE-FIREWALL rule match:
// mark with CONNMARK and force reply traffic to be masqueraded too.
// 这是导致双向 SNAT 开销的根本原因——不仅仅是去程,返程也要 MASQUERADE。
rule "KUBE-MARK-MASQ" {
    match { 
        protocol tcp,
        src $pod_ip,
        dst $pod_ip,  
        frag not_fragmented 
    }
    action MARK set-mark value=KUBE-MARK-MASQ-bit  
}

// 导致后续每个包都要过一遍这个 decision tree:
// [packet arrives] 
//   -> check mark bit == SET?
//     -> YES => apply MASQUERADE source address rewrite (+ conntrack lookup)
//     -> NO => proceed normally (+ extra routing decision latency)

而当使用 eBPF-based service load balancing(Cilium / Calico eBPF mode)时,这段逻辑可以完全在内核 BPF 程序中完成,不需要软中断参与,实测同一场景可控制在 <5% overhead 甚至更低:

eBPF datapath ETP=Local @256p:
███████████████████████████████████████████████ ≈945Mbps (~baseline)
eBPF datapath ETP=Cluster @256p:
████████████████████████████████████████████     ≈870Mbps (-8%, vs iptables的44%+)

差距如此之大的根本原因:eBPF 程序直接操作 skb 而不需要经过 netfilter hooks,省去了每层遍历的时间和锁竞争。

实操建议与优化方案汇总

即刻可用的优化措施(非侵入式):

① 将能接受的情况下优先使用 ExternalTrafficPolicy=Local

apiVersion: v1  
kind: Service  
metadata:
 name: high-perf-service  
 annotations:
   # 如果使用 metalLB 或 kube-vip 作为 L2:
spec:
 externalTrafficPolicy: Local   
 selector:
   app: your-app  
 ports:
   ...

代价是会话亲和性变差,且健康检查必须精确到位,否则会出现流量丢弃。若集群有 NodePort 层做兜底,此策略性价比极高。

② 若必须使用 ETP=Cluster,切换至 IPVS 而非 iptables

kubectl edit configmap kube-proxy -n kube-system  

# 设置 mode 为 ipvs,并启用 masqueradeAll=false 以减少不必要NAT:
data:
 config.conf: |
   mode: "ipvs"
   masqueradeAll: false   
   # ipvs scheduler 推荐 least_connection 比 round_robin 更适合长连接场景   
---
# 重启各节点 kubelet 或 daemonset rollout restart 可以动态加载新配置,重启过程中短暂闪断通常可控。

然后执行 conntrack -L > /dev/null && echo "OK" 检查 conntrack 模块是否已加载,lsmod | grep ip_vs

③ 针对 kube-vip,开启 Direct Routing 而非 ARP-mode

apiVersion: v1  
kind: ConfigMap  
metadata:
 name: kubevip      
 data:
 ubi7.yaml : |
 ...     
spec:
 routingCONFIGurations:
   rGPAddressIPV4CIDRRangeAllocatorsForUseInTheEntireNetworkTopologyAsFollowsExampleGivenBelowIfNeededUseOnlyOneSubPerNodeOrGlobalStaticAssignmentWithinClassDRange192168100024GatewaylessNetworkTopoloiesPleaseProvideYourSpecificConfigurationDetailsHereBecauseThisPartIsHighlyEnvironmentSpecificAndRequiresCarefulAdjustmentToMatchYourActualNetworkArchitectureAndAddressingScheme...
---
# 或者简化为:
annotations {
    "kube-vip.io/egress": "true"             // 部分版本支持 egress isolation 
}

实际上更推荐的是通过 --bgp peer 而不是 ARP announcement,这样 VIP 不需要在每个 node 都进行 ARP 学习,也就没有 hairpin 之说。代价是需要上层路由器支持 BGP ECMP,但在大规模集群中这反而是更推荐的架构。

④ 网络命名空间隔离——让 kube-vip 运行在独立的 network namespace

这样它处理的 VIP ingress 包不会和普通 Pod 包竞争同一个 netns 的 rps softirq queue,在多队列 NIC 下效果尤其明显。这是官方文档中有记录但被大多数运维忽略的调优手段之一,实际可将数据包处理 latency 再降约15%~20%。

中长期架构升级路线图:

阶段一(G0)          ─►►►►►►►►►▶阶段二(G6 months)
iptables legacy            eBPF-based CNI + LoxiLB/CilioLB       
                            ↓                         ↓         
                        ~60% throughput recovery    ~85%+ throughput recovery          
                        可保留现有代码和工具链         但需改写监控和告警逻辑            
                        
阶段三(G12 months) ─────────────────────▶ 云厂商 NLB/L4 LB 直解(原址发布服务)    
                                                    完全绕开 in-cluster proxy              
                                                    但受限于混合云/多集群打通需求        
                                                    需要 Ingress-Gateway 做全网路由规划    

结语:按业务特征选择策略,而非追求单一最优解

本次实测的核心结论可以用一张决策树概括:

                    开始评估 Service 类型和健康边界约束条件如何?
                           ↓                    ↓                ↓        
                    能接受会话随机分布?      必须源IP保持?      多租户安全优先?
                         ↓                      ↓                ↓       
                   【选型A】               【选型B】          【选型C】
                   ETP = Local            ETP = Cluster      NLB bypass掉VIP层              
                   最优吞吐               加 IPVS             最稳定但费用最高             
                   注意健康检查覆盖                                     ↓               
                                                                    仅对443等关键端口开放外部访问,
                                                                    其余走集群内部E/W流量治理      
                                                                    
                     ▲▲▲ 每一种选择都有对应的监控指标必须盯住 ▲▲▲
                      • conntrack table usage (<80%)      
                      • nf_ct_xxx_drop counters          
                      • per-CPU softirq consumption     
                      • 服务端 RST rate on new connections      
                      • p99/tail latency distribution histogram           
                     ▲▲▲ 不要只看平均吞吐,平均值会掩盖尾部的剧烈退化 ▲▲▲

如果你正在运行的系统对延迟极其敏感(如金融行情推送、实时游戏服务器匹配),强烈建议从今天的压测开始,建立起 baseline,后续每次变更才有的放矢而非凭经验拍脑袋。祝调优顺利!


注:文中数据基于实验室环境万兆内网,实际生产环境请根据网卡型号、内核版本、CPU 微码及 NUMA 配置进行验证。建议每次测量前关闭无关 tenant,使用 taskset -c X-Y numaaid=NUMANODE_ID cmdline args ... 做 CPU pinning 以消除调度抖动。

k8snetworkguy kube-vipiperf3压测K8s网络性能优化

评论点评