mod_rubyによるWebシステム構築

ゼータビッツ株式会社 前田 修吾 shugo@zetabits.co.jp

はじめに

Rubyは言語的にはPerlとまったく異なっています が、提供される機能の多くはPerlと似通っていま す。したがって、Perlが得意とする分野ではRuby もその威力を発揮することができます。たとえば、 CGIのようなWebアプリケーションもその一例です。

たいていの場合、CGIが行うのはたいした処理で はありませんが、Cのような言語の場合、簡単な ことをするのにも複雑な手続きが必要になります。 一方、RubyやPerlのようなスクリプト言語は、簡 単なことは簡単にできる、という性質を持ってい る(RubyやPerlとは異なるポリシーのスクリプト 言語もありますが)ため、ある程度の規模のCGIプ ログラムなら簡単に作ることができます。

ここでちょっと気になるのが、実行効率はどうな のか、という点です。Rubyはたしかに開発効率と いう点から見れば非常に優れているのですが、実 行効率という点ではあまり優れているとは言えま せん。はっきり言ってしまうと、Rubyは遅いので す。

ここには開発効率と実行効率のトレードオフがあ ります。こういったトレードオフはよくあること です。メモリと実行時間、品質とコスト、美食と 体重、などなど。私たちは、生きていくためには、 どこかの点で妥協をしなければなりません。右の 飼葉と左の飼葉を見比べながら、どちらを食べる か迷いながら餓死してしまう馬のようになるわけ にはいかないのです。Webアプリケーションの開 発にRubyを採用するのは妥当な選択と言えるでしょ う。Rubyの開発効率は非常に素晴らしいものです し、遅いと言っても我慢できないほどのものでは ないのですから。

しかし、そうは言ってもまったく実行効率を考慮 しないというわけにも行かないでしょう。そこで、 本稿ではちょっとしたトリックによって、Rubyに よるWebアプリケーションの実行効率を改善する 方法をご紹介します。

mod_ruby

Rubyの実行効率についてお話しましたが、実は CGIという仕組みそのものも、実行効率の点で問 題を持っています。ある局面においては、これは Rubyの実行効率の問題以上に重大な影響を及ぼし ます。

ここで、まず、CGIの仕組みについて説明してお きましょう。通常、静的なコンテンツ(HTMLファ イルや画像ファイルなど)のリクエストに対して は、Webサーバは自分自身でレスポンスを返しま す。一方、CGIの場合、Webサーバはリクエストが 来るたびに外部プログラム(CGIプログラム)を起 動し、その出力をクライアントに返します。つま り、一つのリクエストに対して、毎回一つのプロ セスが生成されるわけです。(図1)

これは実行効率の面から見れば、非常に不利です。 なぜならプロセスの生成というのはとても重い処 理だからです。Apacheという人気のあるWebサー バでは同時に複数のクライアントに応答するため に複数のプロセスを生成して処理をしますが、あ らかじめいくつかのプロセスを生成しておいて、 それらを使い回すことによって極力プロセスの生 成を避けるような実装になっています。しかし、 CGIを利用する場合はリクエスト毎にプロセスを 生成せざるを得ないため、このような努力も水の 泡となってしまいます。特に多数のリクエストが 集中した場合にサーバマシンにかかる負荷は大変 なものになります。

そこで、この問題を解決するために、mod_rubyと いう仕組みが考え出されました。(実際には mod_perlという先例の真似をしただけですが。) ApacheというWebサーバは非常に拡張性に優れた 設計となっており、モジュールとよばれるユニッ トを組み込むことによってサーバの機能を拡張で きるようになっています。mod_rubyもこのモジュー ルの一つで、Rubyスクリプトを実行する機能を提 供します。これによって、CGIのように外部プロ グラムを起動する代りに、Webサーバ自身がRuby スクリプトを実行することによって、プロセスの 生成を大幅に削減することができるわけです。 (図2)

問題はmod_rubyの導入が開発効率に与える影響で すが、mod_rubyは基本的にCGI互換のインタフェ イスを提供するため、CGIからの移行は容易になっ ています。そのため、とりあえずCGIを想定して 開発を行い、後でmod_rubyの導入を検討すること も可能です。逆に、mod_rubyに何か問題があった 場合には、実行環境を従来のCGIに戻すこともで きます。

ただ、mod_rubyにも欠点はあります。一つは Apacheでしか利用できないという点です。しかし、 Apacheは優れたWebサーバですから、これはたい した問題ではないでしょう。もう一つは、Rubyイ ンタプリタを複数のスクリプトで共有されるため、 あるスクリプトが他のスクリプトに悪影響を与え てしまう可能性があるという点です。このため、 mod_rubyの機能を信頼できないユーザに開放する ことは避ける必要があります。また、悪意がなく とも、グローバル変数を乱用したり、組み込みク ラスの挙動を変更するような行儀の悪いスクリプ トを書いてしまうと、他のスクリプトが正常に動 作しなくなってしまう可能性があるので、注意が 必要です。

