土法炼钢兴趣小组的算法知识备份

【Kubernetes 网络深度系列】DSR(Direct Server Return):让回包绕过负载均衡器

文章导航

分类入口
kubernetesnetworking
标签入口
#DSR#Direct-Server-Return#Cilium#Maglev#load-balancing#LVS#SNAT#eBPF

目录

上一篇我们聊了 MTU 的问题 – 隧道封装、加密头部都会吞掉有效载荷空间,如果 MTU 不对,就是分片、丢包、性能暴跌。这一篇我们来看另一个影响性能的关键路径:回包路径。

你有一个 NodePort 或 LoadBalancer 类型的 Service,客户端请求从外部打到集群的入口节点(LB 节点),被 DNAT 到某个 Backend Pod。Backend Pod 处理完请求,生成响应 – 问题来了:这个响应走哪条路回去?

在默认的 SNAT 模式下,回包必须先回到入口节点,由入口节点做 reverse SNAT(把源 IP 从 Pod IP 改回 Service VIP),然后再转发给客户端。这意味着入口节点要处理所有流量的双向数据 – 进来一遍,出去一遍。对于视频流、大文件下载这类响应远大于请求的场景,入口节点的带宽和 CPU 就成了瓶颈。

DSR(Direct Server Return)的思路很简单:让 Backend Pod 的回包直接发给客户端,绕过入口节点。入口节点只处理请求方向的流量,响应走最短路径直达客户端。

这篇文章的目标:

  1. 剖析 SNAT 模式的瓶颈
  2. 拆解 DSR 的三种实现方式
  3. 深入 Cilium DSR 的 BPF 实现
  4. 讨论 DSR 与 ExternalTrafficPolicy 的交互
  5. 列举 DSR 的常见坑和排查方法
  6. 构建 Maglev + DSR 的完整高性能 LB 方案
  7. 实验对比 DSR 和 SNAT 模式的包路径

本文基于 Cilium v1.16.x,Linux 6.x 内核。LVS 部分基于 Linux 6.1 的 IPVS 模块。 实验环境:Ubuntu 22.04, kernel 6.5, kind 集群 + Cilium Helm 安装。


一、SNAT 模式的瓶颈:为什么回包路径是问题

默认 SNAT 模式的完整流程

在 Kubernetes 的默认 Service 实现里(无论是 kube-proxy 的 iptables/IPVS,还是 Cilium 的默认模式),外部流量的处理遵循这个流程:

请求方向:
  Client (1.2.3.4:54321) -> VIP (10.0.0.100:80)
    -> Node A 接收,执行 DNAT:dst 改为 10.244.1.5:8080
    -> Node A 执行 SNAT:src 改为 Node A IP(否则回包不会回到 Node A)
    -> 转发到 Node B 上的 Backend Pod (10.244.1.5:8080)

响应方向:
  Backend Pod (10.244.1.5:8080) -> Node A IP
    -> Node A 查 conntrack,执行 reverse DNAT:src 改为 10.0.0.100:80
    -> Node A 执行 reverse SNAT:dst 改为 1.2.3.4:54321
    -> 发送给 Client

关键点在第二步的 SNAT:入口节点必须把源 IP 改成自己的 IP,否则 Backend Pod 的回包会根据路由表直接发给客户端。但客户端发起的连接目标是 VIP,如果回包的源 IP 不是 VIP 而是某个 Pod IP,客户端的 TCP 栈会直接丢弃这个包(源 IP 不匹配)。

所以 SNAT 模式的逻辑是:强制回包经过入口节点,由入口节点负责把地址改回去。

SNAT 模式的三个性能问题

问题一:入口节点带宽瓶颈。 请求可能只有几百字节,而响应可能是几十 KB 到几 MB。所有响应都要经过入口节点转发,出站带宽很容易成为瓶颈。10 个 Backend 节点各有 10Gbps 带宽,但响应都挤入口节点的一条 10Gbps 链路 – 总吞吐量被钉死在 10Gbps。

问题二:额外一跳延迟。 跨 AZ 场景下,回包多经过入口节点这一跳可以贡献 0.5-2ms 的额外延迟。对延迟敏感的实时通信或金融交易场景不可接受。

问题三:入口节点 CPU 开销。 每个回包都要做 conntrack 查找 + reverse SNAT + reverse DNAT。高 PPS 场景下 CPU 成本不可忽视。

DSR vs SNAT 数据流对比

现实案例:大流量场景的痛点

一个典型案例:视频流服务,请求约 200 字节,响应约 2MB(比例 1:10000)。1000 并发客户端时:

入站请求:1000 * 200B = ~200KB/s(微不足道)
出站响应:1000 * 2MB = ~2GB/s = ~16Gbps(入口节点扛不住)

