CGI-binで1日2億件のリクエストを処理する
(simonwillison.net)- Jake Goldは16スレッドのAMD 3700XでGo + SQLiteのCGIプログラムを動かし、1990年代式のCGIが現代のハードウェアでどの程度通用するのかを確かめた
- 結果は、ありふれたハードウェアでも毎秒2400件以上、1日あたり2億件以上のリクエスト処理が可能であることを示した
- CGIはリクエストごとにプロセスを起動・実行・終了するため、過去にはオーバーヘッドが大きく、これを減らすためにPHPやFastCGIのような方式が登場した
- Simon Willisonは2020年にdatasette-ripgrepでRust製のripgrep CLIを呼び出して検索を処理し、Webリクエスト中のプロセス実行は避けるべきだという長年の思い込みを改めることになった
- GoやRustのように起動が速い言語とマルチCPU環境では、CGI的な処理が以前より現実的な選択肢になりうるが、一般的に推奨される方法ではない
現代ハードウェアで見直すCGIの性能
- Serving 200 million requests per day with a cgi-binは、Jake GoldがGo + SQLite CGIプログラムで1990年代式CGIの性能をテストした記事
- テスト環境は16スレッド AMD 3700Xベースのシステム
- 核心となる結果は、CGIでもありふれたハードウェアで毎秒2400件以上、1日2億件以上のリクエストを処理できるという点
- CGIは、到着したリクエストごとに別プロセスを起動し、実行後に終了する方式
- 初期のWebコミュニティはこのオーバーヘッドを避けるため、コードをメモリ常駐させて追加コストを減らすPHPやFastCGIを生み出した
CGIは昔ほど悪くないかもしれない理由
- 過去のCGIのボトルネックの1つは、Perl、Python、Javaのように非常に高速な起動を目標に設計されていない言語でWebスクリプトを書いていたこと
- 今日ではGoやRustを使えば、CGIスタイルのリクエスト処理ははるかに効率よく動作しうる
- Simon Willisonは2020年にdatasette-ripgrepを作った際、Rustで書かれたripgrep CLIツールをシェルから呼び出して検索を実行し、良い結果を得た
- CGIプログラムは別プロセスとして実行されるため、複数CPUを活用する構成とも相性がよい可能性がある
- 現代のサーバーは384 CPUスレッドを持つことがある
- 小規模なVMでも16 CPUを持てる
- CPUとメモリの性能も過去より大幅に向上している
- 1998年式のWebアプリケーション開発手法も、GoやRustと組み合わせれば興味深い実験対象になるが、ほとんどのサービスが従うべき基本選択肢ではない
1件のコメント
Hacker Newsの意見
Pythonのようなものを使っても、最近のCGIはかなり速い
CGIスクリプトの起動にCPU 400msかかり、サーバーが64コアなら、秒間160リクエスト、サーバー1台あたり1日1,400万ヒットを処理できる
静的アセットを除いて1日数百万リクエストでも苦しいWebサービスなら、ボトルネックはCGIプロセスの起動時間ではない
昔なら「Python標準ライブラリで長くサポートされてきた退屈な技術」と言っただろうが、残ったPythonメンテナーたちは安定性と後方互換性を有害だと見ているようで、退屈すぎて安定しすぎたモジュールを標準ライブラリから削除してきた
cgiモジュールはPython 3.13で削除された
この25年の大半を毎日使ってきたので、今でもプロトタイピングにはPythonを使う癖があるが、今ではそれが残念で、JSとLuaの間で悩んでいる
興味深いことに、そこでは2000年7月14日、つまり25年前のhttps://peps.python.org/pep-0206/にリンクしており、その時点でもcgiパッケージを「設計が悪く、もはや修正はほぼ不可能」と表現していた
https://github.com/jackrosenthal/legacy-cgiパッケージが標準ライブラリモジュールのドロップイン代替を提供しているようだ
そのサポートはhttp.serverモジュールのCGIHTTPRequestHandlerに残っている
cgiモジュールにあったのは、HTMLフォームデータをパースするいくつかの関数だけだった
ただ、その次の代替として標準ライブラリ自体がないJSを検討するというのはよく分からない
Luaにも標準ライブラリにCGIモジュールはない
今では、速くリリースするために入れたとんでもなく無駄の多い部分を見つけ、それを止めることに近い
きちんと試せば、アプリはストレージアクセス以外にはほとんどレイテンシを増やさないこともある
Pythonは1.6から学んだが、主にOSスクリプティング用途で使ってきた
1999〜2003年にApacheとIISモジュールでTclを使い、モジュールをCで書き直し続けながら、あまりにも多くの教訓を苦労して学んだ
最近、350ドルのミニサーバーでGolangバイナリ、RabbitMQ、Redis、MySQLをすべて同じマシンに載せてテストしたところ、秒間5,000リクエストを継続処理できた
24時間換算では4億リクエストになる
最近の無料ツールがどれほど優れているかに驚くし、それでも私たちがクラウドプロバイダーにあれほど多くのお金を払っていることにも驚く
完全に同じ比較ではないが、地下室の機材で全部を開発してチューニングする過程は本当に良かった
「Googleがそうしているから」以外に理由もなく、こうしたオーバーヘッドを負い続けるのは変だ
私たちがうまく使っているモジュラーモノリスアーキテクチャについて、ぜひ記事を書かなければ
ホスティング会社で専用サーバーを借りて、好きなだけ使えばいい。ただし帯域幅や転送量の制限に大きく縛られる
クラウドが使われるのは利害関係が多いからだ。たとえばVCや投資家がクラウド会社の株式も持っていて、実際には来ない想像上のトラフィック急増のせいで投資が台無しになるのを恐れている
クラウド営業は投資家の不安を突くのがうまい
旅行中にドライブが故障したらどうしようもなく、うっかりロックアウトしてしまうとfallback用のシリアル端末もない
r/homelabの人たちのように構成することもできるが、そうなると特に自分の時間も含めたとき、本当にお金を節約できているのかも不明になる
結論として、クラウドプロバイダーは規模の経済のおかげでかなり良い取引だと思う
奇妙なことに、片方だけを個別に得ることができない
20TBドライブ4台とそこそこのCPUだけが付いていたら、どれほど大きな事業やサービスを運営できるか想像してみてほしい
クラウドプロバイダーからそういう構成を得るのは簡単ではない
「初期のWebコミュニティはこれが悪い考えだとすぐに学び、PHPのような技術を発明した」という部分は、厳密に言えば中核技術はmod_phpだった
PHP自体は実行方式ではPerlと変わらなかったが、mod_perlと比べたmod_phpの設計上の選択のおかげで、PHPスクリプトはサーバーにそのまま放り込んでも高速に実行され、mod_perlは動かすには少し考えと魔法が必要だった
コンテンツ管理、課題アップロード、イベントカレンダー、成績管理、リアルタイムチャット、フォーラムがあり、すべてCGI上の純粋なCだったので、作業は地獄のようだった
PHPを知った日にほとんど涙が出そうになったのは、RFCを読んだりHTTPをリバースエンジニアリングしたりしながら一から苦労して組んでいたすべてが、PHPでは単なる関数呼び出しだったからだ
もういい加減なurlencode実装をデバッグしたり、HTTPヘッダーの奇妙なキャリッジリターン1つのせいで1日を無駄にしたりしなくてよくなった
Apacheモジュールの最初のバージョンは、1996年のPHP/FI Version 2.0向けだったようだ: https://www.php.net/manual/phpfi2.php#module
cgi-bin が DB にアクセスする必要があるなら、プロセスが起動するたびに接続を開かなければならない
FastCGI のようにコードをメモリ上に残しておくのは、起動時間のコストを避けるためだけではなく、DB 接続プール、少なくともスレッドごとの持続的接続を持てるからでもある
「Python はシングルスレッドだからたくさん立ち上げよう」と「Python は遅いからたくさん立ち上げよう」を同時にやったときがそうだった
規模が大きくなると、結局 pgbouncer のような Python の外側にある共有接続プールと多くのチューニングで、DB を落とさずに負荷をさばけるようにしなければならない
もちろん、その後ある程度の性能が出るマルチスレッド言語で再実装したら、また非常に単純になった
実質的にプロキシとして動作する別デーモンを立ち上げる方式だ
TCP/IP の代わりに Unix ソケットを使えば、接続コストは比較的低くなる
このハードウェアで hello world アプリが秒間 2,400 リクエストなら、少し悪いのではないか
性能を犠牲にして正確に何を得ているのかも分からない。コードがより単純になったわけでもない
そういうサイトは全体の一部にすぎない
HN のトラフィック爆弾にも耐えられそうだ
昨日も議論されていた: https://news.ycombinator.com/item?id=44464272
会社では、たまに素早く作る社内 Web アプリ用に、今でも cgi-bin ディレクトリを提供している
単純に保てば使い勝手がよい
CGI だからといって HTTP/1.0 を stdout に直接 print しなければならないという意味ではない
例えば Python の組み込み wsgiref.handlers.CGIHandler を使えば、任意の WSGI アプリを CGI スクリプトとして実行できる
import wsgiref.handlers, flask
app = flask.Flask(name)
wsgiref.handlers.CGIHandler().run(app)
私たちはスクリプトを uwsgi とその CGI プラグイン[1]で実行している
mod_cgi のために Apache や lighttpd を立ち上げるより、より単純で柔軟だと感じている
uwsgi は systemd ユニットとして実行されるので、systemd の強化機能やサンドボックス機能もすべて活用できる
uwsgi の CGI 処理で mod_cgi にはない便利な点は、特定のファイル形式に使うインタプリタを指定できることだ
cgi = /cgi-bin=/webapps/cgi-bin/src
cgi-allowed-ext = .py
cgi-helper = .py=/webapps/cgi-bin/venv/bin/python3 # all dependencies go here
first byte までの時間は 250〜350ms で、私たちの用途には許容範囲だ
[1]: https://uwsgi-docs.readthedocs.io/en/latest/CGI.html
最近サイドプロジェクトで Apache を使ったが、理由の一つが .htaccess 機能だったので、似たような会話をした
.htaccess ファイルをどこにでも置けば、Apache が各リクエストごとに追加のサーバー設定として読み込める: https://httpd.apache.org/docs/2.4/howto/htaccess.html
これを避けていた大きな理由は性能だった。各リクエストごとに追加のディスクアクセスが必要で、可能ならメイン設定ファイルに入れるほうが常に良かった
しかし今では、ほとんどのサーバーに SSD があり、Linux がファイルシステムキャッシュに使える余裕のある RAM もある可能性が高い
もちろん Apache が設定を一度ではなく各リクエストごとにパースしなければならないので、性能は依然として少し悪くなる
それでも、最近のほとんどのサーバー CPU がより強力であることを考えると、多くのユースケースでは受け入れられる
サイドプロジェクトはごく初期版だが、すでに利用中だ: https://github.com/StaticPatch/StaticPatch/tree/main
「私は本物のプログラマーではない。動くまであれこれ継ぎ足して、動いたら次へ進む。本物のプログラマーなら『なるほど、動いてはいるが、あちこちでメモリが漏れているね。直すべきでは?』と言うだろう。私は Apache を 10 リクエストごとに再起動すればいい。」
PHP はその後非常に遠くまで来たが、その大きな部分は初期の失敗を正す過程だった
「PHP 8 がはるかに良い理由は、私のコードがはるかに少なく入っているからだ。」
この選択のせいで、HTTP リクエストの 99.99% が不要なディスク読み取りで遅くなる
ラピッドプロトタイピングのワークフローの一部として、これについてさらに考えてみた
現代的な JIT 言語は、FastCGI モデルにしない限り、起動時間の大半が import に支配されそう
ローカルスクリプトに h2o Web サーバーを使い始めてからそう思うようになった。mruby と FastCGI ハンドラーで設定ファイルをきれいかつ素早く書けるし、速度も非常に速い: https://h2o.examp1e.net/configure/fastcgi_directives.html
もう一つ有用なのは、顧客がローカルソフトウェアを自分のコードで拡張できるようにする場合
例えば AI ツールを拡張するために MCP を使わせる代わりに、特定のリクエスト構造を CGI として実装してもらうことができる
MCP サービス自体も CGI で実装できるのではないかと気になってきた
MCP フレームワークが、両方の実行モードをサポートするプログラムの形で機能を公開することもできそう
仕様をもう少し掘ってみる必要がありそう
自分にとっては
inetdがまさに CGI だったおかげでインターネットがずっと面白くなった
自分で inetd を使って複数のシェルスクリプトをホストしていたし、完全に Bash で書いた HTTP サーバーもあった
その VPS はずいぶん前になくなり、当時はバージョン管理も使っていなかった
それを書いたノートパソコンもなくなった
それでも本当に楽しかった
デプロイは Makefile + scp と同じくらい簡単で、テストは複数の
netcatと grep を組み合わせた別の Bash スクリプトだった本当に良い時代だった