この文書全体は、Passengerのアーキテクチャを説明します。この文書の目的は、新たなコントリビュータの参入障壁を下げ、同時に我々がなすべき設計方針を説明する事です。

また、Passengerの働きを知りたい人々の楽しい読み物でもあります。

1. 関連技術について

1.1. 典型的なWebアプリケーション

Passengerを説明する前に、典型的なWebアプリケーションがどのように働くかを、アプリケーションをWebサーバにつなげる事をを望む誰かの視点から理解する事は重要です。

典型的で孤立したWebアプリケーションは、いくつかのI/OチャネルからHTTPリクエストを受け入れ、内部でそれを処理し、HTTPレスポンスを出力し、それをクライアントに送り返します。これは、アプリケーションが終了を命令されるまで繰り返し行われます。 この事は、WebアプリケーションがHTTPを直接的に話す必然性がない事を意味します: WebアプリケーションはあるHTTPリクエストの何種類かの表現を受け入れる事を意味します。

Architecture of a typical web application in isolation

いくらかのWebアプリケーションは、HTTPクライアントによって直接アクセス可能です。共通の構成は:

  1. Webアプリケーションはアプリケーションサーバーに含まれます。このアプリケーションサーバーは複数のWebアプリケーションを含める事が、できるかもしれないしできないかもしれません。 アプリケーションサーバーはWebサーバーに接続されます。Webサーバーはリクエストをアプリケーションサーバーに差し向け、アプリケーションサーバーが適切なWebアプリケーションにWebアプリケーションに理解できる形式でリクエストを差し向けます。 逆に、Webアプリケーションによって出力されたHTTPレスポンスは アプリケーションサーバーに送られ、さらにWebサーバーに送られ、最終的にHTTPクライアントに送られます。

    このような典型的な構成例は、Apache Webサーバの背後にあるTomcat Webサーバに含まれたJ2EEアプリケーションです。

  2. WebアプリケーションはWebサーバに含まれます。この場合、Webサーバーはアプリケーションサーバーのように行動します。これは、mod_phpとApache Webサーバーを使ったPHPアプリケーションのケースです。これはWebアプリケーションはWebサーバーと同じプロセスの中で動かなければならないという意味ではない事に注意してください: ただWebサーバーがアプリケーションを管理することだけを意味します。

  3. WebアプリケーションWebサーバーで、HTTPリクエストを直接受け入れられる場合。これはTracバグとラッキングシステムが自身のスタンドアロンサーバーで動いている場合です。 HTTPリクエストを直接受け入れる代わりに、異なるWebサーバーの背後に置かれるWebアプリケーションのような多くの構成の場合。フロントエンドのWebサーバーはリバースHTTPプロクシの様に行動します。

  4. Webアプリケーションは直接HTTPを話さないが、いくつかの通信アダプタを通してWebサーバーに直接接続する場合。CGI、FastCGIとSCGIはこの好例です。

これらの説明は、PHPベース、Django、J2EE、ASP.NET、Ruby on Raials、その他何でも、ほぼすべてのWebアプリケーションの本質です。これら構成のすべては同じ機能を提供し、ある構成でほかの構成ではできない何かができるわけではない点に注意してください。 注意深いな読者は、これら構成のすべてが最初の図で表現されたものと同一であり、Webサーバー/アプリケーションサーバー/Webアプリケーション/ほか、の組み合わせが唯一の実態だと見なせる事に気がつくでしょう; a black box if you will.

これら構成は個別のI/O処理実装に強制されない事もまた注意されるべきです。Webサーバ/アプリケーションサーバー/Webアプリケーション/その他は、I/Oを順次処理(都度のリクエストなど)でき、単独スレッドの多重I/O(select(2)poll(2)の利用)ができ、マルチスレッドもしくはマルチプロセスのI/O処理が可能です。

もちろん、多くのバリエーションを持つ事が可能です。例えば、ロードバランサーを使う事ができます。ただし、それはこの文書の範囲外です。

1.2. Ruby on Rails

