2026 年 Hacker News 上一个名为 Zeroserve 的项目引发了热议——它用 eBPF 作为 Web 服务器的脚本引擎,在单核 HTTPS 性能上超越了 nginx。这不是又一个玩具项目,而是 eBPF 从内核可观测性工具走向应用层架构的标志性事件。如果你还在纠结 nginx 配置文件写不对、Lua 脚本调试痛苦,这篇文章会告诉你:Web 服务器的下一个范式转移正在发生。
🔐 一、eBPF 不只是内核工具——它正在吞噬应用层
什么是 eBPF,为什么开发者需要关注
eBPF(Extended Berkeley Packet Filter)最初是 Linux 内核中的包过滤器,但今天的 eBPF 已经是一个通用的、沙箱化的虚拟机,可以在内核和用户空间安全地运行自定义代码。它的核心优势是:JIT 编译到原生机器码 + 严格的内存安全保证 + 极低的运行时开销。
传统上,eBPF 主要用于可观测性工具(如 bpftrace、Cilium)和网络策略。但 Zeroserve 的出现证明:eBPF 可以作为 Web 服务器的一等公民脚本引擎,替代 nginx 的 Lua 模块或 Caddy 的插件系统。
💡 **关键区别:**eBPF 程序是 JIT 编译的原生 x86-64 代码,而 LuaJIT 是追踪式 JIT。两者都生成原生代码,但 eBPF 的沙箱模型(指针笼 + 内存隔离)提供了更强的安全性,不需要信任脚本作者。
为什么 nginx 的配置模型已经过时
nginx 使用声明式配置语言(location 块、rewrite 规则、map 指令),当逻辑复杂时,你不得不引入 Lua 脚本。问题是:行为被分成了两层——声明式指令悄悄增长出自己的控制流,而脚本运行在请求生命周期的某个阶段,你必须在脑子里记住这个时序。
Zeroserve 的设计哲学是:程序就是配置。一个 eBPF 程序看到每个请求并决定发生什么——路由、头部修改、认证、限流、反向代理,全部在一个程序中,从上到下可读。
// 一个 eBPF 脚本 = 完整的请求处理逻辑
#include <zeroserve.h>
ZS_ENTRY
zs_u64 entry(void) {
char path[64];
zs_req_path(path, sizeof(path));
// 路由:/health 端点
if (zs_strcmp(path, "/health") == 0) {
zs_meta_set(ZS_STR("zs.response.header.content-type"),
ZS_STR("application/json"));
zs_respond(200, ZS_STR("{\"status\":\"ok\"}\n"));
return 0;
}
// 认证:/api/* 需要 Bearer token
if (zs_strncmp(path, "/api/", 5) == 0) {
char auth[256];
zs_req_header(ZS_STR("authorization"), auth, sizeof(auth));
if (zs_strncmp(auth, "Bearer ", 7) != 0) {
zs_respond(401, ZS_STR("{\"error\":\"unauthorized\"}\n"));
return 0;
}
}
// 其他请求:继续处理链(静态文件)
return 0;
}
🚀 二、性能实测:Zeroserve vs nginx vs Caddy
基准测试环境
测试在 8 核 Ryzen 7 3700X 上进行,所有服务器绑定到单个 CPU 核心(公平比较),使用 wrk -t4 -c100 驱动负载,HTTPS/TLS 1.3,取三次 10 秒运行的中位数。
静态文件服务性能
| 服务器 | 小文件 (174B) req/s | 小文件 p99 | 大文件 (100KB) req/s | 大文件 p99 |
|---|---|---|---|---|
| Zeroserve | 36,681 | 5.4 ms | 8,000 | 22 ms |
| nginx 1.26 | 31,226 | 7.8 ms | 7,600 | 28 ms |
| Caddy 2.11 | 12,830 | 22 ms | 6,084 | 44 ms |
⚡ **关键结论:**Zeroserve 在小文件场景比 nginx 快 17%,尾延迟更低。大文件场景三者接近,因为 TLS 加密成为共同瓶颈。
eBPF 脚本 vs nginx Lua 性能
这是最有意思的部分——当需要执行自定义逻辑时,eBPF 的表现如何?
| 引擎 | 中间件注入 req/s | 中间件 p99 | 动态响应 req/s | 动态响应 p99 |
|---|---|---|---|---|
| Zeroserve eBPF (10ms) | 43,709 | 5.1 ms | 46,945 | 4.5 ms |
| Zeroserve eBPF (2ms 默认) | 31,334 | 6.7 ms | 32,393 | 6.7 ms |
| nginx Lua | 28,653 | 8.4 ms | 41,231 | 6.4 ms |
⚠️ **重要发现:**预抢占定时器间隔对性能影响巨大。默认 2ms 的保守设置会拖累性能约 30%,生产环境建议调整到 10ms。
为什么 eBPF 能比 LuaJIT 更快
两者都编译到原生代码,但 eBPF 的优势在于:
- 更轻量的运行时:eBPF 的 uBPF JIT 编译器生成的代码更紧凑,没有 LuaJIT 的追踪编译开销
- 零 GC 压力:eBPF 程序没有垃圾回收,内存分配是确定性的
- io_uring 集成:Zeroserve 的所有 I/O 都通过 io_uring 提交,与 eBPF 脚本在同一事件循环中运行
- 指针笼(Pointer Cage):内存访问被掩码到程序自己的 arena,没有额外的边界检查开销
💡 三、实战:用 eBPF 构建现代 Web 服务器
从零开始:部署 Zeroserve
Zeroserve 的部署极其简单——一个 tarball 就是整个站点:
# 1. 准备静态文件目录
mkdir -p site/public
echo '<html><body>Hello from Zeroserve!</body></html>' > site/public/index.html
# 2. 打包成 tarball
zeroserve --pack ./site/public > site.tar
# 3. 启动服务器(HTTPS 自动配置)
zeroserve --addr 0.0.0.0:8443 site.tar
# 4. 热重载(更新站点 + TLS 证书,零停机)
killall -SIGHUP zeroserve
💡 **提示:**Zeroserve 使用 BoringSSL 终止 TLS,支持 TLS 1.3、HTTP/2、Encrypted Client Hello (ECH),以及 JA4 客户端指纹识别——这些都是零配置自动启用的。
实战案例 1:API 网关 + 限流
// .zeroserve/scripts/01-ratelimit.c
#include <zeroserve.h>
ZS_ENTRY
zs_u64 entry(void) {
char path[64];
zs_req_path(path, sizeof(path));
// 只对 /api/* 路径限流
if (zs_strncmp(path, "/api/", 5) != 0) return 0;
// 获取客户端 IP 作为限流键
char peer[64];
zs_req_peer(peer, sizeof(peer));
// 令牌桶限流:每个 IP 每秒 100 个请求
if (!zs_ratelimit_check(ZS_STR("api_limit"), peer, zs_strlen(peer),
100, // 容量
100)) { // 每秒补充
zs_meta_set(ZS_STR("zs.response.header.retry-after"),
ZS_STR("1"));
zs_respond(429, ZS_STR("{\"error\":\"rate limit exceeded\"}\n"));
return 0;
}
// 反向代理到后端服务
zs_reverse_proxy(ZS_STR("http://127.0.0.1:9000"));
return 0;
}
实战案例 2:无状态 OIDC 登录
Zeroserve 内置了完整的 OIDC 依赖方流程(Authorization Code + PKCE),登录状态存储在 XChaCha20-Poly1305 加密的 Cookie 中,服务器完全无状态:
// .zeroserve/scripts/02-auth.c
#include <zeroserve.h>
ZS_ENTRY
zs_u64 entry(void) {
char path[64];
zs_req_path(path, sizeof(path));
// 登录端点:重定向到 Google
if (zs_strcmp(path, "/login") == 0) {
zs_oidc_login(ZS_STR("https://accounts.google.com"),
ZS_STR("your-client-id"),
ZS_STR("https://yoursite.com/callback"));
return 0;
}
// 回调端点:验证 code,设置加密 Cookie
if (zs_strcmp(path, "/callback") == 0) {
zs_oidc_callback(ZS_STR("https://accounts.google.com"),
ZS_STR("your-client-id"),
ZS_STR("your-client-secret"),
ZS_STR("https://yoursite.com/callback"));
return 0;
}
// 受保护路径:检查登录状态
if (zs_strncmp(path, "/admin/", 7) == 0) {
char email[128];
if (zs_oidc_get_email(email, sizeof(email)) <= 0) {
zs_respond(401, ZS_STR("Please login first\n"));
return 0;
}
// 已登录,继续处理
}
return 0;
}
📌 **记住:**Zeroserve 的限流状态能跨热重载存活,OIDC 会话存储在客户端 Cookie 中——整个服务器没有任何状态文件需要备份。
实战案例 3:动态页面 + 模板注入
eBPF 脚本可以通过元数据系统向静态 HTML 注入动态内容:
// .zeroserve/scripts/00-enrich.c
#include <zeroserve.h>
ZS_ENTRY
zs_u64 entry(void) {
// 获取客户端信息
char peer[64];
if (zs_req_peer(peer, sizeof(peer)) <= 0)
zs_strcpy(peer, "unknown");
// 注入到 HTML 模板
zs_meta_set(ZS_STR("visitor"), ZS_STR(peer));
// 添加响应头(对所有响应生效)
zs_meta_set(ZS_STR("zs.response.header.x-served-by"),
ZS_STR("zeroserve-ebpf"));
zs_meta_set(ZS_STR("zs.response.header.x-request-id"),
ZS_STR("req-12345"));
return 0;
}
对应的 HTML 文件中使用 <zs-meta> 标签:
<!-- index.html -->
<html>
<body>
<p>访客 IP:<zs-meta>visitor</zs-meta></p>
</body>
</html>
📊 四、架构对比:Zeroserve vs 传统方案
| 特性 | Zeroserve | nginx + Lua | Caddy | Node.js (Fastify) |
|---|---|---|---|---|
| 配置模型 | eBPF 程序 = 配置 | 声明式 + Lua 脚本 | Caddyfile / JSON | 代码即配置 |
| 脚本引擎 | eBPF (JIT 原生) | LuaJIT | Go 插件 | V8 JIT |
| 内存安全 | 指针笼沙箱 | Lua 沙箱 | Go 内存安全 | V8 沙箱 |
| TLS 支持 | TLS 1.3 + ECH + JA4 | TLS 1.2/1.3 | TLS 1.3 自动 | 需手动配置 |
| 部署模型 | 单个 tarball | 配置文件 + 模块 | 配置文件 | node_modules + 代码 |
| 热重载 | SIGHUP 原子切换 | reload 命令 | 自动 | 需进程管理器 |
| I/O 模型 | io_uring | epoll | Go goroutine | epoll/kqueue |
| 单核小文件 | 36,681 req/s | 31,226 req/s | 12,830 req/s | ~15,000 req/s |
| 学习曲线 | 中(需懂 C + eBPF) | 低(配置) | 低(配置) | 低(JavaScript) |
⚠️ **适用场景:**Zeroserve 最适合需要高性能 + 可编程逻辑的场景(API 网关、静态站点 + 认证、边缘计算)。如果只是简单静态文件服务,nginx 的配置模型更简单。
✅ 五、最佳实践与避坑指南
✅ 推荐做法
- ✅ 生产环境将预抢占定时器设为 10ms:默认 2ms 太保守,会损失约 30% 的脚本性能
- ✅ 使用脚本链实现关注点分离:
00-enrich.c(通用逻辑)→01-auth.c(认证)→02-ratelimit.c(限流),按文件名排序执行 - ✅ 利用元数据传递脚本间状态:
zs_meta_set()设置的值在整个请求生命周期可用 - ✅ 使用 SIGHUP 热重载:更新站点只需替换 tarball 并发送信号,零停机、零丢连接
- ✅ 单进程 + 多实例部署:Zeroserve 是单线程设计,通过运行多个进程利用多核 CPU
❌ 避免做法
- ❌ 不要在 eBPF 脚本中做重计算:脚本有 256KB 内存限制,复杂逻辑应该代理到后端
- ❌ 不要忘记处理错误路径:
zs_req_path()可能失败,始终检查返回值 - ❌ 不要硬编码敏感信息:客户端 ID、密钥等应该通过环境变量或外部文件管理
⚠️ 注意事项
- ⚠️ Zeroserve 目前只支持 Linux(需要 io_uring 内核支持,5.1+)
- ⚠️ eBPF 脚本需要用 C 编写,编译时需要 clang 和 llc
- ⚠️ 单线程设计意味着单个请求的脚本阻塞会影响其他请求(虽然有预抢占机制)
🎯 总结:Web 服务器的下一个范式
Zeroserve 代表的不只是一个新工具,而是一种新范式:用安全的、JIT 编译的沙箱化程序替代传统的声明式配置。eBPF 的优势在于它同时提供了:
- 安全性:指针笼 + 内存隔离,脚本无法逃逸
- 性能:JIT 编译到原生代码,与 LuaJIT 同级甚至更优
- 可编程性:一个程序覆盖路由、认证、限流、代理所有逻辑
对于需要高性能 + 可编程逻辑的场景(API 网关、边缘计算、静态站点 + 动态逻辑),Zeroserve 是一个值得认真考虑的选择。对于简单的静态文件服务,nginx 仍然是最成熟的方案。
💡 相关工具推荐:
- Zeroserve - 零配置 eBPF Web 服务器
- async-ebpf - 用户空间 eBPF 运行时
- monoio - io_uring 异步运行时
- Cilium - eBPF 网络和安全方案
- bpftrace - eBPF 可观测性工具
如果你正在构建需要极致性能和灵活编程的 Web 服务,现在是时候关注 eBPF 了。它不再是内核黑客的专属工具——Zeroserve 证明,它已经准备好进入应用层了。