随分長いことブログ放置してしまったのだけどネタ見付けたので久々の記事。

UnicornはPassengerより遅かった?

なんかTwitterで「アクセス少ないときはPassengerよりUnicornのが速いのに、アクセス多くなってきたらその逆になった」って話をみかけたので、それ単にUnicornのworkerが足りないんじゃないの、と返したのだけど、どういうことかという話を少しまとめる。

まず、Unicornのworkerは1プロセスにつき1度に1リクエストしか処理しない。だから例えば、凄い大雑把な計算だけど、平均50msくらいでレスポンスを返すアプリだとすれば、1workerは20req/secくらいは返せるかなと見積もって、ピーク時に100req/secくらいアクセスがありそうだったらworkerを5個くらい立てとくかな、足んなかったらもうちょっとかな、みたいに考える。実際どんくらいのアクセスなのかは聞いてないので知らないけど、Nginxが10workerいるのにUnicornが2workerだって言ってたから、普通逆じゃない、とつっこんだ。Nginxは逆に1workerで複数リクエスト捌ける(数千くらい同時にアクセスされても耐えられるらしい)のでCPUのコア数と同じかちょっと多いくらいでよくて、Unicornのworkerの方をガンガン立ち上げるべき。

Passengerもそれ自体は同じなんだけど、Passengerの場合負荷上がってきたら空気読んで設定した上限までプロセス数を増やしてくれるので、「アクセス増えても強い」のではなくて「アクセス増えたらいつの間にか仲間呼んで倍の人数で戦ってた」とかそういうことじゃないかと思う。上記逆転が起こるのは「Unicornのworkerが少ない」「リクエスト数が増えても1個1個の処理時間は大して変わらない」「メモリやCPUには余裕がある」って条件のときだと思うので、多分Unicornのworker数を適切な数に調整すれば解決する。

workerの数増やしたらメモリ食うんじゃ、って言う話なら、Unicornはworkerプロセスを立ち上げる前にアプリをpreloadする仕組みがある。アプリ初期化時点で必要なライブラリを全部読み込んであれば、workerプロセスの起動は単なるforkなので、CopyOnWriteで親プロセスとメモリを共有して効率的に使ってくれそうな気配はある。それでも食うっちゃ食うけども、worker一杯立ち上げたらメモリ足んなくなるならPassengerでも同じ話。平常時には節約してくれるけども、裏を返せば急激なアクセス増大に際しては慌ててプロセスを沢山作るコストで負荷上がることもあるし、単体で比較するとUnicornのがまぁ普通に強い。大量アクセスがある程度定期的に来るの分かってるならUnicornで、滅多にないならお手軽なPassengerで、みたいな使い分け。

Rainbows!って何

ここまでの話はあくまで、1個1個のリクエストは大して重くないけど一度に沢山のリクエストが来ちゃったら遅くなった、みたいな場合。例えばさっきと違って、平均で見れば100msくらいなんだけど、20回に1回くらいの割合で1500msくらいかかるリクエストがくることがある、とする。それがたまたま5人同時にリクエストしてしまったら、5workerがそれぞれ1.5秒ずつ拘束されてその間一切レスポンスを返せなくなる。内部ではファイルサーバにデカいファイル書いてるとか、DBに重いクエリ投げてるとか、API叩いて外部からデータ取って来てるとかで、実際にはアプリ自体は待ってるだけでCPUもメモリもスッカスカ、みたいな状態であっても。それ以外のリクエストは30msくらいで即答できるのだとしても。

Rainbows!の場合はUnicornをベースにしているけれども、EventMachineとかRevacatorみたいなイベント駆動ライブラリを使って1workerで並列にリクエストを処理できるようにしている。さっきみたいに長時間待ちになるリクエストが来てても、待ってる間に他の数十ms程度で返せるリクエストを受け取って処理して返せる。1プロセス1リクエストの枷が外れるので素のUnicornよりはworker数減らせてコンパクトになるかもしれない。そしてイベントループの中でアプリが動くのでAsyncSinatraみたいなフレームワークを使ってアプリ内で非同期処理を書ける。なので重いリクエストがある場合はこっちを使うといい、って話になる。

なんだ良いことづくめじゃん、Unicornいらない子じゃん、ってなるかと思えばそうでもない。Unicornがそもそもpreforkで1プロセス1リクエストでみたいな、ひどく割り切った、見ようによっては割と古典的な仕組みになってるのは、先行していたイベント駆動型のThinみたいなアプリケーションサーバに対しての「シンプルな方が作るのも管理も楽だし、なんだかんだでパフォーマンスも出るじゃん」みたいなアンチテーゼ的なところだったりするので、わざわざ複雑さを戻してるRainbows!はそういうメリットとのトレードオフになる。使いこなすと素敵な感じにはなるけど、worker増やせば解決する程度の話なら素のUnicornでも大して困らない。

追記

「Unicornシグナル送ればプロセス増やしたり減らしたりできるから、netstatとか見て詰まってそうだったら自動で増えるようにしてるー。滅多に増えないけど」っていう情報を貰った。TTINでプロセス増やす、TTOUでプロセス減らすとかできるので、自動で増減して欲しければなんか適当に監視スクリプト書いて適宜シグナル送るようにしとけばいいかも。まぁ手で送ってもいいし。