世界を動かす技術を、日本語で。

CGI-binを使用して1日あたり2億件のリクエストを処理する

概要

  • 90年代CGI技術 が現代ハードウェアで再評価
  • Go + SQLite によるCGIプログラムで高いリクエスト処理性能を実証
  • CGIの プロセスごと起動の欠点 と現代CPUの性能向上
  • GoやRust の高速起動性がCGIの弱点を補完
  • 現代的CGI活用 の可能性と制限

90年代CGIと現代ハードウェアの再評価

  • Jake Goldが Go + SQLite を使い、 CGIプログラム の性能検証を実施
  • 16スレッドの AMD 3700X1秒あたり2400リクエスト超、1日2億リクエスト 処理を達成
  • 90年代ウェブ開発で主流だった CGI の仕組みは、 リクエストごとにプロセスを起動・終了
  • 当時は プロセス起動のオーバーヘッド が問題となり、 PHPやFastCGI などの技術が登場
  • その経験から「 プロセス起動は非効率」という認識が長年定着

ハードウェア進化と新言語によるCGI再評価

  • 近年の CPU性能・メモリ速度 の大幅向上
  • GoやRust など、 高速起動 が可能な言語の登場
  • CGI方式は マルチコアCPU を最大限に活用可能
    • 各リクエストが 独立したプロセス として処理
    • 384スレッド 対応サーバや 16CPU の小型VMでも高効率
  • Jake Goldは「 現代のCGIは意外と有効」と指摘

CGIと現代Web開発の適用可能性

  • CGI方式 は一部用途で 有効な選択肢 になり得る
  • GoやRust の利用で 従来の欠点 を大幅に緩和
  • ただし、 すべてのWebアプリケーション に推奨できる手法ではない
  • 技術的興味 として再評価の価値
  • 1998年風のWebアプリ開発 も現代技術で現実的選択肢

CGI技術の今後の展望

  • プロセス分離 による セキュリティ・スケーラビリティ の利点
  • クラウド環境サーバレス 技術との親和性
  • 従来の常識 を疑い、 新旧技術の融合 による最適解探索

Hackerたちの意見

昨日も話題になったこと: https://news.ycombinator.com/item?id=44464272

新興のウェブコミュニティはすぐにこれは悪いアイデアだと気づき、PHPのような技術を発明しました。実際には、ここで重要だった技術はmod_phpです。PHP自体はPerlと実行方法に違いはなかったけど、mod_phpの設計選択がmod_perlと比べて、PHPスクリプトをサーバーにそのまま置いて高速に実行できる理由でした。mod_perlでは少し考えたり魔法が必要でした。

そうだね、でもmod_phpはPHPエコシステムへの早期の追加で、すぐにデフォルトのデプロイ方法になったんだ。最初のApacheモジュールは1996年のPHP/FIバージョン2.0用だったと思う: https://www.php.net/manual/phpfi2.php#module

あの時、友達と一緒に「学習管理システム」って呼ばれるものを開発してたんだ。それにはコンテンツ管理、課題アップロード、イベントカレンダー、成績管理、リアルタイムチャット、フォーラム... ぜんぶCGIでプレーンCだったから、マジで地獄だった。PHPのことを知ったとき、RFCを読んだりHTTPをリバースエンジニアリングしたりして、自分たちが一生懸命プログラミングしてたことが、PHPではただの関数呼び出しだったって知ったときは、涙が出そうになったよ。自分たちのボロいurlencode実装をデバッグしたり、HTTPヘッダーの余計なキャリッジリターンで一日を無駄にしたりすることもなくなるんだから...

