CGIスクリプトは何で書く?
FastCGIで10倍高速化

2003年2月15日 Matchan

CGIは何語で書くか?

webでアクティブなページ、ダイナミックなページを作る普通の方法は、CGI (Common Gateway Interface)を使うことです。 CGIは、ユーザー(ブラウザ)からのリクエストに応じてweb server 上で走るプログラムで、結果としてhtmlや画像ファイルを生成し、 web serverがそれをユーザーのブラウザに届けます。データベースの検索結果を表示するページなどは、まちがいなくサーバー側でCGIが実行されています。

CGIの記述には、perlやphpなどのインタプリタ言語が使われることが多いようです。 sh, bashなどのシェル言語も多少使われることがありますが、Cのようなコンパイラ型言語で記述されることはほとんどありません。これらの新興言語(?)が良く使われる理由は、.比較的初期から CGIインタフェースが用意されたこと, .postgresなどのデータベースへの接続インタフェースが 用意されたこと,の二つではないかと思います. (3番目にオブジェクト指向であったことを付け加えたくなりますが)。 PerlやPhpには、多くの機能がライブラリとして組み込まれますから、何かをやるとき、 たとえば単語の数を数えるのに、いちいちプロセスを立ち上げるようなことはないので、 shよりはずっと効率が良く、文字列処理も得意です。 なぜコンパイラ型言語が使われないかと言えば、Cなどはなかなか一般の人には手を出しづらいから、 処理時間は気にしないから(だって使うのは自分じゃなくて他の人だもの?)、ではないかと思います。 Cでは開発に手間がかかるのは確かですが、 「CGIの処理時間は気にしなくて良い、シェルより速いからいいじゃないの」、 というのは間違っているのではないか、というのが本稿の1番目の趣旨です。

CGIに与えられた処理時間

処理時間は気にしなくてよいのか?CGIに与えられた処理時間は、ユーザーがボタンやリンクをクリックしてから次の画面が表示されるまでの数秒が限度です。もしそのサイトがインターネットの人気サイトで、一秒に一回ものアクセスがあるならば、1秒以内で終わらせないとサーバーはパンクすることになります。通信時間も考慮しなければなりません。ネットワークのlatencyとして1秒くらいを、先ほどの許容限度とした数秒から差し引く必要があります。さらに一つの画面が一つのCGIを走らせて完結するとは限りません。そのよい例がアクセスカウンターでしょう。フレームを使ったページだと各々のフレームがCGIということもあるかもしれません。

という一般的な考え方が可能でしょう。そこで思い当たるのが、1990年代前半のインターネットの黎明期のこと。当時、この二つのシナリオのうち、世界中のほとんどのサイト(といっても数は今とは比べものにならないでしょうが)が、第2ののシナリオを採用する環境にあったことでしょう。アクセスは滅多に来ない、通信速度は遅い、CGIなんて一つだけ、だったらCGIはPerlで十分さ、ということではなかったでしょうか?そしてそれが、その後もCGIはPerlで書くもの、と信じ込まれて誰かのコードを借りてきては手を入れて、というふうにはびこって来たのではないでしょうか?

Perlは速いから大丈夫?

さて、次に問題にするのは、PerlのCGIプログラムが立ち上がる過程です。web serverは、httpのリクエストを受けてCGIを起動しますが、それは、Perlのインタプリタを起動し、 PerlがCGIプログラム(スクリプト)を読み込んで実行し、結果をhtmlドキュメントとしてweb serverに送る、という手順になります。 上で述べたように、一つのCGIは0.1〜1秒くらいで走り抜けてしまいますが、 そういう短時間の中では、起動に関わる時間が相対的に大きな比重を占めるのです。

下の表は、Perl, PHP, sh という普通のCGI scriptingに使われそうな インタプリタ言語のテキストサイズ(プログラムコードのサイズ)、 仮想メモリ、常駐メモリ、を示します。何も処理をしないときですから、 これらが最小値です。 これらのメモリが大きくなると起動も遅くなるだろうと推測されます。

言語textサイズ 仮想メモリ 常駐メモリ
Perl 0.7MB 3.8MB 1 MB
PHP 1 MB 8.2MB 2.8MB
sh 0.5 MB 1.4MB 1.4MB