セットアップ

では、実際にmod_rubyを利用するためのセットアッ プの手順について説明します。以下のソフトウェ アが既にインストールされていることを前提とし ます。

Debian GNU/Linuxなどではmod_rubyのバイナリパッ ケージが標準で提供されていますが、ここではソー スアーカイブを利用することにします。

まず、インストールに必要なソースアーカイブを mod_rubyの公式サイトから入手します。必要なの は以下のファイルです。

erubyはeRubyファイル(HTMLなどの文書にRubyプ ログラムを埋め込んだもの)を扱うプログラムで す。単体で利用することもできますが、mod_ruby から利用することもできるようになっています。 eRubyは今回は使用しませんが、インストール方 法だけ説明しておくことにします。

erubyもmod_rubyも、rubyのようなautoconf化さ れたプログラムと同じような手順でインストール を行うことができます。異なるのはconfigureの 代りにMakefile.RBというRubyスクリプトを利用 する点です。

まず、erubyを以下のような手順でインストール します。

$ tar zxvf eruby-0.1.3.tar.gz
$ cd eruby-0.1.3/
$ ruby Makefile.RB --enable-shared --default-charset=EUC-JP
$ make
# make install

Makefile.RBに--enable-sharedというオプション を指定していますが、これはerubyを共有ライブ ラリ化するという意味です。この共有ライブラリ をmod_rubyで利用するわけです。 --default-charset=EUC-JPというのはeRubyファ イルのデフォルトのcharsetをEUC-JPにするとい う意味です。

次に、mod_rubyをインストールします。

$ tar zxvf mod_ruby-0.2.2.tar.gz
$ cd mod_ruby-0.2.2/
$ ruby Makefile.RB --enable-eruby
$ make
# make install

erubyの場合とほとんど同じですが、今度は --enable-erubyというオプションを指定していま す。これはeRubyを利用できるようにするという 意味です。

さて、これでmod_rubyのインストールが完了した わけですが、インストールを行っただけでは mod_rubyを利用することはできません。 Apacheの設定ファイル(httpd.conf)にいくつかの 設定を追加する必要があります。(リスト1)

リスト1 httpd.conf

LoadModule ruby_module /usr/lib/apache/1.3/mod_ruby.so

<IfModule mod_ruby.c>
AddHandler ruby-script .rbx
AddType application/x-httpd-eruby .rhtml

RubyKanjiCode euc
</IfModule>

まず、LoadModuleはモジュールをロードするディ レクティブ(Apacheの設定ファイルに記述する命 令)です。引数にはモジュール名(mod_rubyの場合 はruby_module)とモジュールのパスを与えます。 mod_rubyがインストールされる場所は環境によっ て異なるので適宜変更してください。この例では 他のディレクティブと同じ場所に書いてあります が、他のモジュールがロードされているところと 同じ場所に記述した方がよいでしょう。

ただ、LoadModuleより後でClearModuleListとい うディレクティブが記述されている場合は注意が 必要です。(RedHat系のディストリビューション ではデフォルトでそのような設定がされています。) この場合、それまでにロードされたモジュールは いったんすべて無効にされてしまいますので、次 のようにAddModuleディレクティブで明示的にモ ジュールを有効にしてやる必要があります。

AddModule mod_ruby.c

<IfModule mod_ruby.c>から</IfModule>までは mod_rubyが有効になっている場合のみ有効になり ます。mod_rubyのための設定はこの中に記述して やります。こうしておけば、mod_rubyを無効にす る場合には、LoadModuleの部分のみコメントアウ トすれば、他の設定を全部コメントアウトする必 要がないので楽です。

AddHandlerとAddTypeはある拡張子のファイルを どのように扱うかを指定します。AddHandlerでは ハンドラ名を指定するのに対し、AddTypeでは MIMEタイプ名を指定します。ruby-scriptはRuby スクリプトを実行するハンドラで、 application/x-httpd-erubyはeRubyファイルを表 すMIMEタイプです。この例では.rbxという拡張子 を持つファイルをRubyスクリプトとしてmod_ruby 上で実行し、.rhtmlというファイルをeRubyファ イルとして扱います。.rbではなく.rbxとしてい るのは、mod_rubyで実行しない通常のRubyスクリ プトと区別するためです。

RubyKanjiCodeはスクリプトの文字コードを指定 するディレクティブで、mod_rubyによって定義さ れています。ここではEUCを指定していますが、 環境によってSJISなどの他のコードを指定してく ださい。

mod_rubyで定義されているディレクティブは他に もいくつかありますので、表1にまとめておきます。