これについては、素早くプロトタイピングするためのワークフローの一部として考えたこともある。少なくとも現代のJIT言語の多くは、スタートアップ時間がインポートに支配されると思う。fastcgiモデルを使わない限りね。h2oウェブサーバーをローカルスクリプト用に採用し始めたときにこれが浮かんできた。mrubyとfast-cgiハンドラーでクリーンで書きやすい設定ファイルがあり、しかもめちゃくちゃ速いから: https://h2o.examp1e.net/configure/fastcgi_directives.html これが役立つもう一つの場所は、顧客が自分のカスタムコードでローカルソフトウェアを拡張できるようにすることだ。だから、AIツールを拡張するのにMCPを使う代わりに、CGIを通じて特定のリクエスト構造を実装すればいい。

CGIプログラムのためのMCPフロントエンドは、エンドユーザー環境には悪くないアイデアだと思う。MCPサービスもCGIとして実装できるのではないかと考える。MCPフレームワークは、両方の実行モードをサポートするプログラムとして機能を公開するかもしれない。仕様を詳しく調べてみないと。

Fastcgiはcgiの利点を全部失っちゃうけどね。

Pythonのようなものでも、最近のCGIはかなり速いよ。CGIスクリプトが起動に400ミリ秒のCPUを使うとして、サーバーが64コアなら、1秒あたり160リクエストを処理できる。つまり、1日あたり1400万ヒットになる。これは高トラフィックのサイトだね。つまり、もしあなたのウェブサービスが静的な「アセット」を除いて1日あたり数百万のリクエストを処理するのに苦労しているなら、CGIプロセスの起動がボトルネックではない。数年前は「もちろん、Pythonの標準ライブラリでずっとサポートされている退屈な技術だ」と言ってたけど、どうやら残っているPythonのメンテナは、コードの安定性や退屈な技術との後方互換性が実際には有害だと思っているみたいで、退屈で安定しているモジュールは標準ライブラリから削除している。これは本当に言ってるんだ。cgiモジュールは3.13で削除された。プロトタイピングにはPythonを使う習慣がまだ残ってるけど、ここ25年間毎日使ってきたから、今はそれを後悔してる。JSとLuaの間でちょっと迷ってる。

cgiを削除する理由はこちら - https://peps.python.org/pep-0594/#cgi 面白いことに、これは2000年7月14日のhttps://peps.python.org/pep-0206/にリンクしていて(25年前!)、その当時からcgiパッケージは「設計が悪く、今では修正がほぼ不可能」と説明されていた。どうやら、https://github.com/jackrosenthal/legacy-cgiパッケージが標準ライブラリモジュールのドロップイン置き換えを提供しているみたい。

わからない。PythonがstdlibからCGIを削除することに文句を言うのはいいけど、じゃあJSを考える方がいいって言うのはどういうこと?JSには標準ライブラリすらないじゃん。LuaもstdlibにはCGIモジュールがないよ。

そうだね、高パフォーマンスなウェブは昔はアートみたいなもんだった。でも今は、早く出すためにやってた無駄なことを見つけて、それをやめることが大事だよ。頑張れば、ストレージ以外でほとんど遅延を出さずにアプリを作れるはず。

Perlを考えてみて。Pythonほどバッテリー内蔵じゃないけど、ほぼどこでもプリインストールされてるし、JSやLuaよりも安定してるよ。(Pythonもね。)

でも、400msは今の時代では受け入れられないね。

一言いいたいんだけど。

Pythonのメンテナーたちが_cgiというモジュールを削除するけど、CGIスクリプトを実装するためのサポート、つまりhttp.serverモジュールのCGHTTPRequestHandlerは残すみたい。cgiモジュールにあったのは、HTMLフォームデータを解析するためのいくつかの関数だけだったからね。

JITがあるから、PHPかJSの方がいいかな。Pythonは1.6から始めたけど、主にOSスクリプト用だった。1999年から2003年にかけて、ApacheやIISモジュールでTclを使って苦い経験をたくさんしたから。

CGIの怖いところは、フォークよりもシェルの狂気だと思う。シェルショックのRCEの後、多くのサーバーはおそらくCGIプロセスを直接実行するように切り替えたんじゃないかな。