このメモリの割り当てと読み込みに要する時間、ライブラリなどとリンクする時間、 メモリ上にPerlの翻訳実行に必要な構造を作成する時間、などが、Perlの起動に要する時間の中身です。 Perlが走り始めると、CGI実行に必要なPerlライブラリを読み込む、 指定されたCGIプログラムを読み込む、ことが必要で、 これでやっとCGIの本体の処理が始められることになります。 余談ながら、その本体が、今日の日付を表示する、というようなはかないプログラムだと、 起動に要した準備の方が多くの処理時間を要することになるでしょう。 それで、SSI (server side include)という、ささいなプログラムはweb serverの中ですませる方式も実用化されています。

Hello Worldを出力して終わるプログラムをPerl, PHP, C, Lispの4種類の言語で用意しました。 親となるプロセスから、こららのプログラムのそれぞれをfork and execし、結果をpipeを使ってreadして終わるサイクルを100-1000回繰り返し、elapsed timeを測定した結果が下表です。プロセッサはEPIA 533MHzです。

表1 Perl, Php, C, Lisp の空プログラムの起動に要する時間(ミリ秒)
処理 Perl PHP C Lisp
仮想メモリ量 3.8MB 8.2MB 5.6MB
常駐メモリ量 1.0MB 2.8MB 2.6MB
スクリプト言語処理系を立ち上げる
(メモリを割り当てtextを読み込む,ld.soがリンクする, 処理系の初期化)
42ms 140-150ms 7ms 270ms
2 CGIに必要なライブラリ、CGIプログラムを読み込む
3 CGI本体の処理をして.HTML等を生成する

右端にLispのデータを載せてあります。本音を言うと、LispでCGIを書きたいのだが、 どうも立ち上がりが遅くてパフォーマンス悪いので、速くなる方法を考えようと言うことなのです。 lispと言うと、括弧だらけで醜い、car, cdr, cons なんて意味不明で古くさい、という人が必ずいます。 私に言わせると、Perlやshの$変数の方がよほど括弧悪く、 @変数や%変数に至っては、迷ってしまいます。言い争いはやめましょう:-)

2のCGI用ライブラリの読み込みやCGIプログラムのロードに要する時間は実測できていませんが、 数ミリ秒ではないかと推測します。

アクセスカウンターCGIの性能

CGIはいろいろなことができるのですが、よく使われるCGIというのはごく少数です。少数だからこそ、プロバイダは典型的なCGIをいくつか用意できているわけです。中でも最もよく使われるのがアクセスカウンターです。サーバーにカウントを格納しておくファイルがあって、アクセスがあるごとにそれをincrementし、結果をgifの絵にして出力します。一番時間がかかるのは、各桁のgifの絵をつないで一つのgif fileにするところではないかと思います。Perlで書いた、gifcat.plというプログラムがよくできていて、いろいろなところで使われているようです。このPerlの関数の実行速度を測りたいのですが、私はPerl使いではないので、時間測定関数などを知りません。とにかく、とほほさんのgif解析のページを参考に、Lispでgifcatを書いてみました。6桁のgifcatに要する時間は、インタプリタで20ms、コンパイルしたコードでは3msほどでした。この時間と、表1の時間を比べると、起動に要する時間の方がずっと大きいことがわかります。

起動時間をなくす-- mod_perlとFastCGI

アクセスカウンターのような軽いCGI処理においては、CGI本体の処理よりも起動時間が支配的になることがわかりました。CGIの起動時間がweb serverにとって大きな負荷になることは、ずっと以前からよく認識されていて、いろいろな解決策が提供されています。Apacheサーバーでは、mod_perl, mod_phpなどのDSOモジュールにしてperl、phpのインタプリタをサーバーに組み込んでしまうのが一般的になっています。新たなプロセスを起こすのではなく、スレッドを立てるくらいの手間で済ませられるのだろうと推定します。mod_perlのホームページには、cgiだと秒7回のリクエストにしか応えられないが、mod_perlに登録しておくと秒240回のサービスが可能になる、とあります。Hello Worldですけどね。httpd.confでcgiの登録が必要ですが、CGIプログラムの方には何も手を加えなくて良い、というのがよいですね。
ただ、不思議なのは、mod_phpを組み込んだapacheのプロセスのサイズが、virtualで5.7MB、常駐部が2.8MBくらいしかないことです。libexecに置かれたlibperl.soは、1.2MB位のサイズがあります。libphp.soは、1.4MB位あります。おそらく、何もCGIをregisterしない状態では、これらのライブラリはリンクされないのでしょう。mod_python.soもでかい。言語処理系のDSOモジュールは、他のapacheモジュールと比較すると、圧倒的にでかいです。