DSR 模式下,16Gbps 响应流量直接从各 Backend 节点发出,入口节点只处理 200KB/s 入站请求。


二、DSR 的基本原理

核心思路

DSR 的核心思路是:入口节点(LB 节点)在转发请求时,不做 SNAT,而是把客户端的原始信息(源 IP、源端口、目标 VIP)编码到转发的包里。Backend 节点收到包后,解码出这些信息,在回包时直接用 VIP 作为源 IP 发给客户端。

这样,客户端看到的响应来自 VIP(和它发起请求的目标一致),TCP 连接状态正确,但包实际上是从 Backend 节点直发的,没有经过入口节点。

对 Backend 节点的要求

为了让 Backend 节点能用 VIP 作为源 IP 发包,Backend 节点必须”认识”这个 VIP – 具体来说,VIP 必须存在于 Backend 节点的某个网络接口上。否则内核的源地址验证(rp_filter)会阻止发出源 IP 不属于本机的包。

传统的做法是在 Backend 的 loopback 接口上配置 VIP,并关闭该接口对 VIP 的 ARP 响应(否则会产生 IP 冲突):

# 传统 LVS DSR:在 Backend 上配置 VIP
ip addr add 10.0.0.100/32 dev lo
# 关闭 loopback 对 VIP 的 ARP 响应
echo 1 > /proc/sys/net/ipv4/conf/lo/arp_ignore
echo 2 > /proc/sys/net/ipv4/conf/lo/arp_announce

在 Cilium 的实现里,不需要手动配置这些。BPF 程序在 TC 层直接改写包头,绕过了内核的源地址验证。


三、DSR 的三种实现方式

DSR 有三种主流的实现方式,按它们操作的网络层分类。

3.1 L2 DSR(MAC 改写)

最简单的 DSR 实现工作在数据链路层。入口节点收到客户端请求后,不修改 IP 头,只把目的 MAC 地址改为 Backend 的 MAC 地址,然后在同一个二层网络里直接转发。

L2 DSR 包流转过程

优点:

限制:

这是 LVS 的 DR 模式(ipvsadm 的 -g 选项),也是 F5 等硬件负载均衡器最常用的 DSR 模式。在传统数据中心里用得很广泛,但在 Kubernetes 的 overlay 网络或跨子网场景下基本不可用。

3.2 L3 DSR / IP-in-IP 隧道

当 LB 和 Backend 不在同一个二层网络时,需要用三层隧道把请求封装后发给 Backend。最常见的是 IP-in-IP 隧道:

原始包:
  IP: src=1.2.3.4, dst=10.0.0.100 (VIP)
  TCP: sport=54321, dport=80

LB 封装后:
  外层 IP: src=LB-IP, dst=Backend-IP
  内层 IP: src=1.2.3.4, dst=10.0.0.100 (VIP)
  TCP: sport=54321, dport=80

Backend 解封装:
  剥掉外层 IP,得到原始包
  目的 IP 是 VIP(配在 loopback 上),内核接收
  回包:src=10.0.0.100 (VIP), dst=1.2.3.4

这是 Cilium 的默认 DSR dispatch 方式(loadBalancer.dsrDispatch=ipip)。也是 LVS 的 TUN 模式(ipvsadm 的 -i 选项)。

# LVS IP-in-IP 隧道模式配置
ipvsadm -A -t 10.0.0.100:80 -s rr
ipvsadm -a -t 10.0.0.100:80 -r 192.168.1.10:80 -i
ipvsadm -a -t 10.0.0.100:80 -r 192.168.1.11:80 -i

优点:

限制:

3.3 L3 DSR / IP Option 编码

第三种方式不用隧道封装,而是利用 IPv4 头部的 Option 字段来携带原始的客户端信息。LB 在转发时直接修改 IP 头的目的地址为 Backend IP,同时把原始的 VIP 和端口信息编码到 IP Option 里。

原始包:
  IP: src=1.2.3.4, dst=10.0.0.100 (VIP)
  TCP: sport=54321, dport=80

LB 改写后:
  IP: src=1.2.3.4, dst=192.168.1.10 (Backend IP)
  IP Option: {VIP=10.0.0.100, VPort=80}
  TCP: sport=54321, dport=80

Backend 收到:
  从 IP Option 解码出 VIP 和 VPort
  回包:src=10.0.0.100:80 (VIP), dst=1.2.3.4:54321

这是 Cilium 的另一种 DSR dispatch 方式(loadBalancer.dsrDispatch=opt)。

# Cilium 启用 DSR + IP Option 模式
helm install cilium cilium/cilium \
  --set loadBalancer.mode=dsr \
  --set loadBalancer.dsrDispatch=opt

优点:

限制:

三种方式对比

