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

DSR(Direct Server Return):让回包绕过负载均衡器

目录

上一篇我们聊了 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 地址,然后在同一个二层网络里直接转发。

Client 发出:
  L2: src=client-MAC, dst=LB-MAC
  L3: src=1.2.3.4, dst=10.0.0.100 (VIP)

LB 转发(只改 MAC):
  L2: src=LB-MAC, dst=Backend-MAC
  L3: src=1.2.3.4, dst=10.0.0.100 (VIP)  <-- IP 头完全不动

Backend 收到:
  目的 IP 是 VIP,VIP 配在 loopback 上,内核接收
  回包:src=10.0.0.100 (VIP), dst=1.2.3.4
  直接发给 Client,不经过 LB

优点:

限制:

这是 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_addrdsr_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 的检查 检查 externalTrafficPolicyhealthCheckNodePort
间歇性连接断开 CT 条目超时,Maglev 重映射 cilium bpf ct list global \| grep DSR,检查 expires
回包被云平台丢弃 源 IP 反欺骗 检查实例的 source/dest check 设置
部分节点 DSR 不工作 BPF 程序未正确加载 cilium statuscilium 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 等工具来观测和诊断集群网络问题。


By .