- 小さく高速なHTTPSサーバーである zeroserve は、Webサイトの tarball を受け取り、HTTP/2 と TLS 1.3 で配信し、tarball 内のeBPFプログラムをユーザー空間のサンドボックス化ミドルウェアとして各リクエストごとに実行する
- 設定ファイルなしで、eBPF プログラムがリクエストごとのルーティング、ヘッダー、認証、レート制限、プロキシを決定し、nginx・Caddy の宣言的設定と別個のスクリプティング層を一体化する
- サイトは単一のtarファイルとしてインデックス化され、ディスク上に展開されず、tarball の置き換えと
SIGHUP によってサイト・スクリプト・TLS 資料を接続断なしでアトミックに置き換える
- 単一コア HTTPS ベンチマークでは、zeroserve は小さな静的ファイルで 36,681 req/s、10ms eBPF 動的 JSON で 46,945 req/s、小さなプロキシで 26,486 req/s を記録したが、100KB プロキシでは nginx が 5,882 req/s で優位だった
- zeroserve は nginx と Caddy の代替を目指し、単一 tarball 配布、プログラム可能な設定、ユーザー空間 eBPF、モダンな TLS を組み合わせているが、大きなプロキシ応答には nginx のほうが適している
概要
- zeroserve は、Webサイトの tarball 1つを HTTP/2 と TLS 1.3 で配信する、小さく高速な設定不要の HTTPS サーバー
- tarball 内に入れた eBPF プログラムは、すべてのリクエストでユーザー空間のサンドボックス化ミドルウェアとして実行され、リクエスト書き換え、認証、レート制限、バックエンドのリバースプロキシ処理が可能
- 単一コア基準で、小・大静的ファイル、スクリプトミドルウェア、小さな応答のプロキシといった大半のワークロードで nginx より高性能を目指すサーバー
- eBPF スクリプトはネイティブコードに JIT コンパイルされ、ユーザー空間でサンドボックス化され、各リクエストごとに実行できるほど低コストであることを目標としている
- ネットワークおよびディスク処理は monoio ランタイムを通じて
io_uring で投入される
- TLS 1.3、HTTP/2、Encrypted Client Hello、SNI 証明書選択、JA4 フィンガープリンティングをサポート
- サイト全体と TLS 資料は 1つの tarball から提供され、
SIGHUP によるホットリロードが可能
構成モデル: プログラムがそのまま設定
- zeroserve は nginx と Caddy の代替を目指しており、中核となる設計上の選択は構成方式にある
- nginx と Caddy は
location ブロック、rewrite ルール、map ディレクティブ、try_files のような宣言的設定言語を提供し、限界に達すると Lua や Caddy プラグインのような任意のスクリプティングランタイムを横に付け足す構造になっている
- この構造では、動作は独自の制御フローを持つディレクティブ層と、リクエストライフサイクルの特定地点で実行されるスクリプト層に分かれる
- zeroserve には設定ファイルがなく、1つの eBPF プログラムがすべてのリクエストを見て、ルーティング、ヘッダー、認証、レート制限、プロキシを決定する
単一 tarball をそのまま配信
- サイト全体は 1つの
tar ファイルで、zeroserve はロード時に path -> byte-range マップを作成し、tarball 自体に対してバイト範囲読み込みを行ってファイルを配信する
- どのファイルもディスク上に展開されないため、サイトは単一ファイルの中にしか存在せず、誤った
location ルールで露出するドキュメントルートもない
- デプロイは単一ファイルのアトミック置き換え方式で、新バージョンのデプロイは tarball を置き換えた後に
SIGHUP を送る
- ディレクトリのパッケージ化と実行コマンドは次の形式
zeroserve --pack ./public > site.tar
zeroserve --addr 0.0.0.0:8080 site.tar
killall -SIGHUP zeroserve
- リロードは同じプロセス内でサイト、スクリプト、TLS 資料をアトミックに置き換え、接続断なしで動作する
- 各インスタンスは単一スレッドのイベントループであり、1プロセスあたりでは制限になるが、スケール単位を「より多くのプロセス」とする場合には適した形とされる
ユーザー空間 eBPF スクリプティング
.zeroserve/scripts/ 配下に置いたすべての .c ファイルは、パッケージ化時に clang と llc で eBPF オブジェクトにコンパイルされ、すべてのリクエストで実行される
- eBPF はカーネル BPF サブシステムや
CAP_BPF なしで、通常の非特権プロセス内の async-ebpf ランタイム上でユーザー空間実行される
- async-ebpf は uBPF を内蔵し、バイトコードをネイティブ x86-64 マシンコードに JIT コンパイルする
- ポインタケージ(pointer cage)は、JIT コンパイル済みコードのすべてのメモリアクセスをプログラム専用アリーナへマスクし、不正アクセスをスクリプトメモリ内に閉じ込める
- スクリプトは zeroserve の単一イベントループ上で直接実行され、遅いスクリプトが他の接続を止めないよう、タイマーが JIT コンパイル済みネイティブコードの実行途中で割り込みを入れ、制御をイベントループへ戻せる
- プログラミングモデルは、ファイル名のソート順で実行されるスクリプトチェーンで、スクリプト間ではリクエストごとのメタデータマップを共有する
- スクリプトが
zs_respond または zs_reverse_proxy を呼び出すと、チェーンは短絡終了する
zs.response.header.* 配下のキーはすべての応答のヘッダーになり、その他のキーは HTML ファイル内の <zs-meta>visitor</zs-meta> のようなプレースホルダーを出力時に置換する小さなテンプレートパスで使われる
- ヘルパーAPI は、リクエストメソッド・パス・クエリ・ヘッダー・ピアアドレスの読み取り、URI 書き換え、ヘッダーの設定・削除をサポートする
- 暗号化およびエンコード用ヘルパーとして SHA-256、HMAC-SHA256、base64、hex、
getrandom を提供
- JSON ヘルパーは、リクエストボディの解析、ドキュメントツリーの生成・変更、
zs_json_respond による応答をサポートする
- レート制限は、ピア IP や API キーのような任意キーに基づくトークンバケットをサポートし、状態はホットリロード後も維持される
- AWS SigV4 ヘルパーは、S3 やその他 AWS サービス通信向けの署名付き
Authorization ヘッダーと presigned URL をサポートする
- OIDC ログインは Authorization Code + PKCE ベースの relying-party フローを提供し、完全なログインセッションを sealed XChaCha20-Poly1305 Cookie に格納することで、サーバーをステートレスに保ったまま静的サイトを「Google でログイン」の背後に置ける
- 動的エンドポイントは、特定パスでスクリプトが直接応答する仕組みで、例では
/health リクエストに application/json ヘッダーと {"status":"ok"} ボディを返す
- 各スクリプトはデフォルトで 256KB のメモリ上限のもとで動作し、ランタイムは長時間実行されるスクリプトを実行器上で時間分割し、暴走スクリプトをスロットリングする
- スクリプトは
zs_call で相互に呼び出せ、呼び出し深さには制限がある
- 無限ループに陥ったスクリプトは自分のリクエストだけを遅らせ、プリエンプトタイマーがそれに割り込みを入れることで、サーバーは他のリクエスト処理を継続できる
- TLS 層は TLS 1.3 専用で、BoringSSL によって終端される
- Encrypted Client Hello は実際の SNI が平文で現れないようにし、ディレクトリベースの SNI 証明書選択と、スクリプトに公開される JA4 クライアントフィンガープリントを提供する
- 透過 ECH リレーモードでは、復号できないハンドシェイクを実際のアップストリームへバイト単位でそのまま転送し、保護された名前を公開名の背後に混在させられる
性能
-
ベンチマーク条件
- zeroserve、nginx 1.26、Caddy 2.11 を 8コア Ryzen 7 3700X 上で、同じコンテンツと同じ自己署名証明書を使って HTTPS 配信比較
- zeroserve インスタンスは設計上シングルスレッドのため、比較基準はコアあたり性能
- すべてのサーバーは
taskset で 1つの CPU に固定し、nginx は worker_processes 1、Caddy は GOMAXPROCS=1、zeroserve は既存のシングルスレッド構造を使用
- 負荷は別コアから
wrk -t4 -c100 で生成し、10秒実行を 3回行った中央値を使用
wrk は HTTP/1.1 を使うため、数値は TLS 1.3 上の HTTP/1.1 であり、長い keep-alive 接続によってハンドシェイクコストを分散した、既に開いている HTTPS 接続の定常状態コストを示す
-
小さな静的ファイル 174B
| サーバー |
req/s |
p99 |
| zeroserve |
36,681 |
5.4 ms |
| nginx |
31,226 |
7.8 ms |
| Caddy |
12,830 |
22 ms |
- zeroserve は単一コアで nginx より約 17% 高速に小さなファイルを配信し、テールレイテンシも低い
- HTML ページ、小さな JSON、CSS のような静的サイトの基本ケースが zeroserve のチューニング対象
-
大きな静的ファイル 100KB
| サーバー |
req/s |
スループット |
p99 |
| zeroserve |
8,000 |
782 MB/s |
22 ms |
| nginx |
7,600 |
773 MB/s |
28 ms |
| Caddy |
6,084 |
590 MB/s |
44 ms |
- 3サーバーの結果は近く、zeroserve が単一コアで約 780 MB/s とわずかに先行した
- nginx の大きなファイルでの強みである
sendfile() は TLS 配下では使われず、バイト列をユーザー空間で暗号化する必要があるため、3サーバーとも暗号化と書き込みループに縛られる
- 3サーバーすべてで kernel TLS を無効化した条件では、zeroserve の
io_uring 読み書き経路がわずかに高速だった
eBPF vs Lua
- スクリプティング比較対象は、Web サーバー内で高速コードを実行する一般的な手法である nginx + LuaJIT
ngx_http_lua_module
- zeroserve はデフォルトでスクリプトのプリエンプトタイマーを 2ms ごとに設定しており、細かい間隔は問題のあるスクリプトを素早くスロットリングできる一方で、正常なスクリプトにもコストを与える
- デフォルトの 2ms では、完全動的応答基準で eBPF は約 32k req/s と、nginx Lua の 41k req/s を下回る
--preempt-timer-interval-ms を 10 に引き上げると、スクリプティングのスループットが約 40% 回復し、結果が逆転する
-
リクエストごとのヘッダー注入ミドルウェア
| エンジン |
req/s |
p99 |
| zeroserve eBPF 10ms |
43,709 |
5.1 ms |
| zeroserve eBPF 2ms デフォルト |
31,334 |
6.7 ms |
nginx Lua header_filter |
28,653 |
8.4 ms |
- スクリプトは実行されるが静的ファイルは引き続き配信されるミドルウェアのケースでは、10ms eBPF は nginx Lua より約 50% 高く、テールレイテンシも低い
-
完全動的 JSON 応答
| エンジン |
req/s |
p99 |
| zeroserve eBPF 10ms |
46,945 |
4.5 ms |
nginx Lua content_by_lua |
41,231 |
6.4 ms |
| zeroserve eBPF 2ms デフォルト |
32,393 |
6.7 ms |
- 10ms 間隔に調整した eBPF は、完全合成応答でも nginx の
content_by_lua より高いスループットを記録する
- 両エンジンともネイティブコードへコンパイルされ、LuaJIT はトレーシング JIT、async-ebpf は uBPF を通じて eBPF を JIT コンパイルする
- TLS 暗号化が共通のリクエストコストである条件では、調整済み eBPF 経路がスループットで上回る
- デフォルトの 2ms では eBPF はミドルウェアでの優位を維持するものの、合成応答での首位は譲るため、運用スクリプトには 10ms の使用が推奨される
リバースプロキシとしての利用
- zeroserve はスクリプトから
zs_reverse_proxy("http://127.0.0.1:9000") を呼び出してバックエンドへプロキシできる
- アップストリーム接続プールは、バックエンドごとに最大 128 接続と 30 秒のアイドル再利用をサポートする
- 公平な比較のため、nginx はデフォルトではリクエストごとにアップストリーム接続を閉じる特性を考慮し、
keepalive 128、proxy_http_version 1.1、空の Connection ヘッダーを明示指定している
- Caddy はデフォルト動作どおり接続再利用
- 各プロキシは単一コアで TLS を終端し、共有プレーンテキストバックエンドへ転送しており、バックエンドは別の 2コアサーバーで単独 100k req/s を維持し、プロキシのオーバーヘッドだけを測定した
-
小さな 174B 応答のプロキシ
| プロキシ |
req/s |
p50 |
p99 |
| zeroserve |
26,486 |
3.3 ms |
8 ms |
| nginx |
21,761 |
4.2 ms |
10.5 ms |
| Caddy |
7,683 |
10.3 ms |
33 ms |
- zeroserve のプール型
io_uring プロキシは nginx より約 22% 先行し、Caddy 比で約 3.4 倍のスループットを記録した
- API 呼び出し、小さな JSON、アプリサーバーの HTML といった一般的なプロキシワークロードでは、zeroserve のほうが TLS 終端とバックエンド転送を高速に処理する
-
100KB 応答のプロキシ
| プロキシ |
req/s |
スループット |
| nginx |
5,882 |
585 MB/s |
| Caddy |
4,285 |
406 MB/s |
| zeroserve |
3,631 |
359 MB/s |
- プロキシ本文が大きくなると、nginx のバッファリングがバイト列をより効率よく移動させて先行し、Caddy が中間、zeroserve が後れを取る
- プロキシ応答が大きい場合は nginx のほうが適したツールであり、小さく数の多い応答では zeroserve のほうが速い
メモリ
- アイドル状態の単一 zeroserve インスタンスは約 15MB PSS を使用し、nginx の約 6MB より多く、Caddy の約 60MB より少ない
- 実行単位がプロセス全体である点が重要で、コアごとにコピーを起動する際も同じバイナリをマップしてコードページを共有する
- 追加プロセスは、それ自身のワーキングセット以外には少量のメモリしか増やさない
公開
- zeroserve は GitHub でオープンソースとして公開されているプロジェクト
1件のコメント
Hacker Newsのコメント
TechEmpowerのWebサーバーベンチマークがなくなって、こうした新しいプロジェクトが自らを証明する機会が減ったように思える
修正: 私が遅れていただけで、最近注目されているのは https://www.http-arena.com/leaderboard/ のようだ。幸運を祈る
ただ、もともと頻繁に回していたわけでもなく、ラウンド記録を見ると年1回以下の実行だ
こうした試みがLLMのおかげで比較的安く速く探索できるようになって出てきているのを見るのはよいことだ
ただ、ここで感じたのは nginx 自体がかなり印象的だということだ。もう一つ目についたのは、このプロジェクトが nginx と Caddy の代替であり、設定方式に賭けているという説明だった
nginx と Caddy は宣言的な設定言語を提供し、その限界に達すると Lua や Caddy プラグインのようなスクリプトランタイムを横に付け足す構造なので、動作が二層に分かれる
しかし、その賭けは間違っているように思う。人々は昔からコードより設定を好んできたし、組み込み機能だけで十分な場合も多く、Cコードを書く必要はない
すべての設定ファイル形式は最初は単純に始まるように見える。YAMLも基本はかなり合理的だったが、人々はアンカーやエイリアスでより複雑なことを望み始めた
GitLabでさえ条件文や変数に似た独自形式を持っていて、特定の場所でしか動かないハックに近い。ApacheもXMLベースの設定形式で似た道をたどった
結局、設定管理のための数多くの特化型プログラミング言語が生まれた。企業環境では直接編集せず、Ansibleワークフローをスクリプト化してリモート手術を行う
いっそサーバーに Lua や Python のようなインタプリタを内蔵して設定管理をさせていれば、この過程を飛ばせただろうし、特注の設定ファイルをプログラムでいじるより単純だったはずだ
もちろん、こうした特注の試みが一般言語より特定用途に最適化されていると言うことはできるが、そうした主張が当てはまるのは、そもそもその仕掛け自体が不要だったおもちゃの例の狭い範囲に限られる
Windows の INI ファイルを覚えているだろうか。コードはコード、データはデータだったよき時代だった
もっと単純には、Kubernetesクラスターの Ingress マニフェストをすべて読んで pack を作り直すこともできる
要するに、ツールと設定の間のインターフェースもまた1つのAPIにすぎず、システム運用者はすでにより高水準の構成物でシステム状態を記述していて、設定を構成する具体的なバイト列はその生成物だということだ
AIの立場では、その方式のほうが扱いやすいかもしれない。AIはどちらも扱えるので、そのような転換が明確によいアイデアとして定着するまでには長い時間がかかるかもしれない
アイデアは気に入った
ただ、eBPFディレクトリに
.cファイルの代わりに**.rsファイル**を置けるなら、もっと安心できそうだ。すでにRustプロジェクトでもあるしそれに、なぜかカーネルアクセラレーションされたWebサーバーを期待していた。eBPFで安全にできるなら本当にすごいと思う
それに単一スレッドなのか? Linux で fork して入ってくる接続キューを共有するのはほとんど些細なことで、Rustでも数行で済むはずだ。SO_REUSEPORTを使えば、あとはカーネルが処理してくれる
参考までに、io_uring を推すつもりなら kTLS も一緒に推すべきだと思う。ハンドシェイク後にユーザー空間でのSSL処理を避けられるなら、設計は大幅に単純になる
これまではこういう用途に nftables を使ってきたので、自分で必要になることはなかった
とてもクールだ。これをXDPプログラムやソケットマップにアタッチされるプログラムのような別のBPFプログラム型と組み合わせて、L7 HTTP機能をさらに下位レイヤーへ統合できるのか気になる
アイデアはよいが、静的ファイルに注力すべきかは分からない。今はその目的でサーバーを新しく立てることはあまりない
だからこれは自分のために作られたように感じるが、自分が一般的なユーザーではないことは認める
良さそうだし機能も悪くない。だが、どこかあまりに人工的に感じられて、強く惹かれはしない。
指標が偽物ではないか、便利関数が実際に動くのか、きちんとしたハードニングが行われたのかも分からない。
バイブコーディングで作って README まで自動生成された、というところまでは受け入れられる。だが、発表ブログ記事まで AI が作ったもので、ソフトウェア品質についての理解が自分と同じかどうかを判断する根拠がまったくない。
奇妙な時代だ。数年前なら AI であることの告知なしに公開されても疑いなく受け入れていただろうが、今では立派な README やもっともらしいコマンドライン引数を見ると、README が幻覚しているだけで、実際にはそのオプションが存在しないのではないかとすぐ疑ってしまう。
zeroserve 自体を作る際には AI の助けをかなり使っていますが、AI の出力は自分で確認し、責任も自分が負っています。
zeroserve は単一コアで小さなファイルを nginx より約 17% 速く配信し、テールレイテンシもより小さい。HTML ページ、小さな JSON、CSS は zeroserve が得意な領域です。
100KB の大きな静的ファイルでは、zeroserve は 8,000 req/s、782 MB/s、p99 22ms で、nginx は 7,600 req/s、773 MB/s、p99 28ms、Caddy は 6,084 req/s、590 MB/s、p99 44ms です。
それでも自分は、こうした新興プロジェクトよりも、監査され、実運用で検証され、ハードニングされた古いプロジェクトを選ぶ。リスクを負うほど改善幅が大きくない。
できる限り昔ながらの時代にとどまるつもりだ。賢い人たちがソフトウェアを公開し、賢い人たちが保守する。彼らに AI は必要ない。そこが自分のニッチだ。
私たちが消えるかもしれないが、それでもそのほうがいい。ただし、そうした賢い人たちが文書を書くという前提が付く。賢くても文書を書くのを嫌う人は多い。
ずっと前に、ドキュメントのないソフトウェアは、どれほど優れていても自分の時間をかける価値はないと決めた。主にアプリケーションの話で、Linux の文書はほとんど見てこなかったが、他の人はそこまで悪くないとも言うので、何とも言えない。
興味深い新しい概念で、気に入った。
本当の問題は開発者のコミットメントとコミュニティだ。Caddy と Nginx の人たちは継続的に製品を支えてきたし、このプロジェクトにも多くの集中と関心が必要になるだろう。
なぜ tarball なのか?
ディスクには何も展開しない。サイトがその 1 ファイルに完全に収まっているので、誤った location ルールで露出するドキュメントルートがなく、デプロイも単一のアトミックなファイル置換になる。
ただし、その説明も LLM 的な正当化かもしれない。記事のあちこちに “the right shape” や “the surface is broad” のような表現が散らばっている。