维度 L2 DSR (MAC) L3 DSR (IPIP) L3 DSR (IP Option)
封装开销 0 字节 +20 字节 +8~12 字节
MTU 影响 无 需要调小内层 MTU 需要调小 MTU(幅度较小)
二层要求 必须同子网 无要求 无要求
Backend 配置 lo 加 VIP + ARP 抑制 隧道接口 + lo 加 VIP 无(Cilium 自动处理)
中间网络兼容性 好 好(IPIP 广泛支持) 差(IP Option 可能被丢弃)
实现代表 LVS DR, F5 LVS TUN, Cilium 默认 Cilium opt 模式
适用场景 传统数据中心同子网 跨子网 K8s 集群 网络设备兼容的 K8s 集群

四、Cilium DSR 模式深度拆解

4.1 整体架构

Cilium 的 DSR 实现完全基于 BPF 程序,不依赖 LVS/IPVS 内核模块。BPF 程序分别挂在入口节点和 Backend 节点的 TC hook 上,协作完成 DSR 的请求转发和回包改写。

整体流程:

1. Client 请求到达入口节点 eth0
2. TC ingress BPF 程序:
   - 查 Service Map,匹配 VIP:VPort
   - 选择 Backend(Maglev 或 Random)
   - 如果是 DSR 模式,不做 SNAT
   - 把 {ClientIP, ClientPort, VIP, VPort} 编码到包里(IPIP 或 Option)
   - 修改目的 IP 为 Backend IP
   - fib_lookup 查路由,redirect 出去
3. 包到达 Backend 节点 eth0
4. TC ingress BPF 程序:
   - 检测到 DSR 标记(IPIP 封装或 IP Option)
   - 解码 {ClientIP, ClientPort, VIP, VPort}
   - 存入 CT Map:key={BackendIP:BackendPort, ClientIP:ClientPort} -> val={VIP, VPort}
   - 剥掉封装,把包交给 Pod
5. Backend Pod 生成响应,回包经过 TC
6. TC egress/ingress BPF 程序(lxc 接口):
   - 查 CT Map,匹配到 DSR 条目
   - 把回包的源 IP 改为 VIP,源端口改为 VPort
   - 直接通过 fib_lookup 发给 Client,不经过入口节点

4.2 入口节点的 BPF 逻辑

入口节点的 TC ingress 程序是 DSR 的起点。下面是简化后的关键逻辑:

// 简化的 Cilium DSR 入口节点处理逻辑
// 位于 bpf/bpf_lxc.c 和 bpf/lib/lb.h

static __always_inline int
lb4_xlate(struct __ctx_buff *ctx, struct lb4_backend *backend,
          struct lb4_key *key, struct ct_state *state)
{
    // 查 Service Map,获取 Backend
    // key = {VIP, VPort, proto}
    // backend = {BackendIP, BackendPort, weight}

    if (lb_is_dsr(state)) {
        // DSR 模式:不做 SNAT
        // 编码原始客户端信息

        if (dsr_dispatch == DSR_DISPATCH_IPIP) {
            // IP-in-IP 封装:
            // 在原始包外面加一层 IP 头
            // outer src = 入口节点 IP
            // outer dst = Backend IP
            // inner 保持原始 {ClientIP -> VIP}
            ipip_encap(ctx, backend->address);
        } else {
            // IP Option 模式:
            // 修改 dst IP 为 Backend IP
            // 把 {VIP, VPort} 写入 IP Option
            set_dsr_opt(ctx, key->address, key->dport);
            ctx->dst = backend->address;
        }

        // fib_lookup 查路由
        fib_params.ifindex = 0;
        fib_params.ipv4_dst = backend->address;
        fib_lookup(ctx, &fib_params, sizeof(fib_params), 0);

        // redirect 到出口网卡
        return redirect(fib_params.ifindex, 0);
    }

    // 非 DSR 模式:正常 SNAT + DNAT
    // ...
}

核心区别在于 lb_is_dsr 分支:DSR 模式下不做 SNAT,而是编码原始信息后直接 redirect。

4.3 Backend 节点的 BPF 逻辑

Backend 节点的 TC ingress 程序负责解码 DSR 信息并存入 CT Map:

// 简化的 Cilium DSR Backend 节点处理逻辑

