Zeroserve:基于eBPF脚本化配置的零配置Web服务器
速览
Zeroserve利用eBPF技术实现无需配置的Web服务器,支持通过脚本灵活定制。
AI 深度解读
Zeroserve:基于 eBPF 的零配置 Web 服务器深度解读
背景
在 Web 服务器领域,nginx 和 Caddy 长期以来占据着主导地位。它们通常采用声明式配置语言(如 location 块、rewrite 规则、map 指令等)来定义行为。然而,当声明式配置达到极限时,用户往往需要引入外部的脚本运行时(如 Lua 或 Caddy 的插件系统)来扩展功能。这种架构导致行为被分割在两个层面:一部分是静默增长控制流的指令,另一部分则是运行在请求生命周期中某个难以追踪位置的脚本。这种分离增加了理解和维护的复杂度。
Zeroserve 的出现旨在解决这一痛点。它不仅仅是一个 Web 服务器,更是一个试图将“配置”与“逻辑”合二为一的实验性项目。其核心理念是“程序即配置”(Program-as-configuration),通过利用用户态 eBPF(Extended Berkeley Packet Filter)技术,允许开发者编写单一的、沙盒化的程序来处理每一个请求,从而消除传统声明式配置与脚本逻辑之间的割裂。
核心内容
1. 极简架构与零配置理念
Zeroserve 是一个小型、快速且无需配置的 HTTPS 服务器。用户只需提供一个包含网站内容的 tar 归档文件,服务器即可通过 HTTP/2 和 TLS 1.3 提供服务,并支持热重载(Hot Reload)和极小的内存占用。
其核心创新在于“程序即配置”。Zeroserve 没有传统的配置文件。开发者编写的 eBPF 程序本身就是配置。这个单一的、普通的、沙盒化的程序可以看到每一个请求,并决定其命运:路由、修改头信息、身份验证、速率限制或反向代理。这种设计让开发者能够从头到尾阅读一个程序来理解整个请求路径,而非在分散的配置指令和脚本之间跳转。
2. 基于 Tar 归档的静态文件服务
Zeroserve 将整个网站打包为一个单一的 tar 文件。
- 索引机制:服务器在加载时建立
路径 -> 字节范围的映射表。 - 无解压部署:文件通过向
tar归档本身发起字节范围读取(byte-range reads)来服务,无需解压到磁盘。 - 原子性部署:网站完全存在于该文件中,避免了因
location规则错误暴露文档根目录的风险。部署新版本只需替换tar文件并发送SIGHUP信号,即可原子性地交换站点、脚本和 TLS 材料,且不会断开现有连接。
打包与运行示例:
zeroserve --pack ./public > site.tar
zeroserve --addr 0.0.0.0:8080 site.tar
# 热重载
killall -SIGHUP zeroserve
3. 用户态 eBPF 脚本引擎
这是 Zeroserve 最引人注目的特性。放置在 .zeroserve/scripts/ 目录下的 .c 文件会在打包时通过 clang 和 llc 编译为 eBPF 对象。
- 用户态执行:eBPF 代码完全在用户态运行。
Zeroserve将其加载到其内部的async-ebpf运行时中。这意味着内核的 BPF 子系统和CAP_BPF权限无需介入,服务器进程保持普通且无特权。 - JIT 编译:
async-ebpf将 eBPF 字节码即时编译(JIT)为原生机器代码(基于uBPF),使得配置代码以原生 x86-64 性能运行。 - 沙盒安全:通过“指针笼”(Pointer Cage)技术模拟内核验证器的功能,防止程序访问非法内存。所有内存访问都被屏蔽到程序自己的内存区域中,确保意外访问被限制在脚本内存内。
- 可抢占式执行:脚本直接运行在
Zeroserve的单线程事件循环上。为防止单个慢速脚本阻塞其他连接,运行时支持完全抢占。定时器可以中断 JIT 编译后的原生代码执行,将控制权交回事件循环。
4. 脚本编程模型与功能
脚本按文件名排序链式运行,共享每个请求的元数据映射表。如果脚本调用 zs_respond 或 zs_reverse_proxy,链式调用将短路(提前终止)。
脚本示例( enrich 请求):
#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"));
return 0;
}
上述脚本设置的元数据有两个用途:
zs.response.header.*前缀的键值对将成为所有响应(包括静态文件、zs_respond和代理响应)的头信息。- 其他键值对用于微小的模板引擎:HTML 文件中的
<zs-meta>visitor</zs-meta>占位符将在输出时被替换,从而实现无需模板引擎的动态静态页面。
支持的 Helper 函数:
- 请求检查与修改:读取方法、路径、查询参数、头信息和对等地址;重写 URI 或在响应发出前设置/移除头信息。
- 加密与编码:支持 SHA-256、HMAC-SHA-256、Base64、Hex 和
getrandom。 - JSON 处理:解析请求体、构建和修改文档树,并通过
zs_json_respond回复。 - 速率限制:基于令牌桶算法,密钥可以是 IP 或 API Key,状态在热重载后保留。
- AWS SigV4:支持签名
Authorization头信息和预签名 URL,用于访问 S3 等服务。 - OIDC 登录:完整的依赖方流程(Authorization Code + PKCE),登录会话存储在密封的
XChaCha20-Poly1305Cookie 中,使无状态服务器也能实现“Google 登录”门控。
动态端点示例:
ZS_ENTRY
zs_u64 entry(void) {
char path[64];
zs_req_path(path, sizeof(path));
if (zs_strcmp(path, "/health") != 0) return 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;
}
5. 性能与 I/O 架构
- I/O 模型:所有网络和磁盘 I/O 均通过
io_uring(经由monoio运行时)提交。每个实例是一个单线程事件循环。虽然单线程看似局限,但在“增加进程数”作为扩展单元的场景下,这种设计非常高效,允许同一台机器上共存多个实例。 - 性能基准:在 8 核 Ryzen 7 3700X 上,将
zeroserve、nginx 1.26和Caddy 2.11限制在单核运行。使用wrk进行 HTTP/1.1 over TLS 1.3 的压力测试。结果显示,在单核处理小静态文件(174 字节)时,zeroserve比nginx快约 17%,且尾部延迟更紧凑。
6. 完整的 TLS 支持
尽管主打“零配置”,Zeroserve 内置了比其定位更完整的 TLS 功能:
- 仅支持 TLS 1.3,