言語系をモジュールにして組み込んでしまえ、というのは、 起動のオーバヘッドをなくすためにはベストかもしれませんが、なんか強引な感じがします。 そんなんするくらいなら、CGIプログラムをコンパイルしてから組み込んでくれ、と思ってしまいます。

FastCGIは、言語系とCGIプログラムを別のプロセスにして常駐させておき、 httpリクエストが来るとsocketを通じてコマンドと結果の交換をする、 というインタフェースです。 'mod_fcgi'というモジュールをapacheに組み込んでおきます。 また、FastCGI.プログラムには、'libfcgi.so'をリンクします。

アクセスカウンターFastCGIの性能

Perl, Lisp, sh などの言語で、Hello WorldとアクセスカウンターのCGIを作り、一回のリクエストの反応時間を計測しました。残念ながら、このサーバーにはmod_perlを仕込んでいません。 それからPerlで書いたCGIをFastCGIにする方法をよく知りません。shはCGIしか方法がないでしょう。うーん、あと、Cでも書いてみるか。

Lispで書いた FastCGIは、PostgreSQLのライブラリを読み込んでデータベースサーチなど余分な仕事も一緒にさせています。 Perlをモジュールにすると毎秒240回処理できるとmod_perlのサイトに書いてあったので、Perl moduleは(4ms)としておきました。CGIだと毎秒7回、ということは140msくらいということになりますが、PerlのCGIライブラリをちゃんと読み込んだ後でhello worldを返すだけの空しいperl CGIは、300msくらいでした。うちのCPUはperl moduleサイトさんの半分くらいの性能だということかもしれません。そうであれば、perl module は倍の8msで比較するのが妥当で、データベース仕事もしているLisp FastCGIは全く遜色ないでしょう。

それどころか、どうも、perl は、アクセスカウンターのような処理は速くはないようです。起動は速いが所詮はインタプリタ。LispのCGI, FastCGIはコンパイルされているので、特にLisp FastCGIのカウンターは結構高速です。LispのCGIは、アクセスカウンターの処理に150msもかかっているではないか、Perl CGI はカウンターの処理にかかっているのは116msだよ、と言われるかもしれません。申し訳ありません。ベンチマークがいいかげんなのです。データベースにつなぎに行く時間が100msくらいあると思ってください。FastCGIは、PostgreSQLとの接続をずっと維持したままですから、差の14msのうちの5msくらいがgifの処理、10msくらいがデータベースのサーチだと推測します。

表2 各種言語のCGIとFastCGIの性能比較
Hello World アクセスカウンター
Perl CGI 24ms 140ms
Perl FastCGI
Perl module (4ms?)
Lisp CGI 340ms 490ms
Lisp FastCGI 10ms 24ms
sh CGI 32ms
sh FastCGI


結論

いずれにせよ、とにかくLispのアクセスカウンターは、FastCGIにすることで圧倒的に速くなることがわかります。それに(比較する上ではいいかげんなのですが)、このFastCGIは、データベースを調べに行ったりもするので、処理量は実はずっと多いのです。というわけで、結論です。

  1. CGIはさっと返事を返さないといけない
  2. そのためには起動時間が無視できない
  3. 起動時間をなくすためには、moduleとしてサーバーに組み込むか、 FastCGIインタフェースを使ってCGIプロセスをpersistentに常駐させる
  4. モジュールにしても、FastCGIにしても、小さなCGIでは10倍くらいは簡単に速くなる。 Lispのような起動の遅い言語でも十分実用になる。

webの反応が遅いのはサーバーが古いせいだ、最新マシーンに買い換えよう、 というのは、サーバーが古いのではなくて、CGIが古いのです。 hardwareで10倍速くするのはコスト的にもエネルギー的にも やさしくないですよ。


おまけ

うちのカウンターは、速いだけでなくちょっとインテリジェントです。 同じブラウザからの10分以内のアクセスではカウントアップしません。 ブラウザ毎のアクセスの統計なんかもとっています。


toshihiro@matsui.dip.jp