static __always_inline int
handle_dsr(struct __ctx_buff *ctx, struct iphdr *ip4, bool *dsr)
{
    struct dsr_opt_v4 opt;
    __be32 svc_addr;
    __be16 svc_port;

    if (dsr_dispatch == DSR_DISPATCH_IPIP) {
        // IP-in-IP 模式:检查协议号是否为 IPIP (4)
        if (ip4->protocol == IPPROTO_IPIP) {
            // 剥掉外层 IP 头,露出内层原始包
            // 内层 src = Client IP, dst = VIP
            ctx_adjust_hroom(ctx, -sizeof(struct iphdr), ...);
            svc_addr = inner_ip4->daddr;  // VIP
            svc_port = inner_tcp->dest;   // VPort
            *dsr = true;
        }
    } else {
        // IP Option 模式:解析 IP Option 字段
        if (parse_dsr_opt(ip4, &opt)) {
            svc_addr = opt.addr;  // VIP
            svc_port = opt.port;  // VPort
            *dsr = true;
        }
    }

    if (*dsr) {
        // 把 DSR 信息存入 CT Map
        // 这样回包时就知道要把源 IP 改为 VIP
        struct ct_entry ct = {
            .dsr = 1,
            .dsr_addr = svc_addr,
            .dsr_port = svc_port,
        };
        ct_create(ct_map, &ct, ...);
    }

    return CTX_ACT_OK;
}

4.4 回包改写

当 Backend Pod 发出响应时,包经过 lxc 接口的 TC BPF 程序:

// 简化的 DSR 回包改写逻辑

static __always_inline int
handle_reply(struct __ctx_buff *ctx, struct ct_entry *ct)
{
    if (ct->dsr) {
        // 查到 DSR 标记:改写源地址为 VIP
        struct iphdr *ip4 = ctx_data(ctx);

        // 源 IP: Pod IP -> VIP
        ip4->saddr = ct->dsr_addr;

        // 源端口: Pod Port -> VPort
        struct tcphdr *tcp = (void *)ip4 + sizeof(*ip4);
        tcp->source = ct->dsr_port;

        // 更新 checksum
        l4_csum_replace(ctx, ...);
        l3_csum_replace(ctx, ...);

        // fib_lookup 直接路由到 Client
        // 不经过入口节点
        fib_params.ipv4_dst = ip4->daddr;  // Client IP
        fib_lookup(ctx, &fib_params, ...);
        return redirect(fib_params.ifindex, 0);
    }

    // 非 DSR:正常回包路径(经过入口节点)
    // ...
}

这就是 DSR 的精髓:回包在 Backend 节点的 TC 层就被改写了源地址并直接路由出去,完全不经过入口节点。

4.5 CT Map 中的 DSR 条目

可以用 cilium bpf ct list global 查看 CT Map 中的 DSR 条目:

$ cilium bpf ct list global | grep DSR
TCP IN 10.244.1.5:8080 -> 1.2.3.4:54321 \
  expires=300 rx_packets=1 tx_packets=15 flags=DSR \
  dsr_addr=10.0.0.100 dsr_port=80

flags=DSR 表示这是一个 DSR 连接,dsr_addr 和 dsr_port 记录了回包时需要改写的源地址。


五、DSR 与 ExternalTrafficPolicy 的交互

ExternalTrafficPolicy: Local 回顾

在 Service 与 kube-proxy 那篇文章里,我们讨论过 externalTrafficPolicy: Local – 它要求 NodePort/LoadBalancer 流量只转发到本节点上的 Backend Pod,不做跨节点转发,从而保留客户端真实源 IP(不做 SNAT)。

apiVersion: v1
kind: Service
metadata:
  name: my-svc
spec:
  type: LoadBalancer
  externalTrafficPolicy: Local
  ports:
    - port: 80
      targetPort: 8080
  selector:
    app: my-app

DSR 与 Local 的关系

DSR 和 externalTrafficPolicy: Local 解决的是同一个问题的不同方面:

维度 externalTrafficPolicy: Local DSR
目标 保留客户端源 IP 优化回包路径
方法 不跨节点转发,不做 SNAT 跨节点转发,但回包直发
源 IP 保留 是(因为不做 SNAT) 是(Backend 直接用 VIP 回包)
负载均衡均匀性 差(只能用本节点 Pod) 好(可以用所有节点的 Pod)
可用性 差(本节点无 Pod 则不可用) 好(可以转发到任何有 Pod 的节点)

关键区别在于:Local 模式为了保留源 IP 而牺牲了负载均衡的均匀性和可用性;DSR 在保留源 IP 的同时,仍然允许跨节点负载均衡。

Cilium 中 DSR + Local 的行为

在 Cilium 中同时启用 DSR 和 externalTrafficPolicy: Local 时:如果本节点有 Backend Pod,直接转发(不需要 DSR 封装);如果没有,按 Local 策略返回不可用。DSR 在 Local 模式下实际不会被触发,因为 Local 已经阻止了跨节点转发。

所以 DSR 主要在 externalTrafficPolicy: Cluster(默认值)下发挥作用 – 允许跨节点转发的同时优化回包路径:

# DSR 最佳搭配
externalTrafficPolicy: Cluster  # 允许跨节点
# + Cilium DSR mode              # 回包直发
# + Maglev 一致性哈希              # 稳定的后端选择

六、DSR 的坑:你一定会踩的问题

6.1 MTU 问题

