Nginx 负载均衡基准测试
| |
Latency(延迟)
- Avg 12.31ms:平均每个请求的响应时间为 12.31 毫秒
- Stdev 12.66ms:标准差说明响应时间波动较大
- Max 84.49ms:最长请求花了 84.49 毫秒
- 82.31% +/- Stdev:82% 的请求延迟在 ±1 标准差范围内(说明大部分请求分布较广)
Req/Sec(每秒请求数)
- Avg 5.71k:平均每个线程每秒发出约 5710 个请求
- Stdev 4.96k:标准差说明不同时间点吞吐波动较大
- Max 12.98k:单个线程的最高瞬时请求速率为 12.98k/s
- 63.00% +/- Stdev:约 63% 的样本在这个范围内
接下来我们利用套接字 eBPF 程序优化
理解 Nginx 负载均衡的本质
Nginx 作为反向代理或负载均衡器,本质上是:
- 从客户端接收请求(通过
accept()建立 TCP 连接) - 选择一个上游后端(upstream server)
- 在用户态发起一个新的 TCP 连接 到该后端
- 在两个 socket 之间中转数据(前端 <-> 后端)
关键点:Nginx 的负载均衡策略(如 round robin、ip_hash)在用户态决定;而实际的数据流是两个独立的 socket,中间的数据转发也在用户态进行(效率上有损)。
当我们引入 eBPF(extended Berkeley Packet Filter) 时,目标往往是:
- 把负载均衡逻辑下放到内核态
- 减少 Nginx 用户态转发开销
- 实现更快的流量分发(零拷贝、低延迟)
例如:
- 用 eBPF 程序在 XDP 或 TC ingress hook 阶段直接选择后端;
- 或者使用 sockmap/sockhash 在内核中直接“转发”两个 socket 之间的数据。
使用 socket 映射转发网络包
socket 映射(socket map)是 eBPF 中的一种 BPF map 类型,它可以在内核中保存 socket 的引用(即 TCP 连接对象), 从而允许 eBPF 程序直接操作和转发这些连接上的数据。它有两种形式:
| 类型 | 名称 | 特点 |
|---|---|---|
BPF_MAP_TYPE_SOCKMAP | sockmap | 数组结构,按索引存 socket |
BPF_MAP_TYPE_SOCKHASH | sockhash | 哈希结构,可按 key 查找 socket |
可以理解为:sockmap/sockhash = 一个“连接路由表”,存放 <key, socket> 映射关系。 让 eBPF 程序在内核中知道“哪个 key 对应哪个 socket”。
传统用户态转发的性能瓶颈,以 Nginx 为例:
| |
数据流需要经过:
- 2 次系统调用(recv + send)
- 2 次上下文切换(内核↔用户态)
- 1 次数据拷贝(内核缓冲区↔用户缓冲区)
当并发量很高(比如 wrk 10k QPS)时,这个过程就成为瓶颈。
于是 Linux 引入了 socket 映射 + SK_MSG eBPF 程序可以直接在内核中完成转发:
- 把所有连接(client 和 upstream)的 socket 引用注册进 sockmap;
- eBPF 程序在发送路径中(
BPF_PROG_TYPE_SK_MSG)执行; - 程序从 sockmap 中查找目标 socket;
- 内核直接把数据从一个 socket 发送到另一个 socket —— 零拷贝转发。
在 eBPF 优化体系中:
- 控制面(Control Plane):决定怎么转发(谁转发给谁) → 用户态(Nginx)
- 数据面(Data Plane):实际执行转发 → 内核态(eBPF)
socket 映射就是两者的“桥梁”:Nginx 负责把 socket 注册进 sockmap(告诉内核:client_fd 对应 upstream_fd),eBPF 程序负责从 sockmap 中查找目标并完成转发。
这种模式的好处是:
- eBPF 不需要自己维护连接;
- 用户态应用可以动态调整映射;
- 内核可以高性能地完成数据转发。
数据流简化示意图如下:
- 传统 Nginx 转发
| |
- eBPF + socket 映射优化后
| |
整个转发在内核中完成,无需进入 Nginx 用户态。
那优化的步骤如下:
- 创建套接字映射;
- 在
BPF_PROG_TYPE_SOCK_OPS类型的 eBPF 程序中,将新创建的套接字存入套接字映射中; - 在流解析类的 eBPF 程序 (如
BPF_PROG_TYPE_SK_SKB或BPF_PROG_TYPE_SK_MSG) 中,从套接字映射中提取套接字信息,并调用 BPF 辅助函数转发网络包; - 加载并挂载 eBPF 程序到套接字事件。
socket 映射
这里使用 socket 映射(socket map)中的一种 BPF Map 类型:BPF_MAP_TYPE_SOCKHASH,它的值总是套接字文件描述符,而键则需要我们去定义。
| |
BPF_PROG_TYPE_SOCK_OPS 是 内核套接字操作回调类型的 eBPF 程序,它能在 TCP 连接的生命周期事件被触发时执行,包括:
BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB:被动连接建立(如服务器 accept)BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB:主动连接建立(如客户端 connect)BPF_SOCK_OPS_TCP_CLOSE_CB:连接关闭BPF_SOCK_OPS_STATE_CB:状态变化
也就是说,它可以跟踪 TCP socket 的整个生命周期,在连接建立/关闭时获取 socket 的五元组信息,并把 socket 存入 sockhash,从而让 内核态 eBPF 数据面程序(如 SK_MSG)可以直接高效地查找和转发 socket 数据。
需要注意的是,BPF_PROG_TYPE_SOCK_OPS 程序跟踪了所有类型的套接字操作,我们只需要把新创建的套接字更新到映射中。
| |
转发 socket
转发 socket 可以使用 BPF_PROG_TYPE_SK_MSG 类型的 eBPF 程序,它在内核中的定义是这样的:
| |
捕获 socket 中的发送数据包,并根据之前的 socket 映射进行转发:
| |
测试结果
| |
- 吞吐提升 ≈ 23.2%。
- 平均延迟下降 ≈ 36.0%。