上一篇讲了 DPDK 的内核旁路方案——完全绕过内核,在用户态直接操作网卡。DPDK 性能极致,但代价是放弃了整个 Linux 网络生态。XDP(eXpress Data Path)提供了另一条路:在内核中处理包,但在网络栈之前——既利用内核生态,又获得接近 DPDK 的性能。
一、XDP 的设计定位
1.1 在协议栈中的位置
数据包接收路径(传统 vs XDP):
传统路径:
网卡 → DMA → Ring Buffer → 硬中断 → NAPI 软中断
→ sk_buff 分配 → Netfilter → 路由 → Socket → 用户态
↑
全部处理完才到
XDP 路径:
网卡 → DMA → Ring Buffer → 驱动收包回调
↓
XDP 程序执行 ← 这里!比 sk_buff 分配还早
↓
┌─────────┼─────────────┐
↓ ↓ ↓
XDP_DROP XDP_PASS XDP_REDIRECT
(直接丢弃) (进入正常栈) (重定向到其他接口/AF_XDP)
XDP 的关键设计选择:
- 位置极早:在
sk_buff分配之前,避免了最昂贵的内存分配操作 - 使用 eBPF:安全的沙箱化程序,内核验证器保证安全
- 不绕过内核:XDP 程序在内核中运行,可以与内核栈协同
- 按需处理:只在有包时执行,不像 DPDK 持续轮询
1.2 XDP 动作码
XDP 程序的返回值决定了数据包的命运:
/* XDP 动作码 */
enum xdp_action {
XDP_ABORTED = 0, /* 程序异常,丢弃并记录错误 */
XDP_DROP = 1, /* 丢弃包(最快的丢包方式) */
XDP_PASS = 2, /* 交给正常网络栈处理 */
XDP_TX = 3, /* 从收到包的网卡原路发回 */
XDP_REDIRECT = 4, /* 重定向到其他网卡/CPU/AF_XDP */
};| 动作 | 含义 | 典型场景 |
|---|---|---|
| XDP_DROP | 立即丢弃,不进入协议栈 | DDoS 防御、ACL 过滤 |
| XDP_PASS | 正常进入内核网络栈 | 不需要 XDP 处理的包 |
| XDP_TX | 修改后从同一网卡发回 | 负载均衡(DSR 模式) |
| XDP_REDIRECT | 重定向到其他网卡或 AF_XDP | 转发、用户态收包 |
| XDP_ABORTED | 异常路径,丢弃并触发 tracepoint | 调试 |
1.3 三种执行模式
# 1. Native XDP(原生模式)- 性能最高
# 需要网卡驱动支持 XDP hook
# 在驱动的 NAPI 收包回调中执行
ip link set dev eth0 xdpdrv obj xdp_prog.o sec xdp
# 2. Offloaded XDP(卸载模式)- 最快
# XDP 程序卸载到网卡硬件执行(如 Netronome)
# CPU 零开销,但支持的指令集有限
ip link set dev eth0 xdpoffload obj xdp_prog.o sec xdp
# 3. Generic XDP(通用模式)- 任何网卡都支持
# 在内核网络栈的 netif_receive_skb() 之后执行
# 性能最差,主要用于开发和测试
ip link set dev eth0 xdpgeneric obj xdp_prog.o sec xdp
# 查看当前 XDP 程序
ip link show dev eth0
# 输出示例:
# 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> ...
# prog/xdp id 42 tag abc123
# 卸载 XDP 程序
ip link set dev eth0 xdp off| 模式 | 执行位置 | 性能 | 驱动要求 | 适用场景 |
|---|---|---|---|---|
| Native | 驱动层 | ~25 Mpps | 需要支持 | 生产环境 |
| Offloaded | 网卡硬件 | 线速 | 特定网卡 | 极致性能 |
| Generic | 网络栈 | ~5 Mpps | 任意 | 开发测试 |
二、XDP 程序编写实战
2.1 环境准备
# 安装开发依赖
# Ubuntu/Debian
apt install -y clang llvm libelf-dev libbpf-dev \
linux-headers-$(uname -r) bpftool
# 编译 XDP 程序
# XDP 程序用 C 编写,通过 clang 编译为 eBPF 字节码
clang -O2 -g -target bpf -c xdp_prog.c -o xdp_prog.o2.2 最简 XDP 程序:丢弃所有包
/* xdp_drop.c - 丢弃所有数据包 */
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
SEC("xdp")
int xdp_drop_all(struct xdp_md *ctx) {
/* ctx->data: 包数据起始指针
* ctx->data_end: 包数据结束指针
* ctx->ingress_ifindex: 入接口索引
* ctx->rx_queue_index: 接收队列索引
*/
return XDP_DROP;
}
char _license[] SEC("license") = "GPL";2.3 DDoS 防御:SYN Flood 过滤
/* xdp_synflood.c - SYN Flood 防御 */
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <linux/in.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
/* 每个源 IP 的 SYN 计数器 */
struct {
__uint(type, BPF_MAP_TYPE_LRU_HASH);
__uint(max_entries, 1000000);
__type(key, __u32); /* 源 IP */
__type(value, __u64); /* SYN 计数 */
} syn_count SEC(".maps");
/* 黑名单 */
struct {
__uint(type, BPF_MAP_TYPE_LRU_HASH);
__uint(max_entries, 100000);
__type(key, __u32); /* 源 IP */
__type(value, __u64); /* 封禁时间戳 */
} blacklist SEC(".maps");
#define SYN_THRESHOLD 100 /* 每秒 SYN 包阈值 */
SEC("xdp")
int xdp_syn_filter(struct xdp_md *ctx) {
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
/* 解析以太网头 */
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end)
return XDP_PASS;
if (eth->h_proto != bpf_htons(ETH_P_IP))
return XDP_PASS;
/* 解析 IP 头 */
struct iphdr *ip = (void *)(eth + 1);
if ((void *)(ip + 1) > data_end)
return XDP_PASS;
if (ip->protocol != IPPROTO_TCP)
return XDP_PASS;
/* 解析 TCP 头 */
struct tcphdr *tcp = (void *)ip + (ip->ihl * 4);
if ((void *)(tcp + 1) > data_end)
return XDP_PASS;
__u32 src_ip = ip->saddr;
/* 检查黑名单 */
__u64 *blocked = bpf_map_lookup_elem(&blacklist, &src_ip);
if (blocked) {
__u64 now = bpf_ktime_get_ns();
/* 封禁 60 秒 */
if (now - *blocked < 60000000000ULL)
return XDP_DROP;
/* 封禁过期,移除 */
bpf_map_delete_elem(&blacklist, &src_ip);
}
/* 只检查 SYN 包(非 SYN-ACK) */
if (!tcp->syn || tcp->ack)
return XDP_PASS;
/* 计数 */
__u64 *count = bpf_map_lookup_elem(&syn_count, &src_ip);
if (count) {
(*count)++;
if (*count > SYN_THRESHOLD) {
/* 超过阈值,加入黑名单 */
__u64 now = bpf_ktime_get_ns();
bpf_map_update_elem(&blacklist, &src_ip, &now, BPF_ANY);
return XDP_DROP;
}
} else {
__u64 init = 1;
bpf_map_update_elem(&syn_count, &src_ip, &init, BPF_ANY);
}
return XDP_PASS;
}
char _license[] SEC("license") = "GPL";2.4 负载均衡器(XDP_TX 模式)
/* xdp_lb.c - 基于 XDP 的简单 L4 负载均衡器 */
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <linux/udp.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
/* 后端服务器列表 */
struct backend {
__u32 ip;
unsigned char mac[ETH_ALEN];
};
struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__uint(max_entries, 16);
__type(key, __u32);
__type(value, struct backend);
} backends SEC(".maps");
/* VIP 配置 */
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 64);
__type(key, __u32); /* VIP */
__type(value, __u32); /* 后端数量 */
} vips SEC(".maps");
/* 统计 */
struct {
__uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
__uint(max_entries, 4);
__type(key, __u32);
__type(value, __u64);
} stats SEC(".maps");
#define STAT_RX 0
#define STAT_TX 1
#define STAT_DROP 2
#define STAT_PASS 3
static __always_inline void update_stat(__u32 key) {
__u64 *val = bpf_map_lookup_elem(&stats, &key);
if (val) (*val)++;
}
/* 简单的哈希函数选择后端 */
static __always_inline __u32 hash_flow(struct iphdr *ip, __u32 n_backends) {
return (ip->saddr ^ ip->daddr) % n_backends;
}
/* 重新计算 IP 校验和(增量更新) */
static __always_inline void update_ip_csum(struct iphdr *ip,
__u32 old_ip, __u32 new_ip) {
__u32 csum = (~ip->check & 0xFFFF);
csum += (~old_ip & 0xFFFF) + (~(old_ip >> 16) & 0xFFFF);
csum += (new_ip & 0xFFFF) + ((new_ip >> 16) & 0xFFFF);
csum = (csum >> 16) + (csum & 0xFFFF);
csum += csum >> 16;
ip->check = ~csum;
}
SEC("xdp")
int xdp_loadbalancer(struct xdp_md *ctx) {
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
update_stat(STAT_RX);
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end)
return XDP_DROP;
if (eth->h_proto != bpf_htons(ETH_P_IP))
return XDP_PASS;
struct iphdr *ip = (void *)(eth + 1);
if ((void *)(ip + 1) > data_end)
return XDP_DROP;
/* 检查目标 IP 是否是 VIP */
__u32 *n_backends = bpf_map_lookup_elem(&vips, &ip->daddr);
if (!n_backends || *n_backends == 0) {
update_stat(STAT_PASS);
return XDP_PASS;
}
/* 选择后端 */
__u32 idx = hash_flow(ip, *n_backends);
struct backend *be = bpf_map_lookup_elem(&backends, &idx);
if (!be) {
update_stat(STAT_DROP);
return XDP_DROP;
}
/* 修改目标 IP */
__u32 old_daddr = ip->daddr;
ip->daddr = be->ip;
update_ip_csum(ip, old_daddr, be->ip);
/* 修改 MAC 地址 */
__builtin_memcpy(eth->h_dest, be->mac, ETH_ALEN);
/* 源 MAC 设为 LB 的 MAC(需要从 map 中获取或硬编码) */
update_stat(STAT_TX);
return XDP_TX; /* 从同一网卡发回 */
}
char _license[] SEC("license") = "GPL";2.5 用户态加载程序
/* xdp_loader.c - 加载和管理 XDP 程序 */
#include <bpf/libbpf.h>
#include <bpf/bpf.h>
#include <net/if.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>
static int ifindex;
static int prog_fd = -1;
void cleanup(int sig) {
(void)sig;
if (prog_fd >= 0) {
bpf_xdp_detach(ifindex, 0, NULL);
printf("XDP program detached\n");
}
exit(0);
}
int main(int argc, char *argv[]) {
if (argc < 3) {
fprintf(stderr, "Usage: %s <ifname> <xdp_prog.o>\n", argv[0]);
return 1;
}
const char *ifname = argv[1];
const char *progfile = argv[2];
ifindex = if_nametoindex(ifname);
if (!ifindex) {
fprintf(stderr, "Interface %s not found\n", ifname);
return 1;
}
signal(SIGINT, cleanup);
signal(SIGTERM, cleanup);
/* 打开 BPF 对象文件 */
struct bpf_object *obj = bpf_object__open(progfile);
if (!obj) {
fprintf(stderr, "Failed to open %s\n", progfile);
return 1;
}
/* 加载到内核 */
if (bpf_object__load(obj)) {
fprintf(stderr, "Failed to load BPF object\n");
return 1;
}
/* 找到 XDP 程序 */
struct bpf_program *prog = bpf_object__find_program_by_name(
obj, "xdp_loadbalancer");
if (!prog) {
fprintf(stderr, "XDP program not found\n");
return 1;
}
prog_fd = bpf_program__fd(prog);
/* 附加到网卡 */
struct bpf_xdp_attach_opts opts = {
.sz = sizeof(opts),
};
if (bpf_xdp_attach(ifindex, prog_fd, 0, &opts)) {
fprintf(stderr, "Failed to attach XDP: %s\n", strerror(errno));
return 1;
}
printf("XDP program attached to %s (ifindex %d)\n", ifname, ifindex);
/* 配置后端(示例) */
int backends_fd = bpf_object__find_map_fd_by_name(obj, "backends");
int vips_fd = bpf_object__find_map_fd_by_name(obj, "vips");
/* 添加 VIP */
__u32 vip = 0x0A000001; /* 10.0.0.1 */
__u32 n_backends = 2;
bpf_map_update_elem(vips_fd, &vip, &n_backends, BPF_ANY);
/* 添加后端 */
struct backend {
__u32 ip;
unsigned char mac[6];
};
struct backend be0 = {
.ip = 0x0A000064, /* 10.0.0.100 */
.mac = {0x00, 0x11, 0x22, 0x33, 0x44, 0x55}
};
__u32 idx = 0;
bpf_map_update_elem(backends_fd, &idx, &be0, BPF_ANY);
/* 定期打印统计 */
int stats_fd = bpf_object__find_map_fd_by_name(obj, "stats");
while (1) {
sleep(1);
__u64 rx = 0, tx = 0, drop = 0;
__u32 key;
/* PERCPU map 需要读取所有 CPU 的值 */
int ncpus = libbpf_num_possible_cpus();
__u64 values[ncpus];
key = 0; /* STAT_RX */
bpf_map_lookup_elem(stats_fd, &key, values);
for (int i = 0; i < ncpus; i++) rx += values[i];
key = 1; /* STAT_TX */
bpf_map_lookup_elem(stats_fd, &key, values);
for (int i = 0; i < ncpus; i++) tx += values[i];
key = 2; /* STAT_DROP */
bpf_map_lookup_elem(stats_fd, &key, values);
for (int i = 0; i < ncpus; i++) drop += values[i];
printf("RX: %llu TX: %llu DROP: %llu\n", rx, tx, drop);
}
return 0;
}三、AF_XDP:用户态高性能收包
XDP 程序在内核中处理包,功能受 eBPF 指令集限制。AF_XDP 提供了另一种模式:XDP 将包重定向到用户态 socket,让用户态程序以接近内核的速度处理原始数据包。
3.1 AF_XDP 架构
AF_XDP 的数据路径:
网卡 DMA → 驱动 Ring Buffer
↓
XDP 程序执行
↓ XDP_REDIRECT → bpf_redirect_map(xsks_map)
AF_XDP Socket(UMEM 共享内存)
↓
用户态应用直接访问包数据
关键数据结构:
UMEM(共享内存区域):
┌──────────────────────────────────────────┐
│ Chunk 0 │ Chunk 1 │ Chunk 2 │ ... │
│ (frame) │ (frame) │ (frame) │ │
└──────────────────────────────────────────┘
↑ ↑ ↑
4 个环形队列引用这些 chunk:
FILL Ring: 用户态 → 内核 "这些 chunk 可以用来收包"
COMPLETION Ring: 内核 → 用户态 "这些 chunk 的包已经发送完毕"
RX Ring: 内核 → 用户态 "这些 chunk 已经收到包"
TX Ring: 用户态 → 内核 "请发送这些 chunk 中的包"
3.2 AF_XDP 编程
/* af_xdp_recv.c - AF_XDP 收包示例 */
#include <linux/if_xdp.h>
#include <linux/if_link.h>
#include <bpf/xsk.h>
#include <bpf/bpf.h>
#include <net/if.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#define NUM_FRAMES 4096
#define FRAME_SIZE XSK_UMEM__DEFAULT_FRAME_SIZE /* 4096 */
#define BATCH_SIZE 64
struct xsk_socket_info {
struct xsk_ring_cons rx;
struct xsk_ring_prod tx;
struct xsk_ring_prod fq; /* Fill Queue */
struct xsk_ring_cons cq; /* Completion Queue */
struct xsk_socket *xsk;
struct xsk_umem *umem;
void *umem_area;
};
/* 初始化 UMEM */
int setup_umem(struct xsk_socket_info *xsk_info) {
size_t umem_size = NUM_FRAMES * FRAME_SIZE;
/* 分配页对齐的内存 */
if (posix_memalign(&xsk_info->umem_area, getpagesize(), umem_size)) {
return -1;
}
struct xsk_umem_config cfg = {
.fill_size = XSK_RING_PROD__DEFAULT_NUM_DESCS,
.comp_size = XSK_RING_CONS__DEFAULT_NUM_DESCS,
.frame_size = FRAME_SIZE,
.frame_headroom = XSK_UMEM__DEFAULT_FRAME_HEADROOM,
.flags = 0,
};
int ret = xsk_umem__create(&xsk_info->umem, xsk_info->umem_area,
umem_size, &xsk_info->fq, &xsk_info->cq,
&cfg);
return ret;
}
/* 创建 AF_XDP Socket */
int setup_xsk_socket(struct xsk_socket_info *xsk_info,
const char *ifname, int queue_id) {
struct xsk_socket_config cfg = {
.rx_size = XSK_RING_CONS__DEFAULT_NUM_DESCS,
.tx_size = XSK_RING_PROD__DEFAULT_NUM_DESCS,
.libbpf_flags = XSK_LIBBPF_FLAGS__INHIBIT_PROG_LOAD,
.xdp_flags = 0,
.bind_flags = XDP_USE_NEED_WAKEUP,
};
int ret = xsk_socket__create(&xsk_info->xsk, ifname, queue_id,
xsk_info->umem, &xsk_info->rx,
&xsk_info->tx, &cfg);
if (ret) return ret;
/* 初始填充 Fill Queue:告诉内核哪些 frame 可以用来收包 */
__u32 idx;
ret = xsk_ring_prod__reserve(&xsk_info->fq, NUM_FRAMES / 2, &idx);
for (__u32 i = 0; i < NUM_FRAMES / 2; i++) {
*xsk_ring_prod__fill_addr(&xsk_info->fq, idx + i) =
i * FRAME_SIZE;
}
xsk_ring_prod__submit(&xsk_info->fq, NUM_FRAMES / 2);
return 0;
}
/* 接收循环 */
void rx_loop(struct xsk_socket_info *xsk_info) {
struct pollfd fds = {
.fd = xsk_socket__fd(xsk_info->xsk),
.events = POLLIN,
};
unsigned long rx_packets = 0;
while (1) {
/* 检查是否需要唤醒 */
if (xsk_ring_prod__needs_wakeup(&xsk_info->fq)) {
poll(&fds, 1, 1000);
}
__u32 idx_rx = 0;
unsigned int rcvd = xsk_ring_cons__peek(&xsk_info->rx,
BATCH_SIZE, &idx_rx);
if (!rcvd) continue;
for (__u32 i = 0; i < rcvd; i++) {
const struct xdp_desc *desc =
xsk_ring_cons__rx_desc(&xsk_info->rx, idx_rx + i);
/* 直接访问包数据(零拷贝) */
void *pkt = xsk_umem__get_data(xsk_info->umem_area,
desc->addr);
__u32 len = desc->len;
/* 处理数据包 */
process_raw_packet(pkt, len);
}
xsk_ring_cons__release(&xsk_info->rx, rcvd);
/* 回收 frame 到 Fill Queue */
__u32 idx_fq;
if (xsk_ring_prod__reserve(&xsk_info->fq, rcvd, &idx_fq) == rcvd) {
for (__u32 i = 0; i < rcvd; i++) {
const struct xdp_desc *desc =
xsk_ring_cons__rx_desc(&xsk_info->rx, idx_rx + i);
*xsk_ring_prod__fill_addr(&xsk_info->fq, idx_fq + i) =
desc->addr;
}
xsk_ring_prod__submit(&xsk_info->fq, rcvd);
}
rx_packets += rcvd;
if (rx_packets % 1000000 == 0) {
printf("Received %lu packets\n", rx_packets);
}
}
}
void process_raw_packet(void *data, __u32 len) {
struct ethhdr *eth = data;
(void)len;
/* 解析并处理原始以太网帧 */
/* 用户在这里做任意复杂的处理 */
(void)eth;
}3.3 AF_XDP 的零拷贝模式
# AF_XDP 有两种数据传输模式:
# 1. 拷贝模式(默认):
# 驱动将包数据拷贝到 UMEM 中
# 任何网卡都支持
bind_flags = 0;
# 2. 零拷贝模式:
# 驱动直接 DMA 到 UMEM(需要驱动支持)
# 性能最高,但支持的网卡有限
bind_flags = XDP_ZEROCOPY;
# 支持零拷贝的驱动(Linux 6.x):
# - Intel: i40e, ice, ixgbe, igc
# - Mellanox: mlx5
# - 虚拟: veth3.4 AF_XDP 与 DPDK 的对比
| 维度 | AF_XDP | DPDK |
|---|---|---|
| 内核交互 | 保持与内核的联系 | 完全绕过内核 |
| 驱动模型 | 内核驱动 + XDP hook | 用户态 PMD |
| CPU 占用 | 可以用 poll 等待 | 100% 轮询 |
| 性能 | ~20 Mpps(单核) | ~40 Mpps(单核) |
| 管理接口 | ethtool/ip 正常使用 | 网卡对 Linux 不可见 |
| 安全性 | eBPF 验证器保证 | 无限制 |
| 调试 | bpftool/bpftrace | 有限 |
| 部署复杂度 | 低 | 高 |
| 容器兼容 | 好 | 差 |
四、Katran:Facebook 的 XDP 负载均衡器
Katran 是 Facebook 开源的 L4 负载均衡器,完全基于 XDP 实现,在生产环境中每天处理数十亿请求。
4.1 Katran 的架构
Katran 的工作流程:
客户端 → 路由器(ECMP / Anycast)→ Katran(XDP L4 LB)→ 后端服务
数据包路径:
1. 客户端发送 TCP SYN 到 VIP(如 203.0.113.1:443)
2. BGP Anycast 将包路由到最近的 Katran 实例
3. Katran XDP 程序:
a. 解析 IP + TCP 头
b. 用一致性哈希选择后端
c. 用 IPIP/GUE 封装,修改外层目的 IP 为后端 IP
d. XDP_TX 发回网卡
4. 后端解封装,直接回复客户端(DSR 模式)
┌──────────────────────────────────────────┐
│ Katran XDP 程序 │
│ │
│ ┌──────────┐ ┌─────────────┐ │
│ │ 解析包头 │ → │ VIP 查找 │ │
│ │ ETH+IP+L4│ │ (BPF Map) │ │
│ └──────────┘ └──────┬──────┘ │
│ ↓ │
│ ┌───────────────┐ │
│ │ 一致性哈希 │ │
│ │ (Maglev Hash) │ │
│ └───────┬───────┘ │
│ ↓ │
│ ┌───────────────┐ │
│ │ IPIP/GUE 封装 │ │
│ └───────┬───────┘ │
│ ↓ │
│ XDP_TX │
└──────────────────────────────────────────┘
4.2 Maglev 一致性哈希
Katran 使用 Google Maglev 论文中的一致性哈希算法:
/*
* Maglev 哈希的核心思想:
*
* 1. 为每个后端生成一个偏好列表(permutation)
* 2. 轮流让每个后端"填入"查找表
* 3. 结果是一个固定大小的查找表,
* 添加/删除后端时变化最小
*
* 查找表大小选择质数(如 65537)
* 保证均匀分布
*/
/* 简化的 Maglev 查找表构建 */
#define TABLE_SIZE 65537 /* 质数 */
struct maglev_table {
int lookup[TABLE_SIZE]; /* 后端索引 */
int n_backends;
};
void build_maglev_table(struct maglev_table *t, int n_backends) {
/* 每个后端的偏好排列 */
int *permutation = calloc(n_backends * TABLE_SIZE, sizeof(int));
int *next = calloc(n_backends, sizeof(int));
memset(t->lookup, -1, sizeof(t->lookup));
/* 生成偏好排列(使用两个哈希函数) */
for (int i = 0; i < n_backends; i++) {
uint32_t offset = hash1(i) % TABLE_SIZE;
uint32_t skip = (hash2(i) % (TABLE_SIZE - 1)) + 1;
for (int j = 0; j < TABLE_SIZE; j++) {
permutation[i * TABLE_SIZE + j] =
(offset + j * skip) % TABLE_SIZE;
}
}
/* 轮流填入查找表 */
int filled = 0;
while (filled < TABLE_SIZE) {
for (int i = 0; i < n_backends && filled < TABLE_SIZE; i++) {
int pos = permutation[i * TABLE_SIZE + next[i]];
while (t->lookup[pos] != -1) {
next[i]++;
pos = permutation[i * TABLE_SIZE + next[i]];
}
t->lookup[pos] = i;
next[i]++;
filled++;
}
}
t->n_backends = n_backends;
free(permutation);
free(next);
}
/* 查找:O(1) */
int maglev_lookup(struct maglev_table *t, uint32_t hash) {
return t->lookup[hash % TABLE_SIZE];
}4.3 Katran 的工程亮点
1. 连接追踪与一致性
- 使用 BPF_MAP_TYPE_LRU_HASH 存储连接映射
- 新连接用 Maglev 哈希选后端
- 已有连接直接查表,保证同一连接到同一后端
- 后端变更时,只有 Maglev 表中受影响的条目变化
2. 健康检查集成
- 用户态守护进程执行健康检查
- 发现后端故障时,通过 BPF Map 更新移除后端
- XDP 程序下一次查找自动生效,无需重新加载程序
3. Quic 支持
- QUIC 连接 ID 中编码了服务器 ID
- Katran 解析 QUIC 连接 ID,直接路由到正确后端
- 避免了 QUIC 迁移时的连接中断
4. 性能数据
- 单核处理能力:~10 Mpps(64 字节包)
- 延迟开销:< 50 μs
- 内存占用:< 100 MB
五、XDP 调试与监控
5.1 bpftool 诊断
# 列出已加载的 XDP 程序
bpftool prog list
# 42: xdp name xdp_syn_filter tag abc123 gpl
# loaded_at 2025-07-25T10:00:00+0000 uid 0
# xlated 1234B jited 890B memlock 4096B
# 查看 XDP 程序的 JIT 代码
bpftool prog dump jited id 42
# 查看 BPF Map 内容
bpftool map list
bpftool map dump id 5
# 查看接口上的 XDP 程序
bpftool net list
# xdp:
# eth0(2) driver id 42
# 查看 XDP 统计
bpftool prog show id 42
# 查看运行次数和平均耗时5.2 性能监控
# XDP 相关的 tracepoints
# 列出所有 XDP tracepoints
perf list 'xdp:*'
# xdp:xdp_exception XDP 程序返回异常
# xdp:xdp_bulk_tx 批量发送
# xdp:xdp_redirect 重定向事件
# xdp:xdp_redirect_err 重定向错误
# xdp:xdp_redirect_map Map 重定向
# 监控 XDP 丢弃
bpftrace -e '
tracepoint:xdp:xdp_exception {
@drops[args->act] = count();
}
interval:s:1 { print(@drops); clear(@drops); }
'
# XDP 程序性能分析
bpftrace -e '
kprobe:bpf_prog_run_xdp {
@start[tid] = nsecs;
}
kretprobe:bpf_prog_run_xdp /@start[tid]/ {
@ns = hist(nsecs - @start[tid]);
delete(@start[tid]);
}
'
# 查看 XDP 包处理统计(使用 ethtool)
ethtool -S eth0 | grep xdp
# rx_xdp_aborted: 0
# rx_xdp_drop: 1523456
# rx_xdp_pass: 9876543
# rx_xdp_tx: 0
# rx_xdp_tx_errors: 0
# rx_xdp_redirect: 4567895.3 常见问题排查
# 问题 1:XDP 程序加载失败
# "invalid BPF program" / "verifier error"
# 原因:eBPF 验证器拒绝了程序
# 查看详细的验证器错误
bpftool prog load xdp_prog.o /sys/fs/bpf/xdp_prog 2>&1
# 常见原因:
# - 未检查边界(data + offset > data_end)
# - 循环次数不确定
# - 栈空间超过 512 字节
# - 使用了不允许的帮助函数
# 问题 2:性能低于预期
# 检查是否在 generic 模式运行
ip link show dev eth0 | grep xdp
# "xdpgeneric" 表示通用模式,性能差
# "xdp" 或 "xdpdrv" 表示原生模式
# 检查驱动是否支持 native XDP
ethtool -i eth0
# driver: i40e → 支持
# driver: r8169 → 不支持
# 问题 3:AF_XDP 收不到包
# 检查 XDP 程序是否正确重定向
bpftool map dump name xsks_map
# 确认 queue_id 映射正确
# 检查 FILL Ring 是否有可用 frame
# 如果 FILL Ring 为空,内核无处放收到的包六、XDP 与 AF_XDP 选型指南
需要高性能包处理?
│
├── 只需要简单过滤/统计?
│ └── XDP(内核态处理,最简单)
│
├── 需要复杂处理但可以在内核完成?
│ └── XDP + BPF Maps
│ 适用:DDoS 防御、流量监控、简单 LB
│
├── 需要用户态处理?
│ ├── 需要 TCP/IP 协议栈?
│ │ ├── 是 → F-Stack/DPDK(上一篇方案)
│ │ └── 否 → AF_XDP
│ │ 适用:自定义协议、包捕获分析
│ │
│ └── 性能要求极致(>25 Mpps/核)?
│ └── DPDK(仍然是性能之王)
│
└── 需要与内核栈共存?
└── XDP(DPDK 做不到)
适用:部分流量加速 + 部分走内核栈
参考文献
- Jesper Dangaard Brouer, “XDP - eXpress Data Path,” Linux Kernel Documentation.
- Toke Høiland-Jørgensen et al., “The eXpress Data Path: Fast Programmable Packet Processing in the Operating System Kernel,” CoNEXT 2018.
- Björn Töpel, Magnus Karlsson, “AF_XDP,” Linux Kernel Documentation.
- Facebook Engineering, “Open-sourcing Katran, a scalable network load balancer,” 2018.
- Daniel E. Eisenbud et al., “Maglev: A Fast and Reliable Software Network Load Balancer,” NSDI 2016.
- Cilium Documentation, “BPF and XDP Reference Guide,” docs.cilium.io.
- Andrii Nakryiko, “BPF CO-RE (Compile Once – Run Everywhere),” Linux Kernel Documentation.
上一篇: DPDK 与用户态网络栈:内核旁路的工程实践 下一篇: 网络 I/O 模式:Reactor、Proactor 与协程
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【网络工程】eBPF 可编程网络:从包过滤到流量工程
eBPF 正在重新定义网络工程——从传统的 iptables/netfilter 规则堆砌,到可编程、可观测、高性能的网络数据平面。本文系统讲解 eBPF 网络程序类型(XDP/TC/Socket)、Map 数据结构、Cilium 的 eBPF 数据平面实现,以及 eBPF 在负载均衡、可观测性和网络安全中的工程实践。
【网络工程】epoll 深度剖析:ET/LT 模式、源码分析与性能特征
epoll 是 Linux 高性能网络编程的基石。本文深入剖析 epoll 的内核数据结构(红黑树与就绪链表)、ET 和 LT 两种触发模式的行为差异与编程范式、惊群问题及 EPOLLEXCLUSIVE 的解决方案。
【网络工程】DPDK 与用户态网络栈:内核旁路的工程实践
当内核网络栈的上下文切换和拷贝开销成为瓶颈时,DPDK 提供了内核旁路方案。本文从 PMD 轮询模型、Hugepage 内存管理、NUMA 亲和到 F-Stack 用户态协议栈,系统讲解 DPDK 的工程原理与生产实践。
【网络工程】BPF 网络诊断:bpftrace 与 bcc 工具实战
系统讲解 eBPF 在网络诊断中的工程应用:bcc 工具集(tcplife/tcpretrans/tcpdrop)的使用场景、bpftrace 自定义网络探针编写、XDP 丢包分析、内核协议栈延迟追踪,建立基于 eBPF 的系统化网络诊断方法。