这是 DSR 最常见的坑。IP-in-IP 封装增加 20 字节的外层 IP 头,IP Option 增加 8-12 字节。如果原始包的大小已经接近 MTU,封装后就会超过 MTU。

标准以太网 MTU = 1500 字节
IP-in-IP 封装后:1500 + 20 = 1520 字节 -> 超过 MTU!

结果:
  - 如果包设置了 DF(Don't Fragment)位 -> 被丢弃,发送 ICMP need-frag
  - 如果没设置 DF -> 分片,性能下降
  - TCP MSS 协商不一定能覆盖这个问题(取决于封装发生在 TCP 握手之后还是之前)

解决方案:

# 方案一:调小节点 MTU
# 如果底层网络 MTU 是 1500,把 Pod 网络 MTU 设为 1480
helm install cilium cilium/cilium \
  --set mtu=1480 \
  --set loadBalancer.mode=dsr \
  --set loadBalancer.dsrDispatch=ipip

# 方案二:使用 IP Option 模式(封装开销更小)
helm install cilium cilium/cilium \
  --set loadBalancer.mode=dsr \
  --set loadBalancer.dsrDispatch=opt

# 方案三:如果底层网络支持 jumbo frame(MTU 9000)
# 就不需要特别调整

MTU 问题的详细讨论可以参考 MTU 的前世今生。

6.2 conntrack 不对称

DSR 的本质是请求和响应走不同的路径。入口节点只看到请求,看不到响应;Backend 节点能看到两个方向但 CT 条目与入口节点不同。

问题一:入口节点 CT 条目超时。 入口节点收不到响应包来刷新超时计时器。长连接(WebSocket、gRPC streaming)场景下,CT 条目可能在连接活跃时就超时了。Cilium 的处理是使用 BPF CT Map 默认超时(TCP 6 小时),并通过 CT Map 保证后续请求发往同一 Backend。

问题二:TCP RST/FIN 不可见。 Backend 发送的 RST/FIN 直接到客户端,入口节点看不到,CT 条目只能等超时释放。

# 监控 CT Map 使用率
cilium bpf ct list global | wc -l
# 如果条目数过多,调整上限
cilium config set bpf-ct-global-tcp-max 524288

6.3 Health Check 异常

负载均衡器向 NodePort 发健康检查请求时,如果请求被 DSR 转发到其他节点,回包绕过了 LB,健康检查失败。

LB (10.0.0.1) -> Node A:30080 -> DSR 转发到 Node B
Backend 回包 src=VIP -> LB,但 LB 期望来自 Node A 的响应 -> 检查失败

解决方案:使用 externalTrafficPolicy: Local(健康检查只到本地 Pod),或利用 Cilium 1.14+ 的 health check bypass 功能。当 externalTrafficPolicy=Local 时,K8s 会分配 healthCheckNodePort,返回本节点是否有可用 Backend。

6.4 源 IP 反欺骗(Anti-Spoofing)

DSR 的回包源 IP 是 VIP,不是 Backend 节点的真实 IP。一些云平台(AWS、GCP)的虚拟网络有反欺骗(anti-spoofing)规则,会丢弃源 IP 不属于当前实例的包。

# AWS:需要关闭源/目标检查
aws ec2 modify-instance-attribute \
  --instance-id i-1234567890abcdef0 \
  --no-source-dest-check

# GCP:需要启用 IP 转发
gcloud compute instances create my-instance \
  --can-ip-forward

6.5 IPv6 的限制

IPv6 没有 IP Option 字段(IPv6 的扩展头部是不同的机制)。Cilium 的 dsrDispatch=opt 模式在 IPv6 下使用的是 IPv6 Extension Header 来编码 DSR 信息,但一些网络设备对 Extension Header 的处理不佳。IPv6 场景下推荐使用 dsrDispatch=ipip(实际上是 IPv6-in-IPv6 封装,协议号 41)。


七、Maglev 一致性哈希 + DSR:完整高性能 LB 方案

7.1 为什么 Maglev 和 DSR 是天然搭档

DSR 解决了回包路径的问题,但还有一个问题:当有多个入口节点(多个 LB 节点)时,如何保证同一个连接的请求总是被转发到同一个 Backend?

如果入口节点 A 把某个连接的请求转发到 Backend 1,下一个请求到达入口节点 B(因为上游 ECMP 或 DNS 轮转),入口节点 B 可能把请求转发到 Backend 2。Backend 2 没有这个连接的 DSR CT 条目,回包时不知道该用哪个 VIP,连接就断了。

Maglev 一致性哈希解决了这个问题。Maglev 保证:只要 Backend 列表相同,所有入口节点对同一个五元组哈希值会选择同一个 Backend。即使请求到达不同的入口节点,结果是一致的。

入口节点 A:hash(client_ip, client_port, vip, vport, proto) % table_size -> Backend 1
入口节点 B:hash(client_ip, client_port, vip, vport, proto) % table_size -> Backend 1
入口节点 C:hash(client_ip, client_port, vip, vport, proto) % table_size -> Backend 1

关于 Maglev 算法的详细原理,可以参考 Cilium 深度拆解 中的相关章节。这里只聚焦它和 DSR 的配合。

7.2 完整方案:BGP ECMP + Maglev + DSR

一个生产级的高性能 LB 方案通常是这样的:

              Internet
                 |
            [上游路由器]
              / | \
           ECMP(BGP)
           /   |   \
      Node A  Node B  Node C    <- 所有节点通过 BGP 宣告 VIP
        |       |       |
     Maglev  Maglev  Maglev     <- 一致性哈希选 Backend
        |       |       |
     DSR     DSR     DSR        <- 回包直发客户端

每个节点通过 BGP 向上游路由器宣告 Service 的 VIP。上游路由器用 ECMP 在多个节点间分发流量。每个节点用 Maglev 一致性哈希选择 Backend。回包通过 DSR 直发客户端。

# Cilium 完整高性能 LB 配置
helm install cilium cilium/cilium \
  --set loadBalancer.mode=dsr \
  --set loadBalancer.dsrDispatch=ipip \
  --set loadBalancer.algorithm=maglev \
  --set maglev.tableSize=65521 \
  --set bgpControlPlane.enabled=true \
  --set kubeProxyReplacement=true \
  --set k8sServiceHost=${API_SERVER_IP} \
  --set k8sServicePort=6443

7.3 Maglev 表大小的选择

Maglev 查找表的大小是一个质数,影响 Backend 变化时的流量扰动:

表大小 内存开销(每 Service) Backend 变化时的最大扰动
251 ~1KB ~1/251 = 0.4%
8209 ~32KB ~1/8209 = 0.012%
65521 ~256KB ~1/65521 = 0.0015%

表越大,Backend 增减时受影响的连接越少,但内存开销越大。Cilium 默认使用 65521。

# 查看 Maglev 查找表
cilium service list -o json | jq '.[].backend_addresses'
# 每个 Service 的 Maglev 表存储在 BPF Map 中
cilium bpf lb list --frontends

7.4 Backend 变化时的行为

当一个 Backend 被移除时,Cilium Agent 重新计算 Maglev 表并原子更新 BPF Map。绝大多数连接不受影响 – 只有哈希到被移除 Backend 位置的连接需要重映射。而那些活跃连接如果入口节点有 CT 条目,会继续走旧映射直到超时。这就是 Maglev + DSR + CT 的三层保障。


八、Hybrid 模式:DSR 与 SNAT 的混合

纯 DSR 模式有时候不可行 – 比如某些云平台的反欺骗规则无法关闭,或者 UDP 流量不适合走 DSR(UDP 无连接概念,CT 条目基于超时容易过期,DSR 回包改写可能失效)。Cilium 提供了 Hybrid 模式:

# Hybrid 模式:TCP 走 DSR,UDP 走 SNAT
helm install cilium cilium/cilium \
  --set loadBalancer.mode=hybrid

TCP 流量通常是大体积响应(HTTP、gRPC),DSR 收益最大。UDP 常用于 DNS、健康检查等短小请求,请求和响应大小接近,SNAT 更简单可靠,回包路径优化收益有限。


九、实验:Cilium DSR 模式部署与抓包对比

9.1 环境准备

# 创建 kind 集群(3 节点)
cat <<EOF | kind create cluster --config=-
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
networking:
  disableDefaultCNI: true
  kubeProxyMode: none
nodes:
  - role: control-plane
  - role: worker
  - role: worker
EOF

# 安装 Cilium -- SNAT 模式(默认)
helm install cilium cilium/cilium \
  --namespace kube-system \
  --set kubeProxyReplacement=true \
  --set loadBalancer.mode=snat \
  --set k8sServiceHost=kind-control-plane \
  --set k8sServicePort=6443

9.2 部署测试应用

# 部署一个简单的 HTTP 服务
kubectl create deployment web --image=nginx --replicas=2
kubectl expose deployment web --type=NodePort --port=80

# 确认 Pod 分布
kubectl get pods -o wide
# NAME                   READY   STATUS    NODE
# web-xxx-aaa            1/1     Running   kind-worker
# web-xxx-bbb            1/1     Running   kind-worker2

# 确认 NodePort
kubectl get svc web
# NAME   TYPE       CLUSTER-IP    EXTERNAL-IP   PORT(S)
# web    NodePort   10.96.100.1   <none>        80:30080/TCP

9.3 SNAT 模式抓包

# 在 control-plane 上抓包,然后从外部访问 NodePort
kubectl exec -it cilium-xxxx -n kube-system -- \
  tcpdump -i eth0 -nn port 30080 or host 10.244.1.5
curl http://<control-plane-ip>:30080

# SNAT 模式下看到的包(所有包都经过入口节点 172.18.0.2):
# 10:00:01 IP 172.18.0.1.54321 > 172.18.0.2.30080: TCP SYN          <- 请求进来
# 10:00:01 IP 172.18.0.2.12345 > 10.244.1.5.80: TCP SYN             <- SNAT+DNAT 转发
# 10:00:01 IP 10.244.1.5.80 > 172.18.0.2.12345: TCP SYN-ACK         <- Backend 回包给入口
# 10:00:01 IP 172.18.0.2.30080 > 172.18.0.1.54321: TCP SYN-ACK      <- rev-NAT 回给 Client

9.4 切换到 DSR 模式

# 切换到 DSR 模式
helm upgrade cilium cilium/cilium \
  --namespace kube-system \
  --set kubeProxyReplacement=true \
  --set loadBalancer.mode=dsr \
  --set loadBalancer.dsrDispatch=ipip \
  --set k8sServiceHost=kind-control-plane \
  --set k8sServicePort=6443

# 等待 Cilium Agent 重启
kubectl rollout status ds/cilium -n kube-system

# 验证 DSR 已启用
kubectl exec -it cilium-xxxx -n kube-system -- \
  cilium status | grep "Load Balancer"
# KubeProxyReplacement: True
# ...LoadBalancer: DSR (IPIP)

9.5 DSR 模式抓包

# 在入口节点抓包(proto 4 = IPIP)
kubectl exec -it cilium-xxxx -n kube-system -- \
  tcpdump -i eth0 -nn 'port 30080 or proto 4'

# 在 Backend 节点抓包
kubectl exec -it cilium-yyyy -n kube-system -- \
  tcpdump -i eth0 -nn 'proto 4 or host 172.18.0.1'

curl http://<control-plane-ip>:30080

入口节点看到的包:

# 10:00:01 IP 172.18.0.1.54321 > 172.18.0.2.30080: TCP SYN               <- 请求进来
# 10:00:01 IP 172.18.0.2 > 172.18.0.3: IP-in-IP:                         <- IPIP 封装转发
#   IP 172.18.0.1.54321 > 10.96.100.1.80: TCP SYN
# ** 没有回包经过这里! **

Backend 节点看到的包:

# 10:00:01 IP 172.18.0.2 > 172.18.0.3: IP-in-IP:                         <- 收到封装请求
#   IP 172.18.0.1.54321 > 10.96.100.1.80: TCP SYN
# 10:00:01 IP 172.18.0.3.80 > 172.18.0.1.54321: TCP SYN-ACK              <- 直接回给 Client
# ** 回包没有经过入口节点 172.18.0.2! **

9.6 关键对比

SNAT 模式下请求和响应都经过入口节点(4 次 NAT)。DSR 模式下入口节点只处理请求(DNAT + IPIP 封装),响应直达客户端。实际 benchmark 中,DSR 模式入口节点 CPU 降低 30-50%,响应延迟减少一个网络跳跃 RTT(跨 AZ 约 0.5-2ms),入口节点出站带宽使用接近零。

9.7 验证 CT Map 中的 DSR 条目

# 在 Backend 节点的 Cilium Agent 中查看 CT Map
kubectl exec -it cilium-yyyy -n kube-system -- \
  cilium bpf ct list global | grep DSR

# 输出示例:
# TCP IN 10.244.1.5:80 -> 172.18.0.1:54321
#   expires=21595 rx=5 tx=8 flags=DSR
#   dsr_addr=10.96.100.1 dsr_port=80

# 确认 flags=DSR,表示这个连接的回包会做 DSR 改写
# dsr_addr=10.96.100.1 就是 Service 的 ClusterIP / VIP

十、生产部署检查清单

把前面的内容整理成一个实操检查清单:

10.1 部署前

# 1. 确认底层网络 MTU(如果是 1500 + IPIP dispatch,设 Cilium MTU 为 1480)
ip link show eth0 | grep mtu

# 2. 确认云平台反欺骗规则(AWS: 关 source/dest check, GCP: 启用 can-ip-forward)

# 3. 测试节点间 IPIP 连通性
ip tunnel add test0 mode ipip remote <other-node-ip> local <this-node-ip>
ip link set test0 up && ping -c3 -I test0 <other-node-ip>
ip tunnel del test0

10.2 部署与验证

# 推荐配置
helm install cilium cilium/cilium \
  --namespace kube-system \
  --set kubeProxyReplacement=true \
  --set loadBalancer.mode=dsr \
  --set loadBalancer.dsrDispatch=ipip \
  --set loadBalancer.algorithm=maglev \
  --set maglev.tableSize=65521 \
  --set bpf.masquerade=true \
  --set mtu=1480 \
  --set k8sServiceHost=${API_SERVER_IP} \
  --set k8sServicePort=6443

# 验证 DSR 已启用
kubectl exec -it $(kubectl get pod -n kube-system -l k8s-app=cilium \
  -o name | head -1) -n kube-system -- cilium status --verbose \
  | grep -A5 "KubeProxyReplacement"

# 监控关键指标
kubectl exec -it cilium-xxxx -n kube-system -- \
  cilium bpf ct list global | wc -l        # CT Map 条目数
kubectl exec -it cilium-xxxx -n kube-system -- \
  cilium metrics list | grep bpf_map_pressure  # Map 压力

10.3 故障排查速查表

现象 可能原因 排查命令
请求超时,无响应 MTU 问题导致封装包被丢弃 tcpdump -i eth0 'icmp and icmp[0]=3'(看 ICMP need-frag)
健康检查失败 DSR 回包绕过了 LB 的检查 检查 externalTrafficPolicy 和 healthCheckNodePort
间歇性连接断开 CT 条目超时,Maglev 重映射 cilium bpf ct list global \| grep DSR,检查 expires
回包被云平台丢弃 源 IP 反欺骗 检查实例的 source/dest check 设置
部分节点 DSR 不工作 BPF 程序未正确加载 cilium status,cilium bpf prog list
TCP RST 异常 conntrack 不对称导致的状态不一致 在入口节点和 Backend 节点同时抓包对比

十一、DSR 在更广泛的负载均衡架构中的位置

传统数据中心:LVS + DSR

在 Kubernetes 之前,DSR 最广泛的应用场景是 LVS(Linux Virtual Server)。LVS 的三种转发模式中,DR(L2 DSR)和 TUN(L3 DSR / IPIP)都是 DSR 实现:

# LVS DR 模式(L2 DSR)-- -g = gatewaying
ipvsadm -A -t 10.0.0.100:80 -s rr
ipvsadm -a -t 10.0.0.100:80 -r 192.168.1.10:80 -g

# LVS TUN 模式(L3 DSR / IPIP)-- -i = ipip
ipvsadm -A -t 10.0.0.100:80 -s rr
ipvsadm -a -t 10.0.0.100:80 -r 192.168.1.10:80 -i

LVS 的 DSR 依赖手动配置 Backend 的 loopback VIP 和 ARP 抑制。在 Kubernetes 的动态 Pod 环境下极其不实际 – 这正是 Cilium BPF DSR 的价值所在:把传统 LVS DSR 的手动配置全部自动化。

云原生演进路径

LVS DR (L2 DSR) -> 限制:同子网
  -> LVS TUN (L3 DSR / IPIP) -> 限制:Backend 手动配 VIP + 隧道
    -> Cilium BPF DSR -> 自动编解码、自动 CT、自动回包改写、集成 Maglev + BGP

与 MetalLB 的对比

MetalLB 是 bare-metal 集群常见的 LoadBalancer 实现,但它本身不实现 DSR。MetalLB BGP 模式宣告 VIP 后,流量到达节点仍走 kube-proxy 的 SNAT 路径。如果想要 DSR,可以用 MetalLB BGP + Cilium kube-proxy replacement(DSR 模式)组合,或者更简单地直接用 Cilium 的 BGP Control Plane 一套方案解决 VIP 宣告和 DSR。


十二、总结

DSR 解决了 Service 负载均衡中回包路径的性能问题。核心思路是让回包直接从 Backend 发给客户端,绕过入口节点。

三种实现方式的选择:

Cilium 的 BPF DSR 把传统 LVS DSR 的手动配置全部自动化了。入口节点的 TC BPF 程序编码 DSR 信息,Backend 节点的 TC BPF 程序解码并存入 CT Map,回包时查 CT Map 改写源地址并直接路由出去。

Maglev + DSR 是完整方案的两个核心组件:Maglev 保证多入口节点下的一致性哈希,DSR 优化回包路径。配合 BGP ECMP,构成高性能、水平可扩展的 LB 架构。

DSR 的坑集中在 MTU(IPIP 封装增加 20 字节)、conntrack 不对称(入口节点看不到回包)、健康检查异常和云平台反欺骗规则。部署前必须逐一确认。

DSR 不是银弹 – 对于请求和响应大小接近的场景(如 gRPC 双向流),收益有限。对于响应远大于请求的场景(HTTP API、文件下载、视频流),收益显著。根据流量特征选择 DSR、SNAT 或 Hybrid 模式。

下一篇我们将从网络可观测性的角度,讨论如何用 Hubble、Cilium Monitor、eBPF tracing 等工具来观测和诊断集群网络问题。


系列导航 - 上一篇:MTU 调优 - 返回目录:Kubernetes 网络深度系列 - 下一篇:网络可观测性

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。


By .