表1 mod_rubyで定義されたディレクティブ

RubyKanjiCode kcode

スクリプトの文字コードを指定

RubyRequire library...

libraryをrequireする

RubyPassEnv name...

スクリプトに受け渡す環境変数を指定

RubySetEnv name val

スクリプトに受け渡す環境変数の値を指定

RubyTimeOut sec

タイムアウトの指定

RubySafeLevel level

$SAFEのデフォルト値を指定

テスト

さて、これでmod_rubyを利用できる環境をセット アップすることができました。次に実際に上手く 動作しているかどうか、テストをしてみることに しましょう。test.cgiというテストスクリプトを 用意します。(リスト2)

リスト2 test.cgi

#!/usr/bin/ruby

require "cgi"

cgi = CGI.new
print cgi.header("type" => "text/plain")
print "GATEWAY_INTERFACE: ", ENV["GATEWAY_INTERFACE"], "\n"

cgiというライブラリを利用していますが、これ はCGIプログラミングをサポートするライブラリ です。このライブラリを利用することで、CGIと mod_rubyの違いを意識することなく、プログラミ グすることができます。つまり、このスクリプト はmod_ruby上で実行することも、CGIプログラム として実行することもできるわけです。 (mod_ruby上で実行する場合は先頭の

このスクリプトはGATEWAY_INTERFACEという環境 変数の値を出力します。CGIとして実行すると以 下のような結果が得られます。

GATEWAY_INTERFACE: CGI/1.1

このスクリプトをtest.rbxという名前にコピーし てmod_ruby上で実行すると、以下のような結果が 得られます。

GATEWAY_INTERFACE: CGI-Ruby/1.1

GATEWAY_INTERFACEという環境変数にはCGIのバー ジョンを表す文字列が入っているのですが、CGI の場合は"CGI/1.1"に、mod_rubyの場合は "CGI-Ruby/1.1"になるというわけです。 設定がきちんとできていないと、スクリプトの内 容がテキストとして表示されてしまったりします ので、そのような場合にはもう一度よく設定を確 かめてみてください。その際、Apacheのエラーロ グが参考になるかもしれません。

参考までにApache付属のabというプログラムを用 いたベンチマーク結果を図3・図4に示しておきます。

図3 CGIの場合

$ ab -c 5 -n 30 http://localhost/~shugo/test.cgi
...
Connnection Times (ms)
              min   avg   max
Connect:        0     8    99
Processing:   939  1223  1439
Total:        939  1231  1538

図4 mod_rubyの場合

$ ab -c 5 -n 30 http://localhost/~shugo/test.rbx
...
Connnection Times (ms)
              min   avg   max
Connect:        4    50   107
Processing:   114   126   304
Total:        118   176   411

RD日記スクリプト

では、ここで少し実用的なスクリプトの例を紹介 しましょう。例として取り上げるのは日記を表示 するスクリプトです。

ただ、そのままではあまりにもつまらないので、 少しだけ工夫をしてあります。それは日記を記述 するのにRDを用いるようにしている点です。RDは Ruby標準のドキュメントフォーマットで、プレー ンテキストに近い感覚で記述できるようになって います。そのため、書きやすく読みやすい、とい う特長を持っています。RDの例を図5・6に示して おきます。

図5 変換前のRD

= 日記

RDで日記を書くためのスクリプトを作ったので日記を書くことにした。
(何か話が逆のような気もするけれど。)

図6 変換後のHTML

<H2><A NAME="d17">12月17日(日)</A></H2>
<H3><A NAME="d17:label:0">日記
</A></H3><!-- RDLabel: "日記" -->
<P>
RDで日記を書くためのスクリプトを作ったので日記を書くことにした。
(何か話が逆のような気もするけれど。)
</P>

今回作成するスクリプトでは一日分の日記を一つ のファイルにすることにします。たとえば、2001 年1月1日分の日記はスクリプトからの相対パス では200101/01というファイルに記述します。ス クリプトではこれらのファイルをフォーマットし て連結することで日記を出力します。

RD文書は通常、RDtool(RD/RDtoolについては詳し くは最後に紹介するウェブサイトを参照してくだ さい)というツールを用いて、他のフォーマット に変換して利用します。HTMLに変換するライブラ リも標準で提供されていますが、これは一つのRD 文書を一つのHTML文書に変換するため、日記用の RD文書を一日毎に分けると、日数分のHTMLファイ ルを生成することになってしまいます。

そこで、RDファイルを図6のようなHTMLの一部 (HTML,BODYなどを出力しないもの)に変換するフォー マッタを作成し、各出力を連結して一つのHTMLを 生成することにします。といっても、RDの解析ルー チンなどを一から書くわけではなくて、実際には RDtoolを再利用する形で実装します。

RDからHTMLへの変換を行うライブラリを リスト3 に示します。dateという標準ライブラリの他に、 rd/rdfmt・rd/rd2html-libという二つのライブラ リをrequireしていますが、これらはRDtoolで提 供されるライブラリです。rd2diary.rbの中では 二つのクラスを定義していますが、 RD2DiaryVisitorはRDを日記用のHTMLに変換する ためのクラスで、RD2DiaryFormatterは RD2DiaryVisitorとRDtoolで提供される RD::RDTreeを用いて、実際にフォーマットを行う クラスです。

RDtoolはRDの解析を行う部分と他のフォーマット に変換する部分が明確に分離されているため、新 しいフォーマットへの対応が容易になっています。 RDの解析を行う部分がRD::RDTreeで、他のフォー マットへの変換を行う部分がRD::RD2HTMLVisitor などのVisitorクラスです。実際のフォーマット は、Visitorのvisitメソッドの引数にRD::RDTree オブジェクトを渡すことで行います。Visitorク ラスにはRDの各要素に対応して、 apply_to_DocumentElementやapply_to_Headline などのメソッドが用意されています。これらのメ ソッドにRDの各要素毎のフォーマット方法を記述 することで、全体のフォーマット方法を記述する ようになっているわけです。

RD2DiaryVisitorの場合は基本的にHTMLを出力す ればよいので、RD::RD2HTMLVisitorを継承して一 部のメソッドのみ再定義しています。変更点は以 下の通りです。

・apply_to_DocumentElementでHTML,BODYなどの

代りにH2で日付を出力する。

・apply_to_HeadlineでH3以下を利用する。(H1は

全体のタイトル、H2は日付で使うため。)

・initializeで日付の情報を受け取る。 ・prepare_labelsでラベル名の衝突を避けるため、

日付を元にした文字列を付加する。

RD2DiaryFormatterの方はget_diaryというメソッ ドで指定されたファイルをフォーマットしますが、 その際.datという拡張子を付加したキャッシュファ イルを出力し、RDファイルの更新時刻より新しい キャッシュファイルがある場合は、キャッシュを 利用するようにしています。このため日記ファイ ルを置くディレクトリ(200101/など)はApacheが 動作するユーザの権限で書き込みができるように しておく必要があります。実際のフォーマット はformatというprivateメソッドで行います。 本来RDは=beginで始まり、=endで終 わらなければならないのですが、日記を書くたび に毎回=begin/=endを書くのは面倒 なので、formatの中で全行を読み込んだ後に追加 しています。

さて、これで準備は整いましたので、実際に日記 を出力するmod_rubyスクリプトを作成します。 リスト4

まず、最初にcgiライブラリと先程のrd2diary.rb をrequireしています。次にcgiオブジェクトを生 成し、monthというパラメータが与えられている かどうかをチェックしています。たとえば、

http://foo.bar.baz/diary.rbx?month=200101

のようにパラメータが与えられた場合は2001年1 月分の日記を出力します。パラメータが与えられ なかった場合は最新月の日記を出力します。これ はカレントディレクトリの日記が置かれたディレ クトリ名をDir#grepで取り出し、sortして最後の 要素(つまり最新月のディレクトリ名)を得ること で実現しています。Dir#grepはUnixコマンドの grepに似ていますが、ファイルの内容ではなく、 そのディレクトリ内のファイル名に対して正規表 現によるマッチングを行います。

ディレクトリを決定した後は、そのディレクトリ に対して同様にDir#grepを適用して、日記ファイ ルのリストを得ています。パラメータが与えられ なかった場合(latestが真の場合)は、ファイルリ ストを逆順にしてから最新の5日分だけ取り出し ます。最後にRD2DiaryFormatterで各ファイルを フォーマットして連結します。

後はこの内容を出力するだけですが、HTMLのテン プレートをスクリプトに埋め込むためにヒアドキュ メントを使っています。ヒアドキュメントはスク リプト中に(主に複数行の)文字列を埋め込むため の仕組みで、この場合<<EOFの次の行からEOFの前 の行までをhtmlという変数に文字列として代入し ています。このヒアドキュメント中で#{}によっ てtitleやcontentsの内容を埋め込んでいます。 そして、最後にへッダとhtml変数の内容を出力し ています。

さて、これで簡単な日記表示スクリプトを作成す ることが出来ました。これだけでは実際に使用す るには機能が不足しています(Last-Modifiedを出 力しないなど)が、これをベースにご自分でより 実用的なスクリプトを作成されても面白いかもし れません。

最後に関連ウェブサイトを紹介しておきますので、 この記事で不足している情報はこちらのサイトで 入手してください。

modruby.net

mod_rubyの公式サイトです。

Just Another RD Site

作者のToshさんによるRDtoolの公式サイトです。