すべてのRuby on Railsアプリケーションはディスパッチャを持っています。このディスパッチャはHTTPリクエストの処理に関して責任を持ちます。HTTPを直接は話しません。代わりに、HTTPリクエストの情報を含むデータ構造を受け入れます。 上に述べたように、ディスパッチャは特にRuby on RailsをHTTP処理層(Webサーバー)と接続するソフトウェアを開発したいと望む開発者に興味深いものです。

Ruby on Railsのディスパッチャはリクエストを順次(1度に1つ)処理する事しかできません。Ruby on Railsはスレッドセーフではないため、2つのリクエストを同時にスレッドを使って処理する事はできません。 (実際、これはいくらかの人々の想定として大きな問題ではありません。これは後述の同時リクエストのハンドリングで詳しく述べられます。)

特に興味深い注意すべき点は、Ruby on Railsアプリケーションによって占有される大量のメモリはプログラムコードの記憶に費やされる(例: abstract syntax tree (AST))という事です。これはRuby Enterprise Editionのメモリ統計機能の利用を通して観察されます。同様に、Ruby on Railsアプリケーションの非常に長い起動時間はRailsフレームワークのブートストラップに費やされます。

1.3. Apache

Apache WebサーバーはプラガブルなI/Oマルチプロセッシング(1以上のHTTPクライアントを同時にハンドル可能な)アーキテクチャです。マルチプロセッシング戦略に特化して実装するApacheモジュールは、Multi-Processing Module (MPM)と呼ばれます。prefork MPM — 標準である — が最も一般的なようです。このMPMは複数のワーカー子プロセスを生じます。HTTPリクエストははじめに制御プロセスと呼ばれるものに受け入れられ、ワーカープロセスの1つに転送されます。次のセクションはprefork MPMのアーキテクチャを表す図を含みます。

2. Passenger のアーキテクチャ

2.1. 概要

Passengerのアーキテクチャは典型的なWebアプリケーションで説明した構成の2番によく似ています。 言い換えると、PassengerはApacheを拡張し、アプリケーションサーバーのように行動できるようにします。Passengerのアーキテクチャ — Apache 2とprefork MPMを使うと仮定 — は以下の図で表されます:

Passenger's architecture

PassengerはApaacheモジュールのmod_passengerで構成されます。これはC++で書かれていて、ext/apache2ディレクトリにあります。モジュールはApacheの制御プロセスとすべてのワーカープロセスで有効になります。HTTPリクエストがきたとき、mod_passengerはリクエストがRuby on Railsによって扱われるべきか確認します。もしそうなら、mod_passengerは該当するRuby on Railsアプリケーションを(必要なら)生成し、リクエストをアプリケーションに転送します。

Ruby on RailsアプリケーションはApacheの同じアドレス空間の中で動作しない事に注意すべきです。これは、Passengerをmod_phpやmod_perl、mod_rubyなど他のWebサーバ内のアプリケーションサーバーソフトウェアと区別します。もしRailsアプリケーションがクラッシュするかメモリリークしても、Apacheに影響を与えないでしょう。実際、安定性は我々の最高のゴールの1つです。PassengerはPassengerが原因でApacheがクラッシュする事がないよう、注意深く設計され実装されています。

2.2. コードとアプリケーションのスポーンとキャッシュ

Passengerの非常に単純な実装はCGIのようにHTTPリクエストを受け取るたびにRuby on Railsアプリケーションを生成するかもしれません。しかしながら、Ruby on Railsアプリケーションの生成は不経済です。最近のPCで1〜2秒かかり、負荷がかかっているサーバではさらにかかるかもしれません。このオーバーヘッドは特に共有ホストでは受け入れられません。より単純でない実装は生成されたRuby on Railsアプリケーションのインスタンスを生かし続け、LighttpdやFastCGIといった実装のように動作するかもしれません。しかしながら、これもまだいくつかの問題があります:

  1. Railsウェブサイトへの最初のリクエストは遅く、続くリクエストは速いでしょう。しかし最初のリクエストが別のRailsウェブサイト - 同じWebサーバーの - へのものなら、やはり遅いでしょう。

  2. この文書のはじめに説明したように、Railsアプリケーションに関する大量のメモリはRuby on RailsフレームワークのASTの記憶とアプリケーションに消費されます。とりわけ、共有ホストとメモリが制限されたVPSでは、この事は問題です。

これら両方の問題は間違いなく解決でき、我々はそうする事を選択しました。

