低速受信とAPI応答の実験データ
2026-11-02測定。記事本文の数値は本測定だけを集計しています。
条件
Linux 6.12 / aarch64 / glibc 2.36、CPython 3.11.2、Gunicorn 23.0.0、NGINX 1.22.1(Debian 1.22.1-9+deb12u10)。
- 主条件: Gunicorn sync 2 workers、経路3通り(直結、NGINX buffering off / on)、低速0/1/2接続、各3回。
- 補助条件: Gunicorn gthread 1 worker / 4 threads、直結、低速2接続、3回。並列処理枠が異なるので、ワーカー方式自体の性能優劣の比較には使いません。
/fast: 12バイトJSON、毎秒8件を125ms間隔で9秒間送信、1試行72件。応答完了を待たずに次の要求を送信。/large: 同じ64KiBを256回返す16MiBの固定応答。各低速クライアントは約2MiB/sで読み出し、約8秒で受信。低速接続開始からfast要求開始まで約250ms。- 低速側のSO_RCVBUF設定8KiB、Linuxが返した値16KiB。asyncio StreamReaderのlimitは16KiB。これは実回線の帯域制御ではなく、受信アプリの読み出しペースの制御です。
- 全HTTPは同一ホストの127.0.0.1、HTTP/1.1、平文、接続は応答後に終了。TLS・圧縮・キャッシュなし。既存アプリや公開URLへの測定アクセスなし。
- CPU最大: アプリ0.25コア、NGINX0.20コア、制御/負荷生成0.20コア。CPU制限周期10ms。メモリ上限: アプリ128MiB、NGINX96MiB、制御128MiB。
- on/off以外のNGINX設定は同じ。proxy_buffer_size=16k、proxy_buffers=4×16k、proxy_busy_buffers_size=32k、proxy_max_temp_file_size=32m/リクエスト、proxy_temp_file_write_size=64k。NGINXは1プロセスで動作(master_process off)。
ファイルと集計
conditions.csv: 主条件別。p50/p95は各試行内のnearest-rankパーセンタイルを計算し、3試行の中央値。p95_min/maxは3試行の最小/最大。app_send_sとclient_receive_sは各試行2応答の中央値を求め、その3試行中央値。temp_peak_mibは3試行で観測した最大合計サイズ。runs.csv: 30試行の値。CPU coreはcgroup CPU時間増分/測定時間で、1コア=1.0。throttled_msは同じ区間のcgroup throttled_usecの増分。memory_peak_bytesは約200msのサンプルでのmemory.current最大で、瞬間的な真の最大とは限りません。rss_peak_bytesはcgroup内プロセスRSSの合計最大で、共有ページを重複計上し得ます。requests.csv:/fastの全2160件。時刻は各試行の測定開始を0秒とした相対値。latency_msは実開始〜本文の受信完了。schedule_lag_msは予定〜実開始。connect_msはTCP接続確立まで。statusは全件200。large-responses.csv:/largeの33応答。receive_sは接続確立後の要求開始〜最後のチャンクを読み出しペースに従って消費した時刻。app_send_sはアプリの応答ジェネレーター開始〜最後のチャンクをサーバーが書き込んだ後の時刻で、利用者への到達ではありません。resource-samples.csv: 約200ms間隔の時系列。temp_logical_bytesはNGINXが開いているproxy_tempファイルの合計論理サイズ、temp_allocated_bytesはst_blocks×512の合計。名前を削除したopen中のファイルも/proc/PID/fdから計上し、inodeで重複排除。物理I/O量ではありません。memory_bytesはcgroup memory.currentで、RSSではありません。app.py,experiment.py: 本測定で使ったアプリと測定コード。実行先のホスト名や認証情報を含むSSHランチャー、元の内部ログは公開用資料に含めません。dependencies.json: 配布物のURL、取得バイト数、SHA-256とNGINXパッケージ版。
API遅延グラフの点は3試行の中央値、縦線は最小〜最大です。送信/受信の棒グラフは低速2接続の6応答の中央値、黒点は個別の送信時間です。時系列図は経路ごとにp95が中央の試行を選んでいます。図によって集計単位が違うため、それぞれに明記しました。
再現について
標準ライブラリの測定コードはLinuxのsystemdユーザーサービスとcgroup v2を必要とします。prepareはDebian bookworm-securityのarm64索引と配布物を専用ディレクトリへ取得・展開し、ホストへパッケージをインストールしません。現在の配布索引から取得するため、再実行時のNGINXパッケージ版が測定時と同じとは限りません。完全に版を揃える場合はdependencies.jsonのURLとSHA-256を確認してください。
以下は元の環境と同じ条件のLinuxホストでの例です。リソースに余裕があることを確認してから実行します。既存のサービス・クラスタへ設定を適用する手順ではありません。
lab_root=$(mktemp -d /tmp/kamui-slow-client-XXXXXXXX)
cp app.py experiment.py "$lab_root/"
python3 "$lab_root/experiment.py" --root "$lab_root" --stage prepare
lab_unit="$(basename "$lab_root")-controller.service"
systemd-run --user --wait --pipe --collect --unit "$lab_unit" \
-p CPUQuota=20% -p CPUQuotaPeriodSec=10ms -p MemoryMax=128M \
-p MemorySwapMax=0 -p TasksMax=24 -p RuntimeMaxSec=720 -p TimeoutStopSec=3 \
/usr/bin/python3 "$lab_root/experiment.py" --root "$lab_root" \
--stage main --controller-unit "$lab_unit"
各試行のサーバーは測定コードのfinallyで停止します。実行後は結果を回収し、当該実験の専用サービス・プロセスが残っていないことを確認したうえで、作成した専用一時ディレクトリだけを片付けてください。
除外と限界
8MiB・1MiB/sの探索的予備測定と16MiB条件の予備測定は本測定に混ぜていません。8MiB・1MiB/sから16MiB・2MiB/sへは応答サイズと読み取り速度の両方を変えているため、条件間の差をサイズ単独の効果とは解釈しません。本測定はホスト全体のCPU閾値超過で一度安全停止し、未完了試行を除外して、完了済み17試行の後の13試行を再開しました。安全閾値やアプリ条件は変更していません。
受信側のreceive_sは、最後のソケットread直後の時刻ではありません。各読み取り後に設けた速度調整の待ち(最後の読み取り後も含む)を終えた完了時刻までを測ります。約8秒という値はこの受信ペース制御を含み、実回線の純粋な転送性能を示しません。gthread補助条件はワーカー方式・プロセス数・同時対応数を同時に変更しており、方式だけの効果や最適な担当数を切り分ける比較ではありません。
アプリはフレームワークなしで書いたWSGIアプリです。Flask・Django・FastAPI等のフレームワーク間比較、非同期方式の測定、実サービスとしてのPaaS構成の測定は実施していません。TCPバッファの個別使用量やワーカースタックも採取していないため、実測した書き込み時間・API遅延・一時ファイルと、仕様に基づく説明を区別します。
全条件を最大処理能力まで負荷をかけた実験ではありません。OSの各TCPバッファ容量、TCP_INFO、実インターネット回線、TLS、HTTP/2/3、SSE、WebSocket、低速アップロード、長時間・多数接続、ディスク枯渇を測定していません。他のWebサーバーやPaaSの性能保証には使えません。