通信の負荷試験
NagiのHTTPサンプルに負荷を掛け、処理量、応答の遅れ、メモリ、接続の回収を調べました。初回の測定日は2026年10月1日、対象はNagi 0.1.2のコミットc602abbです。接続の待機期限を加えた実装は、追加試験で別に比較しています。
4KiBの文字列を受け取ってJSONを返す試験では、毎秒15,000件を30分間処理し、27,000,270件すべてがHTTP 200でした。一方で、接続数とメモリの増加、終了後も残るFDが見つかりました。処理量だけで安定性を判断できない結果です。
このページの数値は同じマシン内での通信です。インターネット上の処理量、DDoS耐性、24時間以上の安定性を示すものではありません。
このページの目次
数値の読み方
- 指定レート:負荷生成器に依頼した毎秒の送信件数。
- 実送信レート:実際に送れた毎秒の件数。送信側が詰まると指定値に届きません。
- 成功応答レート:試験時間と最後の応答待ち時間を含めた、毎秒の成功件数。
- p99:送信したリクエストの99%が、この時間以内に完了しました。送れなかったリクエストの待ち時間は含みません。
- RSS:サーバープロセスが使っている常駐メモリ。FDは接続などに使うOSのファイル記述子です。
負荷を増やしたとき
| 処理 | 指定/秒 | 実送信/秒 | 成功応答/秒 | p99 | エラー(3回合計) |
|---|---|---|---|---|---|
| HTTP /health | 10,000 | 10,000 | 10,000 | 2.03ms | 0 |
| HTTP /health | 30,000 | 29,960 | 29,874 | 190.36ms | 0 |
| HTTP /health | 60,000 | 31,990 | 31,739 | 264.83ms | 0 |
| HTTP /health | 100,000 | 32,501 | 32,266 | 301.05ms | 0 |
| 4KiB JSON | 5,000 | 5,000 | 4,999 | 0.83ms | 0 |
| 4KiB JSON | 10,000 | 10,000 | 10,000 | 2.12ms | 0 |
| 4KiB JSON | 15,000 | 15,000 | 15,000 | 5.24ms | 0 |
| 4KiB JSON | 20,000 | 19,997 | 19,993 | 144.35ms | 0 |
| 4KiB JSON | 30,000 | 27,471 | 27,216 | 310.65ms | 0 |
| 4KiB JSON | 50,000 | 27,700 | 27,483 | 261.35ms | 0 |
| SQLite SELECT | 5,000 | 5,000 | 4,999 | 0.64ms | 0 |
| SQLite SELECT | 10,000 | 10,000 | 10,000 | 1.46ms | 0 |
| SQLite SELECT | 20,000 | 19,999 | 19,998 | 6.06ms | 0 |
| SQLite SELECT | 40,000 | 32,174 | 31,939 | 222.11ms | 0 |
| SQLite INSERT | 1,000 | 1,000 | 1,000 | 0.39ms | 0 |
| SQLite INSERT | 5,000 | 4,999 | 4,999 | 0.83ms | 0 |
| SQLite INSERT | 10,000 | 10,000 | 10,000 | 1.64ms | 0 |
| SQLite INSERT | 20,000 | 20,000 | 19,998 | 32.96ms | 0 |
各行は8秒間の試験を3回行った中央値です。p99も各試験のp99の中央値で、3回分をまとめた百分位ではありません。Vegetaの百分位は近似集計値です。SQLiteはメモリ内のDBを使い、読み取りは同じ1行、書き込みは行を追加し続けます。
負荷生成器がCPUを使い切る条件では、指定レートを上げても送信量は増えません。最初の連続試験では送信元ポートの不足も発生しました。その結果は初回試験の記録に保存し、主表には試験ごとに別のループバック送信元を使った再測定を載せています。初回の12,177件の失敗は、送信側のbind: address already in useでした。
接続数を固定した飽和試験
送信側の負担が小さいwrkでも、接続数を増やして測りました。こちらは応答が返ると次を送る方式です。
| 処理 | 接続数 | 応答/秒 | p99 | CPU(1コア比) | エラー |
|---|---|---|---|---|---|
| HTTP /health | 128 | 77,689 | 2.73ms | 99.7% | 0 |
| HTTP /health | 512 | 70,682 | 69.85ms | 99.2% | 0 |
| HTTP /health | 2,048 | 66,528 | 375.86ms | 97.6% | 0 |
| 4KiB JSON | 128 | 39,022 | 4.75ms | 96.9% | 0 |
| 4KiB JSON | 512 | 39,322 | 70.93ms | 99.1% | 0 |
| 4KiB JSON | 2,048 | 38,344 | 386.19ms | 96.5% | 0 |
3回の8秒試験の中央値です。過負荷時に、送れずに待っていたリクエストの遅延は測れません。上のレート指定試験とは別の条件として読んでください。
30分間受け続けたとき
| 条件 | 30分・接続数を固定しないクライアント | 10分・128接続に固定したクライアント |
|---|---|---|
| 指定レート | 15,000件/秒 | 15,000件/秒 |
| リクエスト数 | 27,000,270 | 9,000,090 |
| HTTP 200以外・通信エラー | 0 | 0 |
| 全体のp99 | 34.11ms | 11.07ms |
| サーバーRSSの観測最大 | 180.74MiB | 8.29MiB |
| サーバーFDの観測最大 | 6,315 | 138 |
| 終了後30秒の観測 | FDが1,049個残った | FDが元の10個まで戻った |
メモリの増加は接続数の増加と同じ時期に起きています。ただし、対照試験の長さは10分なので、接続数を固定すれば30分以上も同じ状態になるとは断定できません。30分試験で残ったFDの内訳は取得できておらず、切断回収の不具合かどうかも未確定です。RSSが下がらないことだけでメモリリークとも判断できません。
追加の2分試験では、毎秒30,000件を指定して3,501,137件すべてがHTTP 200となり、FDは6,719個まで増えました。終了後は10個に戻り、120秒間の回復観測でも残存を再現できませんでした。この試験ではFDの種類とTCP状態も保存しています。30分試験の原因は引き続き未確定です。
終了後は0.5秒ごとに/healthを呼び、30秒間応答を確認しました。確認用の接続が一時的にFDを1個使うため、128接続の試験の最後のサンプルは11個ですが、サンプル中と独立したTCP観測では10個への回収を確認しています。
128は測定クライアントの接続数です。Nagi本体に128接続の上限は追加していません。 実運用で小さな上限を一律に設けると、多数の利用者や長い処理の並列性を制限します。
同時接続の限界
| 依頼した接続 | 最初のHTTP成功 | 保持中の再検証成功 | RSS | 切断後のFD |
|---|---|---|---|---|
| 1,000 | 1,000 | 1,000 | 28.49MiB | 10 |
| 10,000 | 10,000 | 10,000 | 246.18MiB | 10 |
| 15,000 | 15,000 | 15,000 | 366.97MiB | 10 |
| 16,000 | 16,000 | 16,000 | 391.07MiB | 10 |
| 16,400 | 16,374 | 16,374 | 400.16MiB | 10 |
| 18,000 | 16,374 | 16,374 | 400.12MiB | 10 |
接続後の最初のHTTP応答に加え、すべての保持中接続で再度HTTP応答を確認しました。TCPの接続成功だけを、処理できた接続数には数えていません。
18,000本を依頼した試験では、TCP接続は16,503本で成功しましたが、HTTP応答を返し、保持中の再検証にも成功したのは16,374本でした。
この環境のFD上限は16,384個です。サーバーは接続以外にもFDを使うため、この値より先に新しい接続を受けられなくなります。OS設定による限界を、Nagi固有の接続上限とは扱いません。
放置・途中送信・異常切断
| 条件 | 120秒の観察結果 |
|---|---|
| 接続後に何も送らない | 100本すべてが残った |
| HTTPヘッダーを途中まで送って停止 | 100本すべてが残った |
| HTTP bodyを途中まで送って停止 | 100本すべて408応答で閉じた |
| 応答後のkeep-aliveを使わず待つ | 100本すべてが残った |
| 全クライアントから通常切断 | 5秒の回復観測中にFDが10個へ戻った |
| 途中ヘッダー送信後に200回の突然切断(RST) | 5秒の回復観測中にFDが10個へ戻った |
各条件100接続を同時に120秒間観察しました。これはローカルで挙動を確かめる小規模な試験です。分散攻撃や実際の公開サービスへの攻撃を行ったものではありません。
待機期限を加えた追加試験
2026年10月1日(UTC)、Nagi 0.1.4の1c013868と、そのHTTP接続待ちに10秒の期限を加えた実装を比較しました。Hyper 1.11.1を改造せずに使っています。以下の数値は、この追加試験の結果です。
接続の回収と通常通信
無通信・途中ヘッダー・途中body・未使用keep-aliveを各100本作った試験では、途中bodyは既存の2秒制限で408となり、残る待機接続も回収されました。FDは410から10へ戻りました。接続は観測前に順番に作成しているため、観測開始からの約10秒を各接続の正確な寿命とは扱いません。200回の突然切断(RST)後もFDは10に戻りました。
別の試験では、無通信・途中ヘッダー・未使用keep-aliveを各350本、計1,050本作りながら、/healthへ毎秒5,000件を25秒間送信しました。
| 項目 | 結果 |
|---|---|
| 実際に送信したリクエスト | 124,977 |
| HTTP 200以外・通信エラー | 0 |
| 通常通信のp99 | 7.01ms |
| 待機接続がすべて閉じた観測時点 | 負荷開始から11.28秒 |
| サーバーFD | 開始時10、観測最大1,092、負荷中の回収後42、終了後10 |
| サーバーRSS | 開始時4.34MiB、観測最大20.08MiB、終了後17.73MiB |
回収後の42個には、通常通信の32接続が含まれます。RSSは開始時まで戻っていません。この短い試験だけでメモリリークや長期安定性は判断できません。最初の試行では負荷生成器が124,997件を送り、すべて200でしたが、測定コードが125,000件ちょうどを要求して失敗しました。実送信数を検証するよう直して再測定した結果が上表です。初回の応答集計も保存しています。
ワーカー数の訂正(2026年10月2日): 上表の回復試験はTOKIO_WORKER_THREADS=1を指定していましたが、Nagiが読む変数はNAGI_THREADSです。未指定時の既定値は4で、元の試験では継承した値を記録していないため、実際のワーカー数は確定できません。小規模な接続回収試験と処理量比較は、共通の測定コードでNAGI_THREADS=1を指定していました。元の結果と測定コードは変更せずに残しています。
同じ0.1.4の変更後バイナリを使い、NAGI_THREADS=1と実際のTokioワーカー1本を記録して追加測定しました。待機接続は同じ1,050本、通常通信は毎秒5,000件・25秒です。
| 項目 | TCP状態を採取した試行 | 両端のTCP状態を採取した試行 |
|---|---|---|
| 実際に送信したリクエスト | 124,997 | 125,000 |
| HTTP 200以外・通信エラー | 0 | 0 |
| 通常通信のp99 | 0.621ms | 0.610ms |
| 待機接続がすべて閉じた観測時点 | 負荷開始から12.37秒 | 負荷開始から11.26秒 |
| 終了後のサーバーFD | 10 | 10 |
この前の2試行では、通常通信は全件成功しましたが、一部クライアントの接続終了を確認できず測定の検証に失敗しました。うち1試行では無通信の25本で終了未確認のまま、サーバーFDは10に戻っていました。原因は未確定です。接続の終了を読み取る観測とTCP状態の採取を追加した後の2試行が上表であり、すべての試行で接続終了を確認できたとは扱いません。TCP状態の採取量と観測期間も異なるため、以前のp99との性能比較には使いません。
上表の試行では、観測終了時にサーバーFDが戻った後も、相手の切断を待つFIN_WAIT2が1,050本ありました。FDの回収は、OS内のTCP状態がすべて消えたことを意味しません。訂正と追加測定の記録には、検証に失敗した試行も含めて保存しています。
短い期限を使う自動試験では、1バイトずつ送ってもヘッダー期限が延びないこと、期限内のkeep-alive再利用、長い応答と低速の受信、ストリーム、アップグレード後のWebSocketが待機期限で切られないことを確認しています。これらは大量のWebSocket配信や公開回線の性能試験ではありません。
通常通信への影響
同じマシンで、変更前・変更後、変更後・変更前、変更前・変更後の順に比較しました。各条件は128クライアント接続・5秒の試験を3回行い、各試行の処理量とp99の中央値を示しています。
| 処理 | 変更前の応答/秒 | 変更後の応答/秒 | 処理量の変化 | p99(前 → 後) | エラー |
|---|---|---|---|---|---|
| HTTP /health | 78,132 | 70,105 | −10.3% | 2.412 → 2.277ms | 0 |
| 4KiB JSON | 40,018 | 38,207 | −4.5% | 4.540 → 5.090ms | 0 |
待機接続を回収する実装では、この条件の飽和時の処理量が下がりました。原因はプロファイルしていません。先に変更前をまとめて測った比較でも、処理量はそれぞれ約7.2%、約5.6%低下しています。順序を交互にした追加試験を上表に載せ、両方の生データを残しています。短時間・1台の結果であり、すべての環境で同じ差が出るとは限りません。
上の処理量比較と小規模な接続回収試験では、サーバーは1論理CPU・1ワーカー、負荷生成器は別の2論理CPUです。CPU割り当てはサーバー0、クライアント1,2、HTTP/1.1・TLSなしのループバック通信で、OSのFD上限は引き上げていません。128はクライアント側の試験条件です。
追加試験の生データとスクリプトには、バイナリ、実装、測定コードのハッシュを記録しています。元の環境記録のsource_commitは作業元のコミットを示すため、変更後の実装の特定にはprovenance.jsonも参照してください。
JSON応答のメモリ確保を削減
0.1.6では、JSON応答の固定Content-Typeを静的な値に変更しました。小さなJSON・4KiB JSON・400/404/503応答の構築を同じプロセスで比較すると、各応答のメモリ確保が1回、割り当て量が16バイト減りました。ステータス、ヘッダー、本文の一致も確認しています。通信や本文の消費は、この割り当て測定に含みません。
現在の10秒の接続待機期限を両方に適用し、変更前後を交互に測りました。各条件は128クライアント接続・10秒を5回、サーバー1論理CPU・NAGI_THREADS=1、負荷生成器は別の2論理CPUです。
| 処理 | 変更前の応答/秒 | 変更後の応答/秒 | 中央値の変化 | p99(前 → 後) | エラー |
|---|---|---|---|---|---|
| /health | 71,069 | 70,186 | −1.2% | 2.721 → 2.859ms | 0 |
| 小さなJSON | 65,438 | 66,191 | +1.2% | 2.947 → 3.127ms | 0 |
| 4KiB JSON | 38,632 | 38,844 | +0.5% | 3.911 → 4.233ms | 0 |
変更していない/healthを含め、試行ごとの差は正負に揺れました。この結果から処理量の向上は断定できません。確認できた効果は、応答構築時の割り当て削減です。全30試行と再現手順を保存しています。
接続管理とDoS対策
現在のserveには1MiBのHTTP body上限と、ハンドラーに対する2秒のタイムアウトがあります。標準では127.0.0.1にのみ待ち受けます。これらが、接続そのものの寿命や公開サービスの通信量まで制限するわけではありません。
接続数の固定上限は設けず、何も送らない接続や途中で止まったヘッダー送信を期限付きにしました。
c602abbでの初回測定は接続期限の導入前の結果です。現在は、初回の無通信・途中ヘッダー・応答後から次のヘッダー完成までを共通10秒で回収します。Hyperを改造せずに使うため、未使用keep-aliveを別の60秒にはしていません。環境変数で期限を変更できます。処理中の応答・ストリーム・アップグレード後のWebSocketは、この待機期限の対象外です。設定はHTTPの説明を参照してください。
| 対策案 | 防ぎたいこと | 決める必要があること |
|---|---|---|
| 切断回収の確認・修正 | 切断後も接続資源が残る | 30分試験の残存FDを再現し、原因を特定する |
| ヘッダー・HTTP待機の期限 | 未送信・途中送信・未使用接続による占有 | 共通10秒を実装済み。公開先に合わせた設定と再接続の影響を確認する |
| body受信・処理待ちの制御 | 遅い送信や過剰な仕事の滞留 | 既存タイムアウトとの関係、待ち行列の上限 |
| 前段のプロキシ・CDN | 公開サービスへの大量通信 | 公開先、通信量・接続数・送信元制限の方針 |
公開環境の制限は、この待機期限とは別に必要です。DDoSで回線自体を使い切られる場合は、Nagiの中だけで対処できず、前段の防御も必要です。
測定環境と再現手順
| 項目 | 条件 |
|---|---|
| Nagi / Rust | 0.1.2 / 1.98.1 |
| OS | Linux x86_64 |
| CPU | AMD EPYC 7763 64-Core Processor |
| CPU割り当て | cgroup上限4CPU、使用可能5論理CPU |
| メモリ上限 | cgroup 16GiB |
| サーバー / 負荷生成器 | CPU 0 / CPU 2,3 |
| サーバーFD上限 | soft・hardとも16,384 |
| HTTP / SQLite | HTTP/1.1、TLSなし / メモリ内DB |
| ビルド | release、opt-level=3、LTO無効 |
サーバーは1論理CPU・1実行ワーカー、負荷生成器は別の2論理CPUへ固定しました。SQLiteには専用のDBワーカーがあります。OSのFD上限は引き上げていません。各短時間試験は新しいサーバー・DBで開始し、ウォームアップを測定時間から外しています。
Linux/cgroup v2、Python 3.12、Rust、taskset、Vegeta v12.13.0を使います。wrk試験はwrk 4.2.0が必要です。同じ言語実装を再現する場合はc602abbをビルドし、このPRの計測スクリプトをそのチェックアウトへコピーしてください。
cargo build --locked --release -p nagic
NAGI_ROOT="$PWD" NAGI_NATIVE_TARGET_DIR="$PWD/native-target" \
./target/release/nagic build examples/crud.nagi --out build/crud
python scripts/http_capacity.py --phase limits --duration 8 --repeats 3 \
--vegeta /path/to/vegeta --out build/http-limits
python scripts/http_capacity.py --phase soak --soak-seconds 1800 --soak-rate 15000 \
--vegeta /path/to/vegeta --out build/http-soak
python scripts/http_saturation.py --wrk /path/to/wrk --out build/http-saturation
python scripts/tcp_capacity.py --out build/tcp-capacity
python scripts/http_connection_lifecycle.py --out build/connection-lifecycle
CPU番号の既定値はサーバー0、クライアント2,3です。利用できない場合は--server-cpusと--client-cpusで、重ならない番号を指定します。負荷を掛ける対象は固定の127.0.0.1:8080です。出力先の既存測定は上書きしません。
生データ・実行条件・測定時のスクリプトを同梱しています。元の測定を残したまま、グラフを再生成できます。図の生成にはmatplotlibが必要ですが、計測には不要です。
python scripts/http_capacity_report.py \
--data benchmarks/results/http-capacity-2026-10-01 \
--assets website/assets/http-capacity
TLS、実回線、永続化するSQLiteの長時間書き込み、WebSocketの大量配信、24時間以上の連続稼働は未測定です。以前の測定とは負荷生成器やCPU割り当てが異なるため、数値をそのまま性能の改善・悪化として比較できません。