最初の問題はRailsアプリケーションを先読みする事で解決できます(ウェブサイトにリクエストがくる前にRailsアプリケーションを起動させておく、など)。これは多くのRailsホストでとられたアプローチで、例えば、Mongrelクラスタを常時稼働させる形があります。しかしながら、これは共有ホストでは受け入れられません: 何もしていなくてもあるだけでメモリを消費してしまいます。代わりに、我々は前述の両方の問題を解決するための異なるアプローチを選択しました。

我々はスポーンサーバーを使ってRailsアプリケーションを生成します。スポーンサーバーはRuby on Railsフレームワークのコードとアプリケーションのコードをメモリにキャッシュします。初回のRailsアプリケーションの生成はやはり遅いのですが、続く生成の試みは非常に速くなります。さらに、フレームワークコードがアプリケーションコードとは独立してキャッシュされるため、異なるRailsアプリケーションの生成も、そのアプリケーションが既にキャッシュされているバージョンのRailsフレームワークを使っている限り、非常に速くなります。

スポーンサーバーの別の影響は、異なるRuby on Railsが互いにメモリを共有するだろう事ですが、それゆえに問題の2番を解決します。これは次節で詳細に説明されます。

フレームワークのコードとアプリケーションのコードをキャッシュするにもかかわらず、生成はHTTPリクエストに比べまだ比較的不経済です。我々は可能な限り生成の回避を望みます。これがアプリケーションプールを導入する理由です。生成されたアプリケーションのインスタンスは生き続け、それらのハンドラはこのプールに格納され、それぞれのアプリケーションインスタンスが後で再利用できるようにします。こうして、Passengerはおおむね非常に良好なパフォーマンスをえられます。

アプリケーションプールは異なるワーカープロセスの間で共有されます。ワーカープロセス達は互いにメモリを共有できないため、共有メモリはアプリケーションプールという方法で使われるか、クライアント/サーバーアーキテクチャで提供されるかのどちらけでなければなりません。我々は実装が易しい後者を選びました。Apacheの制御プロセスはアプリケーションプールのためのサーバーの様に行動します。しかしながら、これはすべてのHTTPリクエスト/レスポンスのデータが制御プロセスを通過する事を意味しているわけではありません。あるワーカープロセスはRailsアプリケーションとの接続セッションについてプールに問い合わせます。このセッションが成り立っているとき、ワーカープロセスはRailsアプリケーションと直接通信するでしょう。

アプリケーションプールはmod_passenger内部で提供されます。詳細はthe C++ API documentationApplicationPoolStandardApplicationPoolApplicationPoolServerといったクラスに関する部分にあります。

Note Passengerはworker MPM(プロセスの代わりにスレッドを使う)を使ったApacheをサポートしません。しかし、アプリケーションプールはモジュール化して実装されているため、worker MPMをサポートするために10行以上のコードを使うべきではありません?

アプリケーションプールはアプリケーションの生成/生成したアプリケーションのハンドラのキャッシュ/有効期限をこえて待機しているアプリケーションの片付けに責任を持ちます。

2.3. スポーンサーバー

スポーンサーバーはRubyで書かれていて、コードはlib/passengerディレクトリにあります。実行部分はbin/passenger-spawn-serverです。詳細はスポーンサーバーのRDocドキュメントにあります。

スポーンサーバーは3つの論理層から構成されます:

  1. スポーンマネージャー。 これは最上位の層で、すべての下層の前面として行動します。スポーンサーバーを使うクライアントはこの層とのみ通信をします。

  2. フレームワークスポーナーサーバー。スポーンマネージャーはRuby on Railsのバージョンごとにそれぞれユニークなフレームワークスポーナーサーバーを生成します。各フレームワークスポーナーサーバーは対応するRuby on Railsフレームワークの完全なコードをキャッシュします。あるアプリケーションに関する生成要求は、そのアプリケーションにとって適切なバージョンのRuby on Railsを含むフレームワークスポーナーサーバーに転送されます。

  3. アプリケーションスポーナーサーバー。これはフレームワークスポーナーサーバーにとってのスポーンマネージャーのように、フレームワークスポーナーに対するものです。フレームワークスポーナーサーバーはそれぞれのユニークなRuby on Railsアプリケーション("アプリケーション"は実行プロセスの意味ではなく、ソースコードファイルの集合を意味します)を生成します。あるアプリケーションスポーナーサーバーは1つのアプリケーションの完全なコードをキャッシュします。

