UnicornでSinatraアプリをデプロイしてみた

 最近は仕事でSinatraアプリを書いたりしているので、Sinatraアプリを動かすためにはどのHTTPサーバを使うのがベストなのかが気になっている。(先に結論を書いておくけれど、どれがベスト、という唯一の選択肢は今のところありません。適材適所です。)
 SinatraはRackの上に構築されているので、Rackに対応したHTTPサーバーを使って動かす事になるのだが、この数がやたらと多く、どれを使えばいいのか迷う。代表的なものを挙げただけでも、WebRick, Mongrel, Thin, Unicorn, Passenger(Apacheとかに組み込んで使うやつ), FastCGI, (普通の)CGI、これぐらいは選択肢がある(いくつかHTTPサーバじゃない物も混ざっているが、Rackが対応してるという点は共通している)。
 WebRickはそもそもパフォーマンスに重点を置いていないし、Mongrelはメンテされてないから新規で使いたくはない、Passengerは高負荷時に関していい事を聞かない、FastCGIは情報がなくて不安、CGIは論外、という事で、これまではパフォーマンスが必要な場合はThin、そうでない場合はデフォルトで入っていて使いやすいWebRickという使い分けをしてきたのだが、最近はUnicornが気になっている。
 前置きがずいぶんと長くなったが、そういうわけで、Unicornを一度、試してみることにした。

Unicornの特徴

 Unicornを紹介しているブログ記事をいくつか読んでみたのだが、UnicornはこれまでのHTTPサーバとはけっこう違うアーキテクチャを採用している事が分かった。
 Unicornには、マルチスレッドだのepollだの、1プロセスあたりの同時接続数を上げるための工夫は一切ない。1プロセスでは同時に処理できるリクエストはわずかに1つである。その代わりに、いくつかの工夫が施されている。
 例えば、RailsSinatraのアプリをロードした後にプロセスをフォークする事ができる。これによって、コードのロードが1回しか行われずにすむので、起動が非常に高速になる。最近のサーバーだと数十プロセス程度は余裕を持って動かすことができるが、この場合、Railsを1プロセス起動するのに0.5秒ですむとしても、数十プロセスの再起動には十秒以上はかかる計算になる。起動がプロセスのフォークで済むなら再起動は1秒以内に完了する。実際には起動も並列で走らせられるだろうからここまでの差はつかないだろうけれど、フォークで済む方が圧倒的に速いのは確かだ。
 また、通常のリバースプロキシではリクエストはラウンドロビンなどで空いてそうなプロセスに送りつけられるが、Unicornでは空いているプロセスが自分でリクエストを取りにいくことができる。推奨されている構成では、手前にリバースプロキシとしてnginxを立て、nginxからUnix Domain Socketにリクエストを書き込む。書き込まれたリクエストは空いているUnicornが取りにいく。同時に複数のUnicornプロセスが空いていた場合、OSのスケジューラーが先に動かした方のプロセスがリクエストを取って処理することになる。
 Unicornアーキテクチャは、大量のアクセスを安定してさばくためのアーキテクチャである。安定して、の部分が非常に重要で、同時接続数が1つ(サーバは複数のリクエストを同時に処理しない)という事は、一見しょぼいように思われるが、並列にリクエストを処理する場合と比べてコードがシンプルに書け、シンプルであるということはバグが出にくく安定しているという事である。また、どうしてもプロセスを殺さないといけない場合にも、巻き添えを食うリクエストが最小で済む。Unicornは同一プロセスでウェブアプリケーションが動く訳だが、ウェブアプリケーションにかけられる開発コストは一般にミドルウェアの開発コストよりも低い場合が多く、したがってミドルウェアほど信用は置けない。そのようなプロセスを無闇にkillする事は危険である。(本当は安全なアプリケーションを書くべきなんだけど。)Unicornがリクエストを取りにいくという点も、たまたま混雑したプロセスにリクエストを送りつける危険性がなくせるというメリットがある。リバースプロキシがしっかりしていれば、アクセスが集中したとしても、Unicornで捌ききれるだけのリクエストはハングせずに処理することができる。
 安定してリクエストの取りこぼしが少なく、また運用も楽である、というところに主眼が置かれたアーキテクチャであると感じた。

Unicornの制限

 Unicornアーキテクチャは万能ではない。例えば、Unicornを単独で使うことは避けるべきである。同時接続数が各プロセスに1つずつしかないので、インターネットのどこかのクライアントからのアクセスを直接受け付けると、例えばブラジルからリクエストが来て、3秒ぐらいプロセスを占有するかもしれない。物理的に遠くにサーバーがあるとデータの送受信にどうしても時間がかかるので、その間プロセスが占有されてしまう。そういう訳で、インターネットからのリクエストを直接処理することは、Unicornにとっては致命的な性能の劣化を招く可能性がある。これは同一LAN内から負荷テストをしているだけでは見つけにくい問題であり、はまるとやっかいなので、Unicornを使う際にはこの事は頭の片隅に留めておいた方が良い。この制限を回避するために、nginxやapacheなどをリバースプロキシとしてUnicornの前に設置して、リクエストをクライアントに送信する部分は並列性の高いサーバに任せる事が推奨されている。
 また、同時接続可能数が少ないので、cometのようにコネクションを張りっぱなしにするような用途にもまったく向かない。Unicornはあくまでも短時間で実行が終わるリクエストを大量に処理するためのサーバである。

Rainbows

 同時接続数が多い、cometなどにも使えるサーバとして、Unicornをベースに複数リクエストの同時処理機能を加えたRainbowsがある。並列処理の実装はEventMachine, Rev, NeverBlockなどいろいろなものをサポートしており、そのどれかを選択する事ができる。同時接続数の増加の代償として、Unicornのシンプルさはやや失われる。Unicornの方がトラブルが起こった場合にデバッグしやすい(し、トラブルも起こりにくいはずである)ので、同時接続数を増やす必要がなければUnicornを使った方が良い。

実際に試してみた

 unicorn.confという名前で設定ファイルを書いた。これだけだとよくわからないが、Rubyスクリプトになっている。

listen "/tmp/unicorntest.sock"
worker_processes 4
pid "/var/lib/pids/unicorn.pid"
stderr_path "/var/log/unicorn.log"
stdout_path "/var/log/unicorn.log"

 Unix Domain Socketを使うことにしたので、手前にnginxでリバースプロキシを立てた。nginxの設定はこんな感じ。

upstream unicorntest {
    server unix:/tmp/unicorntest.sock;
}

server {
	listen   80;
	server_name  localhost;
        (中略)
	location ~ ^.*$ {
	        proxy_pass  http://unicorntest;
 	}
}

 config.ruのあるディレクトリで

unicorn -c unicorn.conf

 を実行したら動いた。特にハマるところはなかった。
 動いたので、簡単にSinatra用のブログアプリでパフォーマンスの実験をしてみた。環境はlinodeの一番安いやつを使った。ab -n 1000 -c 10 http://localhost/ でRequests per secondが352.09 [reqs/sec]だった。マシンスペックは今となってはそんなに高い方ではないと思われるので、それで実際に350 reqs/secも出るならパフォーマンス的には充分かな。