1. 関係する技術の紹介
1.1. Webアプリケーションモデル
Phusion Passengerを説明する前に、アプリケーションをwebサーバに接続しようとする視点から代表的なwebアプリケーションがどのように動作するかを理解することは重要です。
典型的な、隔離された、webアプリケーションはなんらかのI/OチャンネルからHTTPリクエストを受け取り、それを内部的に処理し、HTTPリスポンスを出力します。それがクライアントに送り返されます。アプリケーションが終了するように命令されるまで、これが繰り返されます。これは必ずしもwebアプリケーションがHTTPを直接話す事を意味しません: webアプリケーションがなんらかの種類のHTTPリクエストの表現をwebアプリケーションが受け付ける事を意味するだけです。
HTTPクライアントが直接アクセス可能なwebアプリケーションは稀です。一般のモデルはこのようなものです:
-
webアプリケーションはアプリケーションサーバに含まれています。アプリケーションサーバは複数のwebアプリケーションを含むことができるかも知れないし、できないかも知れません。アプリケーションサーバは次になんらかのプロトコルを使ってwebサーバに接続します。このプロトコルはHTTP、FastCGI、SCGI、AJPまたは何でもかも知れません。webサーバはアプリケーションサーバにリクエストを送信し、次にそれはwebアプリケーションが理解できるフォーマットで正しいwebアプリケーションにリクエストを送信します。逆に、webアプリケーションによって出力されたHTTPリスポンスはアプリケーションサーバに送信され、アプリケーションサーバはそれらをwebサーバに送ります。そして最終的にHTTPクライアントに送られます。
そのようなモデルの典型的な例:
-
Tomcatアプリケーションサーバに含まれるJ2EE アプリケーションは代理をするApache webサーバの後ろにいます。Tomcatは一つのTomcatインスタンスの中に複数のwebアプリケーションを含むことができます。
-
ほとんとのRubyアプリケーションサーバはPhusion Passenger (Thin, Unicorn, Goliate など)の後ろにいます。これらのアプリケーションサーバはインスタンスごとに一つのRuby web アプリケーションを含むことしかできません。それらはwebアプリケーションを自身のプロセスに載せ、リバースプロキシ構成によってwebサーバ(Apache, Nginx)の後ろに隠されます。
-
-
webアプリケーションはwebサーバに含まれます。この場合、webサーバはまるでアプリケーションサーバであるかのように振る舞います。mod_phpを使ったApacheサーバ上のPHPアプリケーションの状況です。これは必ずしもwebアプリケーションがwebサーバのように同じプロセスの中で実行されることを意味しません: webサーバがアプリケーションを管理する事を意味するだけです。mod_phpの場合は、PHPはApacheのworkerプロセスの中で直接実行されます。
ApacheのためのPhusion PassengerとNginxのためのPhusion Passengerはこのモデルを実装しています。
-
webアプリケーションはwebサーバであり、HTTPリクエストを直接受け取ります。Trac バグトラッキングシステムの場合はそうであり、その独立型のサーバの中で実行されます。ほとんどの構成の場合、それらは直接HTTPリクエストを受け取る代わりにApacheまたはNginxのような本当のwebサーバの後ろにリバースプロキシされます。
Phusion Passengerのスタンドアローンはこのモデルを実装しています。Phusion Passenger スタンドアローンはNginxを内部的に使っているため、それをインターネットに直接晒す事ができます。
-
webアプリケーションは直接HTTPを話しませんが、なんらかのコミュニケーションアダプタを使って直接webサーバに接続することができます。CGI, FastCGI と SCGI がこの良い例です。
これらの説明はwebアプリケーションがPHP、Django、J2EE、ASP.NET、Ruby on Railsまたはそれ以外の何であっても、実質的に全てのwebアプリケーションで正しいです。これらのモデルの全ては同じ機能を提供することに注意してください。つまり、他のモデルでできない事はそのモデルでもできません。聡明な読者はこれらのモデルか最初の図で説明されたものと全く同じであることに気づいたことでしょう。webサーバ、アプリケーションサーバ、webアプリケーション等はそうしたいならブラックボックスとしての一つのもの考えられます。
また、これらのモデルは特定のI/O処理実装を強制しないことに注意してください。webサーバ、アプリケーションサーバ、webアプリケーション等はI/O(言い換えると同時に一つのリクエスト)を連続して処理できるか、複数のI/Oを一つのスレッド(例えば、select(2) または poll(2)を使って) で処理できるか、複数のスレッドかつ/または複数のプロセスを使ってI/Oを処理できるでしょう。
もちろん、さまざまな可能性があります。例えば、ロードバランサーが使われることがあります。しかし、それはこのドキュメントの範疇を超えます。
1.1.1. なぜリバースプロキシなのか?
見てきたように、webアプリケーションまたはアプリケーションサーバが既にHTTPを話しているのに、実際のwebサーバの後ろにリバースプロキシの構成にしなければならないことがしばしば必要になります。これは単にプロトコルを喋るだけでなく適切、安全にHTTPを実装するためです。インターネットは悪意に満ちた環境であり、クライアントは任意のデータを送信したり任意のI/Oパターンを提出することができます。ApacheやNginxのようなWebサーバは既に世界的なI/Oや接続処理コードを実装しており、それらを再発明することは時間の無駄です。結局、リバースプロキシ構成の中にアプリケーションを置くことで、システム全体がより堅牢でより安全になります。
slow clientsを扱うことで典型的な問題が生じます。これらのクライアントはHTTPリクエストの送信に時間がかかり、HTTPリスポンスの受信に時間がかかり、それらが完了するのに多くの秒数が掛かるかも知れません。シングルスレッドのHTTPサーバの実装は、HTTPリクエストの受信、処理、HTTPリスポンスの送信のループが実際の処理はとても短いI/Oの待ちに多くの時間が掛かることになるかも知れません。Worse: クライアントが悪意のあるもので、ソケットをオープンするだけでHTTPリスポンスを受け取らず、結果的にサーバが永久にクライアントを待つことになってそれ以上リクエストを処理できなくなるでしょう。
while true client = accept_next_client() request = read_http_request(client) response = process_request(request) send_http_response(client, response) end
この問題を解決するには多くの方法があります。クライアントごとに1つのスレッドの利用、I/Oタイムアウトの実装、イベントI/O構造の利用、送受信のバッファのためにI/Oのみを置こうなうスレッドまたはプロレスの実装。ポイントはこの特性全ての実装は普通ではないということです。それらをそれぞれのアプリケーションサーバに再実装する代わりに、実際のwebサーバに一部始終をまかせ、アプリケーションサーバとwebアプリケーションはそれぞれが所持しているビジネスロジックに最善を尽くすほうが良いです。
1.2. Ruby RackとRuby on Rails
Ruby webアプリケーションの事実上の標準インタフェースはRackです。Rackはwebアプリケーション開発者が実装すべきプログラミングインタフェースの仕様を定めます。このインタフェースはHTTPリクエストとリスポンス処理を対象とし、どの特定のアプリケーションサーバにも依存しません。どのRack準拠のアプリケーションサーバもRack仕様を実装することができ、全てのRack準拠のwebアプリケーションが動作する、という考え方です。
遠い昔、それぞれのRuby webフレームワークが独自のインタフェースを持っていたため、アプリケーションサーバは明示的にそれぞれのwebフレームワークをサポートする必要がありました。今日、アプリケーションサーバは単にRackをサポートします。
Ruby on Rails はバージョン3.0から完全にRack準拠です。Rails 2.3 は部分的にRack準拠ですが、それより前のバージョンは全くRack準拠ではありません。Phusion Passenger はRails1.xとRails.2バージョンをサポートすると同様にRackをサポートします。
特に興味深い注意すべきことは、Ruby on Railsアプリケーションに占有されている多くのメモリはプログラムコード(言い換えると、 abstract syntax tree (AST))をメモリに保存するのに使われているということです。このことはRuby Enterprise Editionのメモリ統計機能の利用によって観察されます。また、Ruby on Railsアプリケーションのスタートアップに掛かる時間の大部分はRailsフレームワークのブートストラップに掛かっています。
1.3. Apache
Apache webサーバは動的なモジュールシステムと差込可能なI/Oマルチプロセシング(同時に1つ以上のHTTPクライアントを扱う能力)構造をもっています。特定のマルチプロセシング戦略を実装しているApacheモジュールはマルチプロセシングモジュール(MPM)と呼ばれます。シングルスレッド マルチプロセスのprefork MPM は長い間デフォルトでもっとも人気があるものでしたが、最近ではパフォーマンスとスケーラビリティからマルチスレッド/マルチプロセスのハイブリッドのworker MPMの人気が出始めています。更には、Apache2.4ではevented/マルチスレッド/マルチプロセスMPMのハイブリッドのevent MPM が導入され、より多くのスケーラビリティを提供しています。
perfork MPMはmod_phpと一緒によく動作する唯一のMPMのため、今日でも広く使われて続けています。
prefork MPMは複数のworker子プロセスをspawnします。HTTPリクエストは最初にコントロールプロセスと呼ばれるものによって受け付けられ、その後workerプロセスの一つに転送されます。次の章にはprefork MPMの構造を示す図があります。
1.4. Nginx
Nginxは人気が出つつある軽量のwebサーバです。evented I/O構造により、Apacheよりも小さく軽くスケーラブルだとして知られています。とは言っても、NginxはApacheに比べて柔軟性がありません。例えば、動的モジュールシステムがありません: 全てのモジュールは静的にNginxにコンパイルしなければなりません。
2. Phusion Passengerの構造
2.1. 概要
Phusion Passengerの構造は、Web アプリケーションモデルの説明の2番目のモデルによく似ています。言い換えると、Phusion PassengerはApache/Nginxを拡張し、アプリケーションサーバのように振舞うことができます。次の図でそれを示します。
Phusion Passengerは次のものから成ります:
-
Apache モジュール、mod_passenger. これはC++で書かれており、ext/apache2ディレクトリで見つける事ができます
-
Nginxモジュール ngx_http_passenger_module. これはCで書かれており、ext/nginxディレクトリで見つける事ができます。
-
ApacheとNginxモジュールの両方で使われる共通のコード例えば、ヘルパーエージェントがこのコードで共通しています。このコードはほとんどはC++で書かれておりext/commonで見つける事ができます。
そのモジュールは全てのApache/Nginxプロセスで有効です。HTTPリクエストが来ると、Phusion PassengerモジュールはそのリクエストがPhusion Passengerが提供するアプリケーションで処理すべきかどうかを調査します。もしすべきであれば、モジュールは応答するためにアプリケーション(もし必要であれば)を1つ以上生成します。それからそのリクエストをアプリケーションプロセスに転送し、クライアントに生成した応答を転送します。これはPhusion Passenger helper agentの助けによって全て行われます。全てのwoker プロセスによって共有されるべき状態を保持し、webサーバとアプリケーションプロセスの間の内部的なI/Oのほとんどを処理します。
アプリケーションはwebサーバと同じアドレス空間で実行されないことに注意が必要です。この事がPhusion Passengerとmod_php,mod_perlそしてmod_rubyのようなwebサーバの中にあるアプリケーションサーバと違うものにしています。もしアプリケーションがクラッシュあるいはメモリをリークしても、webサーバに何も影響はありません。実際のところ、安定性はもっとも高いところにあるゴールの一つである。Phusion Passengerは、Phusion Passengerが原因でwebサーバがクラッシュしないように注意深く設計、実装されています。
2.2. コードとアプリケーションのspawnとキャッシュ
アプリケーションサーバのとてもnativeな実装はCGIのようにHTTPリクエストを受け取る度にアプリケーションプロセスをspawnします。しかしながら、Rubyアプリケーションのspawnは典型的に高価です。最近のコンピュータ上では数秒掛かるかも知れませんし、負荷の高いサーバではもっと掛かるかも知れません。nativeでは無い実装はLighttpdのFastCGI実装のようにアプリケーションプロセスをspawnし続けるでしょう。しかしながら、これにはまだ問題があります:
-
Rails webサイトへの最初のリクエストは遅く、そしてその後のリクエストは速いでしょう。しかし同じwebサーバにある異なるRails webサイトへの最初のリクエストはまだ遅いでしょう。
-
この文章の前のほうで説明したように、Railsアプリケーションの多くのメモリはRuby on RailsのフレームワークやアプリケーションのASTを保持するために使われます。特に共有ホストとメモリに制限のあるVirtual Private Servers (VPS)でこれが問題に成り得ます。
これらの問題は解決可能なものであり、そうします。
最初の問題はRailsアプリケーションのpreloadで解決できます。言い換えるとwebサイトへリクエストが来る前にRails アプリケーションを実行します。これはほとんどのRailsホストで採られている方法であり、例えばMongrel clusterの常に実行している形式です。しかしながら、これは共有ホストでは受け入れられないものです: そのようなアプリケーションはたとえ何もしていなくてもそのまま存在していてメモリを浪費するでしょう。その代わりに、前述の問題の両方を解決する異なる方法を採ります。
Railsアプリケーションをspawn serverを経由してspawnします。spawnサーバはRuby on Railsフレームワークコードとアプリケーションコードをメモリにキャッシュします。最初のRailsアプリケーションのspawnはまだ遅いでしょうが、続くspawnはとても速いでしょう。さらには、フレームワークコードはアプリケーションから独立してキャッシュされるため、異なるRailsアプリケーションのspawnも既にキャッシュされたRailsフレームワークのバージョンをアプリケーションが使う限りはとても速いでしょう。
spawnサーバを使うことは異なるRuby on Railsがお互いにメモリを共有することを意味しています。したがって問題の#2を解決します。次の章で詳しく説明します。
しかし、フレームワークコードとアプリケーションコードのキャッシュにも関わらず、spawnはHTTPリクエストに比べてまだ高価です。可能な時はいつでもspawnを避けたいと思います。これがアプリケーションプールを導入する理由です。Spawnされたアプリケーションインスタンスは活動状態のままで、それらのハンドルはそれぞれのアプリケーションインスタンスが後で再利用できるようにこのプールに保持されます。このようにして、Passengerは平均的にとてもよいパフォーマンスがあります。
アプリケーションプールは異なるworkerプロセスの間で共有されます。workerプロセスはお互いにメモリを共有できないため、共有されたメモリはアプリケーションプールを実装するために使われるか、client/server構造が実装されなければなりません。実装が容易なため、後者を選択しました。Apacheのコントロールプロセスはアプリケーションプールにとってサーバのように振る舞います。しかしながら、これは全てのHTTP リクエスト/リスポンス データがコントロールプロセスを通ることを意味しません。workerプロセスはRailsアプリケーションとの接続セッションのためにプールを要求します。セッションを取得できるとworkerプロセスはRailsアプリケーションと直接通信を行うでしょう。
アプリケーションプールはmod_passengerの中に実装されています。それについてはthe C++ API documentationの中で、特にApplicationPool、StandardApplicationPoolとApplicationPoolServer クラスで詳しい文章を見つけることができるでしょう。
アプリケーションプールは、アプリケーションのspawn、spawnされたアプリケーションのハンドルのキャッシュ、長期間何もしないアプリケーションの掃除に対して責任があります。
2.3. spawn サーバ
spawnサーバはRubyで書かれており、そのコードはlib/passengerディレクトリで見つけることができます。そのメインの実行コードはbin/passenger-spawn-serverです。The spawn server’s RDoc documentationは詳細に実装を説明します。
spawnサーバは3つの論理層から成ります。
-
spawnマネージャー。これは一番上の層にあり、その下の全ての層の前面のように振る舞います。spawnサーバとやり取りをするクライアントはこの層とのみ通信を行います。
-
フレームワークspawnerサーバ 。 spawnマネージャーはそれぞれのバージョンごとのRuby on Railsに対して、フレームワークspawnerサーバをspawnします。それぞれのフレームワークspawnerサーバは正確に1つのRuby on Railsフレームワークバージョンについてコードをキャッシュします。アプリケーションに対するspawnリクエストはアプリケーションの正しいRuby on Railsバージョンを含むフレームワークspawnerサーバに転送されます。
-
アプリケーションspawnerサーバ。これはフレームワークサーバに対するspawnマネージャーにおけるフレームワークspawnerサーバです。フレームワークspawnerサーバはそれぞれのRuby on Railsアプリケーションに対するアプリケーションspawnerサーバをspawnします。(ここで"アプリケーション"は実行プロセスでは無く、(ソースコード)ファイルのセットを意味します。)アプリケーションspawnerサーバは正確に一つのアプリケーションのコードをキャッシュします。
ご覧のようにコードキャッシュの2つの層があります: spawnサーバが新しいアプリケーションインスタンスをspawnするようにリクエストを受けた時、spawn serverは正しいフレームワークspawnerサーバにリクエストを転送するでしょう。 (そしてもしフレームワークワークspawnerサーバが既に存在していないならばフレームワークspawnerサーバを)spawnするでしょう)、今度はフレームワークspawnerサーバがリクエストを正しいアプリケーションspawnerサーバに転送します。 (存在しない場合は再び生成されます)).
それぞれの層はすぐ下にある層にのみ責任を持ちます。spawnマネージャはフレームワークspawnerサーバについてのみ知っており、フレームワークspawnerサーバはアプリケーションspawnerサーバについてのみ知っています。しかしながら、アプリケーションspawnerサーバはspawnされたアプリケーションインスタンスについて責任を持ちません。もしアプリケーションインスタンスがmod_passengerにspawnされたならば、その情報はmod_passengerに返送されるでしょう。mod_passengerは(アプリケーションプールを通じて)アプリケーションインスタンスの生存期間の管理に完全に責任を持ちます。
またそれぞれの層は別個のプロセスであることに注意してください。一つのRubyプロセスは一つのRuby on Railsフレームワークと一つのアプリケーションをロードできるため、このことは必要です。
2.3.1. メモリの共有
最近のほとんどのUnixオペレーティングシステムでは、子プロセスが生成された時に子プロセスはほとんどのメモリを親プロセスと共有するでしょう。プロセスはお互いのメモリにアクセスできるようにサポートされておらず、親プロセスまたは子プロセスによって書き換えられるたびにオペレーティングシステムがメモリの断片のコピーを作成します。これはcopy-on-write (COW)と呼ばれます。詳しい背景情報は Ruby Enterprise Edition’s websiteで見つけることができます。
spawnサーバはこの便利な事実を利用します。それぞれの層はRuby ASTメモリが書き込まれない限りはそれをその下の全ての層と共有します。これは全てのspawnされたRailsアプリケーションが可能であればRuby on Railsフレームワークのコードを共有し、同様にアプリケーションをお互いに共有することを意味します。これにより大幅にメモリの利用量が削減されます。
|
Ruby Enterprise Edition が使われた時のみメモリの共有は動作します。標準のRubyインタプリタのガベージコレクションはcopy-on-wirte firendlyでは無いからです。詳しくはRuby Enterprise Edition のwebサイトを訪ねてください。 Passengerは標準のRubyと良く動作します。Railsスタートアップの時間を短くすることもできます。メモリの共有を使うことができないだろうということだけです。 |
Rubiniusのガベージコレクションは既にcopy-on-wirte friendlyであることに注意してください。
2.4. 同時リクエストの処理
先に説明したように、1つのRailsアプリケーションインスタンスは同時に一つのリクエストを処理することしかできません。これは明らかに好ましいものではありません。しかし解答に急ぐ前に、"competition"がどのようにこの問題を解決しているかを見てみましょう。PHPは似たような問題を抱えています: 1つのPHPスクリプトも同時に一つのリクエストを処理することができません。
-
mod_php はこの問題をApacheのMPMを使って"解決"しています。言い換えると、mod_phpはそれ自身では何もしません。1つのApache worker プロセス/スレッドは同時に1つのPHPリクエストしか処理できませんが、Apacheは複数のworker プロセス/スレッドをspawnします。
-
PHP-FastCGIは複数の永続的なPHPサーバをspawnすることでその問題を解決します。PHPサーバの数はApacheのworker プロセス/スレッドの数と無関係です。このアプローチは既存のRailsセットアップとかなり似ています。フロントエンドのwebサーバが永続的なMongrelクラスタへのリクエストを代理します。
Passengerはリクエストごとに新しいRailsアプリケーションをspawnさせるので前述したように到底受け入れがたいほど遅く、mod_phpのやり方を使うことができません。Passenegrはその替わりにPHP-FastCGIのやり方を利用します。アプリケーションインスタンスのプールを保持し、リクエストを受け取るときにそのリクエストをプール内のアプリケーションインスタンスの一つに転送します。プールのサイズは設定可能で、負荷が高くメモリが少ないサーバの管理者にとって有用です。
読者はアプリケーションのプールのアルゴリズムの学習に興味があるかも知れません。それはつまらないものではありません。アルゴリズムはApplicationPool algorithm.txtで詳しく説明されています。
3. Appendix A: このドキュメントについて
このドキュメントのテキストは Creative Commons Attribution-Share Alike 3.0 Unported Licenseでライセンスされています。