The spawn server's architecture

ご覧の通り、我々はコードキャッシュの2層を持ちます: スポーナーサーバーが新しいアプリケーションのインスタンスを生成するリクエストを受け取ったとき、該当するフレームワークスポーナーサーバー(まだ存在しなければ生成されます)に転送され、 — さらに — 該当するアプリケーションスポーナーサーバー(まだ存在しなければ生成されます)に転送されます。

それぞれの層は直接の下層に関して責任を持ちます。スポーンマネージャーはフレームワークスポーナーサーバーに関してのみ関知し、フレームワークスポーナーサーバーは該当するアプリケーションスポーナーサーバーについてのみ関知します。アプリケーションスポーナーサーバーは、七宝、生成したアプリケーションのインスタンスに関して責任を持ちません。もしあるアプリケーションのインスタンスがmod_passengerによって生成されるなら、その情報はmod_passengerに送り返され、アプリケーションのインスタンスの生存時間について(アプリケーションプールを通して)責任を持ちます。

さらに、それぞれの層はプロセスに分かれる事に注意してください。これはひとつのRubyプロセスはひとつのRuby on Railsフレームワークとひとつのアプリケーションのみをロードできるという事から余儀なくされています。

2.3.1. メモリの共有

最も最近のUnix OSで、子プロセスが作られるとき、親プロセスと最大限メモリを共有するでしょう。プロセス達はそれぞれお互いのメモリにアクセスできると考えないため、OSは親か子のプロセスによってメモリに書き込まれるときにメモリ断片のコピーを作成します。これはcopy-on-write(COW)と呼ばれます。バックグラウンドの情報の詳細はRuby Enterprise Edition's websiteにあります。

スポーンサーバーはこの有用な事実を利用します。それぞれの層はすべての下層とRuby ASTのメモリを、ASTノードに書き込むべきだと判断しない限り、共有します。この意味は、生成したすべてのRailsアプリケーションは — 可能な限り — Ruby on Railsフレームワークのコード/アプリケーションコード/その他を共有するという事です。この結果、劇的にメモリ使用量が減りました。

Note

メモリの共有はRuby Enterprise Editionが使われるときのみ有効です。これは標準RubyインタプリタのGCがcopy-on-writeフレンドリでないからです。技術的詳細はRuby Enterprise Editionのウェブサイトまで。

Passengerは標準Rubyでもよく動作します。減少したRails起動時間を手に入れましょう。ただメモリの共有の利益を得られないだけです。

RubiniusのGCは既にcopy-on-writeフレンドリである事に注意してください。

2.4. 同時リクエストのハンドリング

先に説明したように、ひとつのRailsアプリケーションのインスタンスは1度にひとつのリクエストしか扱う事ができません。これは明らかに望ましくありません。しかし解決に取り組む前に、"競合者"がどのようにこの問題を解決するかをみてみましょう。PHPは同様の問題を抱えています: ひとつのPHPスクリプトは1度に1つのHTTPリクエストしか処理できません。

Passengerでは、リクエストの度に新しいRailsアプリケーションを生成する事を強制すると — 先に説明した通り — 受け入れられないほどに遅いため、mod_phpでの方法を使えません。代わりに、PassengerはPHP-FastCGIのアプローチを採用します。アプリケーションのインスタンスのプールを維持し、リクエストを受けるたびに、そのリクエストをプールの中のアプリケーションのインスタンンスに転送します。プールのサイズは設定可能で、負荷が大きかったりメモリが小さなサーバーの管理者にとって有益です。

読者はアプリケーションプールのアルゴリズムの学習に興味を持つかもしれませんが、ありふれたものです。アルゴリズムの詳細はApplicationPool algorithm.txtにあります。

3. Appendix A: この文書について

The text of this document is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License.

images/by_sa.png

この文書は、Passenger architectural overviewをkoshigoeが私的に翻訳したものです。訳の正確さは保証できませんし、無条件に信用すべきでもありません。