Zen 6cは、ソケットごとに256コア、512 vCPU/スレッド、デュアルソケットシステムで1024 vCPUを搭載する予定なんだ。これで1秒あたり2560リクエスト(またはページビュー)が処理できるけど、キャッシュは含まれてないんだよね。確か、これは8台のサーバーでStackExchangeが日平均で処理していたリクエストの半分くらいだったと思う。GoやCrystalを使えば、少なくとも10倍、下手したら20倍にはスケールするだろうね。ただ、メモリコストが下がってないのが問題で、グラフのどこかでプロセスごとのメモリコストがこの利点を上回ることになると思う。それでも、やってみるのは楽しそうだね。少なくともCGI-BinやPerlの時代を経験した私たちには。

このハードウェアで「Hello World」アプリが2400 rpsって、ちょっと悪くない? で、パフォーマンスを何と引き換えにしてるの? コードは全然シンプルになってないし。

2000 rps以上が必要な場合だけ悪いよ。それは世の中のサイトの中でもほんの一部だから。

そんなに悪くはないけど、多くのユースケースには十分だよ。HNの「ハグ・オブ・デス」も耐えられるはず。

最近、サイドプロジェクトで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]

Rasmus Lerdorfの言葉を引用すると: 「私は本当のプログラマーじゃない。動くまで適当に組み合わせて、動いたら次に進むだけだ。本当のプログラマーは『うん、動くけど、メモリリークがどこにでもあるよ。これ直した方がいいんじゃない?』って言うだろうけど、私は10リクエストごとにApacheを再起動するだけ。PHPはそれ以来ずいぶん進化したけど、その大部分は初期のミスを修正することだった。」「PHP 8は、私のコードがずっと少なくなったから、かなり良くなった。」

Apacheがファイルシステムを監視しない理由がわからない。この選択は、99.99%のHTTPリクエストが不必要なディスク読み込みで遅くなることを意味してるんだよね。

仕事では、時々簡単な内部ウェブアプリ用にcgi-binディレクトリを使ってる。シンプルに保てば、使い勝手はすごくいいよ。cgiだからって、http/1.0を標準出力に手動で出力する必要はないし。例えば、Pythonの組み込みのwsgiref.handlers.CGIHandlerを使えば、どんなwsgiアプリもcgiスクリプトとして実行できる:

import wsgiref.handlers
import flask

app = flask.Flask(__name__)
wsgiref.handlers.CGIHandler().run(app)

スクリプトの実行はuwsgiとそのcgiプラグインでやってる。mod_cgiのためだけにApacheやlighttpdを動かすより、シンプルで柔軟だと思う。uwsgiはsystemdユニットとして動くから、systemdのハードニングやサンドボックス機能も使えるのが便利。mod_cgiにはない、特定のファイルタイプのインタープリターを設定する機能もある:

cgi = /cgi-bin=/webapps/cgi-bin/src
cgi-allowed-ext = .py
cgi-helper = .py=/webapps/cgi-bin/venv/bin/python3

依存関係はここに全部入る。最初のバイトまでの時間は250-350msで、私たちのユースケースには許容範囲だね。

CGIを使ったCプログラムを使ってた頃を思い出す。かなり速かったし、もう何十年も前の話だよ。当時は100コア以上のCPUやスレッドもなかったし、RAMも豊富じゃなかった。せいぜい1GBくらいだったけど、当時はそれでもできたし、今はもっとできるはず。

newsproみたいな古いものの話だけど、CuteNewsやsNews(単一ファイルCMS)みたいなphpスクリプトを覚えてる人いる?

CGIバイナリがDBにアクセスする必要がある場合、プロセスが始まるたびに接続を開かなきゃいけないんだよね。例えば、fastcgiを使ってコードをメモリに保持するのは、起動時間のペナルティを避けるためだけじゃなくて、DB接続プールを持ったり、少なくともスレッドごとに持続的なDB接続を確保するためでもあるんだ。