上一篇我们聊了 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 的回包直接发给客户端,绕过入口节点。入口节点只处理请求方向的流量,响应走最短路径直达客户端。
这篇文章的目标:
- 剖析 SNAT 模式的瓶颈
- 拆解 DSR 的三种实现方式
- 深入 Cilium DSR 的 BPF 实现
- 讨论 DSR 与 ExternalTrafficPolicy 的交互
- 列举 DSR 的常见坑和排查方法
- 构建 Maglev + DSR 的完整高性能 LB 方案
- 实验对比 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 成本不可忽视。
现实案例:大流量场景的痛点
一个典型案例:视频流服务,请求约 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
优点:
- 零封装开销,不增加任何字节
- LB 处理极快,只做 MAC 改写 + 转发
- 包大小不变,无 MTU 问题
限制:
- LB 和所有 Backend 必须在同一个二层网络(同一 VLAN/子网)
- 不适用于跨子网的 Kubernetes 集群
- Backend 必须在 loopback 配置 VIP 并抑制 ARP
这是 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优点:
- 不要求二层可达,支持跨子网、跨 AZ
- 客户端原始信息完整保留在内层 IP
限制:
- 外层 IP 头增加 20 字节,有 MTU 问题(后面详细讨论)
- Backend 需要配置 IPIP 隧道接口并在 loopback 上配 VIP(LVS 场景)
- Cilium 场景下 BPF 自动处理,不需要手动配置
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优点:
- 封装开销小(IP Option 只增加 8-12 字节,比 IPIP 的 20 字节少)
- 不需要在 Backend 配置隧道接口
- 包的外层目的 IP 就是 Backend IP,路由更直接
限制:
- 一些网络设备(特别是云平台的虚拟交换机)会丢弃带 IP Option 的包
- IP Option 会影响部分网络设备的快速转发路径(fast path),可能降低中间网络设备的处理性能
- 仍然有 MTU 问题(虽然比 IPIP 小)
三种方式对比
| 维度 | 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=80flags=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-appDSR 与 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 5242886.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-forward6.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=64437.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 --frontends7.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=hybridTCP 流量通常是大体积响应(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=64439.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/TCP9.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 回给 Client9.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 test010.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 -iLVS 的 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 发给客户端,绕过入口节点。
三种实现方式的选择:
- L2 DSR(MAC 改写):性能最好,但要求同一二层网络,在 Kubernetes 中基本不可用
- L3 DSR / IPIP:跨子网可用,Cilium 默认选择,封装开销 20 字节
- L3 DSR / IP Option:封装开销更小,但网络设备兼容性差
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 等工具来观测和诊断集群网络问题。