从零构建 eBPF/XDP 二层直连返回负载均衡器技术教程
一、概述
1. 简介
A. 是什么
本教程介绍如何使用 eBPF 和 XDP(eXpress Data Path)从零开始构建一个二层(Layer 2)Direct Server Return(DSR)负载均衡器。DSR 是一种高性能负载均衡技术,允许后端服务器直接响应客户端请求,绕过负载均衡器的回程路径。
B. 为什么学
- 理解现代高性能负载均衡的核心原理
- 掌握 eBPF/XDP 在网络编程中的应用
- 学习 DSR 技术如何解决传统 NAT 负载均衡的性能瓶颈
- 深入理解二层网络转发机制
C. 学完能做什么
- 使用 eBPF/XDP 编写二层负载均衡器
- 配置虚拟 IP(VIP)实现 DSR 架构
- 理解 MAC 地址重写与 IP 转发的区别
- 部署高性能、低延迟的负载均衡解决方案
2. 前置知识
A. 必备技能
- Linux 网络基础(IP、MAC、ARP 协议)
- 基本 C 语言编程能力
- 网络命令行工具使用(ip、tcpdump、curl)
B. 推荐知识
- eBPF 和 XDP 基础概念
- NAT 负载均衡原理
- 二层网络与三层网络的区别
二、背景知识
1. NAT 负载均衡的局限性
在之前的教程中,我们构建了基于 NAT 的 XDP 负载均衡器。虽然这种方案可以正常工作,但存在以下问题:
A. 资源消耗
- 负载均衡器需要处理双向流量(请求和响应)
- 入站流量通常远小于出站流量
- 例如:搜索查询或 AI 提示只有几字节,但响应可能有几 KB 甚至更多
B. 性能瓶颈
- 负载均衡器成为网络瓶颈
- 高流量场景下资源消耗显著
C. 客户端信息丢失
- 后端服务器无法看到真实客户端 IP
- NAT 负载均衡器重写了数据包头
- 后端无法基于源 IP 进行会话管理或日志记录
2. DSR 解决方案
Direct Server Return(DSR)概念被引入以克服上述限制。DSR 有多种实现方式:
- Layer 2 DSR(本教程重点)
- IP-in-IP 封装
- GRE(Generic Routing Encapsulation)封装
- 基于 IP 或 TCP 头部字段的变体
三、网络拓扑
1. 实验环境
本教程使用一个包含五个节点的网络拓扑,分布在两个不同的网络中:
节点列表:
- lb: 192.168.178.10/24(负载均衡器)
- backend-01: 192.168.178.11/24(后端服务器 1)
- backend-02: 192.168.178.12/24(后端服务器 2)
- client: 10.0.0.20/16(客户端)
- gateway: 192.168.178.2/24 和 10.0.0.2/16(网关)2. 架构图
graph TB
C[客户端<br/>10.0.0.20/16] -->|请求| GW[网关<br/>192.168.178.2<br/>10.0.0.2]
GW -->|转发| LB[负载均衡器<br/>192.168.178.10<br/>VIP: 192.168.178.15]
LB -->|MAC重写| B1[后端服务器1<br/>192.168.178.11<br/>VIP on lo]
LB -->|MAC重写| B2[后端服务器2<br/>192.168.178.12<br/>VIP on lo]
B1 -->|直接响应| C
B2 -.->|直接响应| C四、核心概念
1. DSR L2 负载均衡原理
与 NAT 负载均衡不同,DSR 保留原始客户端 IP,并允许后端服务器直接响应客户端,绕过负载均衡器的回程路径。
为了理解这是如何实现的,我们需要回答三个核心问题:
后端如何看到客户端 IP?
- 数据包通过负载均衡器时,IP 头部保持不变
- 只有 MAC 地址被重写
后端知道响应哪个客户端吗?
- 后端看到原始客户端 IP,可以正常响应
如何确保客户端接收来自同一 IP 的响应?
- 通过虚拟 IP(VIP)实现,负载均衡器和后端共享同一 IP
2. 虚拟 IP(VIP)
A. 什么是 VIP
在任何网络设置中,当客户端向特定端点发送请求时,期望响应来自相同的 IP 地址。如果响应来自不同的 IP,客户端的网络栈会认为出错并丢弃数据包。
虚拟 IP(Virtual IP,VIP)不是绑定到特定接口或节点的物理 IP,而是由多个节点共享的地址,用于处理同一服务的流量。
B. VIP 配置
在负载均衡器(lb 节点)上配置 VIP:
sudo ip addr add 192.168.178.15/32 dev eth0在后端服务器(backend-01 和 backend-02)上配置 VIP:
sudo ip addr add 192.168.178.15/32 dev loC. 为什么后端使用 lo 接口
两个节点配置相同的 IP 不会混淆客户端或网关吗?这正是我们将 VIP 分配到后端节点的 lo 接口的原因。我们不希望后端广播虚拟 IP(通过 ARP)。
换句话说,网络中没人应该知道 VIP 存在于后端节点上,否则客户端可能绕过负载均衡器直接连接。
D. ARP 配置
为防止后端节点响应 lo 接口上 VIP 地址的 ARP 请求(即广播),在每个后端节点上运行以下命令:
# 仅当目标 IP 分配给接收请求的接口时才响应 ARP 请求
# (防止在 eth0 上广播 VIP)
sudo sysctl -w net.ipv4.conf.eth0.arp_ignore=1
# 发送 ARP 请求时,仅使用分配给出接口的地址
# (防止节点在 ARP 中泄露 VIP 作为源 IP)
sudo sysctl -w net.ipv4.conf.eth0.arp_announce=23. 二层转发原理
负载均衡器不需要知道后端节点上存在 VIP。它只需要后端的 MAC 地址在二层转发数据包。当后端接收到数据包时,它解封装并识别虚拟 IP 为自己的 IP(配置在 lo 接口上),即使该 IP 从未在网络上广播。
这也正是这个概念被称为二层 DSR 的原因——负载均衡器仅使用后端的 MAC 地址到达后端节点,如果后端位于不同的(二层)网络中,这将无法工作。
sequenceDiagram
participant C as 客户端
participant G as 网关
participant L as 负载均衡器
participant B as 后端服务器
C->>G: 请求 VIP (192.168.178.15)
G->>L: 转发到负载均衡器
Note over L: 1. 哈希选择后端<br/>2. 重写 MAC 地址<br/>3. IP 头保持不变
L->>B: 转发(MAC 重写,源 IP 保留)
Note over B: 识别 VIP 为自己的地址
B->>C: 直接响应(绕过负载均衡器)五、实现步骤
1. 准备工作
A. 启用 IP 转发
在负载均衡器(lb 节点)上启用 IP 转发并填充 ARP 表:
sudo sysctl -w net.ipv4.ip_forward=1
# ping 后端节点以填充 ARP 表(bpf_fib_lookup 需要此步骤)
sudo ping -c1 192.168.178.11 # backend-01 节点/真实 IP
sudo ping -c1 192.168.178.12 # backend-02 节点/真实 IPB. 启动 HTTP 服务器
在两个后端服务器(backend-01 和 backend-02)上启动 HTTP 服务器,显式绑定到 VIP:
python3 -m http.server 8000 --bind 192.168.178.152. XDP 负载均衡器代码
A. 核心 MAC 地址重写逻辑
在二层 DSR 负载均衡中,我们只需要在负载均衡器中更新 MAC 地址,以便数据包在二层正确传递:
// 使用简单哈希选择后端
struct four_tuple_t four_tuple;
four_tuple.src_ip = ip->saddr;
four_tuple.dst_ip = ip->daddr;
four_tuple.src_port = tcp->source;
four_tuple.dst_port = tcp->dest;
four_tuple.protocol = IPPROTO_TCP;
__u32 key = xdp_hash_tuple(&four_tuple) % NUM_BACKENDS;
struct endpoint *backend = bpf_map_lookup_elem(&backends, &key);
if (!backend) {
return XDP_ABORTED;
}
// 执行 FIB 查找
struct bpf_fib_lookup fib = {};
int rc = fib_lookup_v4_full(ctx, &fib, ip->daddr, backend->ip,
bpf_ntohs(ip->tot_len));
if (rc != BPF_FIB_LKUP_RET_SUCCESS) {
log_fib_error(rc);
return XDP_ABORTED;
}
// 我们只需要更新 MAC 地址
// 后端需要在 lo 接口上有虚拟 IP(与负载均衡器相同)
// 源 IP 保留为客户端 IP,因此后端将直接响应客户端
__builtin_memcpy(eth->h_source, fib.smac, ETH_ALEN);
__builtin_memcpy(eth->h_dest, fib.dmac, ETH_ALEN);B. 关键要点
无需连接跟踪
- 负载均衡器不需要维护任何连接跟踪状态(与 NAT 负载均衡不同)
- 简单地基于 MAC 地址重定向数据包
保留 IP 头部
- 负载均衡器甚至不接触 IP 头部
- 后端仍接收原始客户端源 IP
- 后端可以直接回复客户端,绕过负载均衡器
FIB 查找
- 通过 bpf_fib_lookup 获取 MAC 地址
- 使用 fib_lookup_v4_full 执行完整查找
3. 编译和运行
A. 编译负载均衡器
cd lab
go generate
go buildB. 运行负载均衡器
sudo ./lb -i eth0 --backends 192.168.178.11,192.168.178.12参数说明:
-i eth0: 指定网络接口--backends: 后端服务器真实 IP 列表(逗号分隔)
C. 测试负载均衡
从客户端节点查询 VIP:
curl http://192.168.178.15:8000D. 验证流量
在后端服务器上查看 HTTP 服务器日志,确认请求确实来自客户端 IP:
10.0.0.20 - - [01/Jan/2026 16:03:45] "GET / HTTP/1.1" 200 -E. 抓包验证 MAC 地址
在两个后端服务器上运行 tcpdump 查看 MAC 地址:
sudo tcpdump -i eth0 -n -t -e -q tcp port 8000预期输出(简化版):
LB_MAC > BACKEND_MAC, IPv4, length 74: CLIENT_IP/PORT > BACKEND_IP/PORT: tcp 0
# 负载均衡器 -> 后端(保留客户端源 IP)
BACKEND_MAC > GATEWAY_MAC, IPv4, length 74: BACKEND_IP/PORT > CLIENT_IP/PORT: tcp 0
# 后端 -> 网关(客户端 IP 为目标地址)F. 查看负载均衡器日志
sudo bpftool prog trace六、技术限制
1. 二层网络要求
DSR L2 负载均衡仅在负载均衡器和后端服务器位于同一子网(即具有直接二层连接)时才能正常工作。
如果负载均衡器尝试将流量重定向到不同二层网络中的后端:
- 负载均衡器会将目标 MAC 地址设置为网关接口(试图退出当前二层网络)
- 由于只有负载均衡器上的 VIP 在网络上广播
- 网关会将数据包发回负载均衡器——因为这是它知道的该 VIP 的唯一路由
- 导致环路
graph TB
LB[负载均衡器] -->|目标MAC=网关| GW[网关]
GW -->|VIP路由回LB| LB
LB -.环路.-> GW
style LB fill:#f9f,stroke:#333,stroke-width:2px
style GW fill:#ff9,stroke:#333,stroke-width:2px2. 架构约束
- 要求后端服务器与负载均衡器共享同一网络
- 增加整体故障风险——网络故障导致全部服务不可用
3. 解决方案
这正是 IPIP DSR 负载均衡发挥作用的地方,将在后续教程中介绍。
七、总结
1. 关键要点
A. DSR 优势
- 后端服务器直接响应客户端,绕过负载均衡器
- 保留原始客户端 IP,便于会话管理和日志记录
- 减轻负载均衡器负担,提高整体性能
B. 二层 DSR 实现原理
- 仅重写 MAC 地址,IP 头部保持不变
- 通过虚拟 IP(VIP)实现 IP 一致性
- 后端在 lo 接口配置 VIP,不参与 ARP 广播
C. 技术限制
- 负载均衡器与后端必须在同一二层网络
- 跨网络场景需要使用 IPIP 或 GRE 等封装技术
2. 下一步学习
- IPIP DSR 负载均衡(跨网络场景)
- GRE DSR 负载均衡
- 高级哈希算法与一致性哈希
- 健康检查与故障转移