導入
Rails(に限らずRuby)で作ったWebアプリケーションを運用する場合、次のような構成を取ることが一般的かと。
Internet <-外と中の壁-> nginx <--> Unicorn <--> Rails
この際、nginx/Unicornのワーカー数の設定をどうするかで迷ったことはないでしょうか。
nginxはイベント駆動型なため、基本的にはワーカー数 > CPU数にしても意味がありません。自動設定ではCPU数と同じになりますが、わざわざ変更する意味もないでしょう。
一方Unicornはシンプルなマルチプロセスモデルであるため、ワーカー数の設定が重要な意味を持ちます。
ワーカー数の設定
もしアプリケーションが純粋にCPUだけを使うものであった場合は、nginxと同様にワーカー数をCPU数より増やしても意味がありません。しかし実際のアプリケーションはDBを読んだり外部APIに問い合わせをしたり、I/O待ちの時間が少なからず存在するはずです。
もしUnicornワーカー数がCPU数とまったく同じであれば、このI/O待ちの時間はCPUが何もしていません。
I/O待ちのプロセスがいるのであれば、その時間CPUを使うプロセスがいないと貴重なCPU資源を無駄にしていることになります。この時点で少なくともCPU数よりはワーカー数が多いほうがいいだろうというのはわかります。でも、どのくらい多くすればいいのでしょうか。
調べてみると、CPU数 + 1がいいよ、みたいな意見を見ましたが、これにはまったく根拠がありません。+1ってなんだよ、と。そこで、ちゃんと考えてみることにしました。
算出方法
モデル
1リクエストをアプリケーションが処理する際の流れを超単純化し、次のようなものとします。
(I/O: r[msec]) -> (Computing: t[msec])
はじめに r[msec] かかるI/Oバウンドな処理があり(この間CPUは一切使用しない)、それが終わったら t[msec] かかるCPUバウンドな処理があるという。
計算式
ワーカー数が最適なときは、次のような等式が成り立っているはずです。
(単位時間あたり、I/O処理が終わってCPU処理に入る数) = (単位時間あたり、CPU処理が終わって次のリクエストを処理し始める数)
実効ワーカー数をN, CPU数をn, I/O処理にかかる時間を r, CPU処理にかかる時間をt, 単位時間をTとおくと、次のような式が成り立つはずです。
(N - n) \times \frac{T}{r} = n \times \frac{T}{t}
(N-n)個のワーカーがI/O処理を行っており、n個のワーカーがCPUを使っています。ワーカーを増やせば増やすほど単位時間あたりI/O処理が終わるリクエストが多くなりますが、CPU処理が終わるリクエストの数はCPU数にのみ依存しています。
実例
平均30msecでレスポンスを返してくれる外部APIに依存するAPIサーバーに対して負荷試験を行った結果として、5まではワーカー数を増やしても性能が向上するがそれ以上増やしても変化がないという結果が得られました。この数字をさきほどの式にあてはめると、 t=20msec というありそうな数字に。その後実際に計測してみたところ、割と近い数字に。
むやみにワーカー数を増やしても、CPUを使う処理を早くしないと意味がないという結論が得られました。
注意
もちろんこれまで述べたことはCPUを使うプロセスはすべてUnicornのワーカープロセスである、という想定に基づいたものです。実際にはOSやらデーモンプロセスやらもCPUを使用していますが、それらの影響は軽微であり無視して良いという切り捨てを行いました。