仕事でRailsを使うことになり、APサーバの選定にあたってPuma, Unicorn, Passenger の比較検討を行いました。方法としてはJMeterでAPサーバにデプロイしたRailsアプリケーションに対して負荷をかけられるだけかけるというやり方です。
# 試験環境
試験の環境としては下記の構成です。
- Ruby2.0, Rails4
- アプリケーションサーバ:1台(VM)
- JMeterサーバ:3台(VM)
- JMeterクライアント:1台(通常の作業PC)
|caption=サーバ構成
|
|hostaname, CPU, 仮想コア数(Per CPU), Memory, Disk, 用途
|
|loadtest01, 2, 4, 8192MB, 20GB, APサーバ
|loadtest02, 1, 1, 4096MB, 20GB, JMeterサーバ
|loadtest03, 1, 1, 4096MB, 20GB, JMeterサーバ
|loadtest04, 1, 1, 4096MB, 20GB, JMeterサーバ
# サーバアーキテクチャの比較
各APサーバのアーキテクチャの比較は下記の通りです。
|caption=各APサーバのアーキテクチャの比較
|
|, Puma, Unicorn, Passenger
|
|デプロイモデル, Reverse Proxy, Reverse Proxy, Nginx Module
|Process/Thread, Multi Processes / Multi Threads, Multi Processes / Single Thread, Multi Processes / Single Thread(OSS Edition)
|||, ||, ||, Multi Processes / Multi Threads(Commercial Edition)
|その他特徴, Workerプロセスごとにスレッドを立ち上げる, Pull型(Workerプロセス側からキューのタスクを取得しにいく), トラフィックに応じてプロセス数を自動調整。商用Editionのみマルチスレッド利用可能
# 処理内容
処理内容としては、純粋にAPサーバの処理能力の比較にするため、DBアクセス等はせず、フィボナッチ数列を計算して返す処理を行います。ロジックは書きサイトを参考にしました。
ruby>>
class StaticPagesController < ApplicationController
def fibonacci
@fibonacci = calc_fibonacci(params[:n].to_i)
end
private
def calc_fibonacci(n)
if (n < 2)
return n;
else
p2 = 0;
p1 = 1;
2.upto(n) { p2, p1 = p1, p2 + p1 }
return p1;
end
end
end
<<--
# JMeterシナリオ
JMeterのシナリオとしては、
- JMeterスレッド数:100
- Ramp up:0
- ループ数:無限ループ
- 継続時間:5分間
という内容です。
JMeterのシナリオファイルはこちらから取得できます。
# APサーバごとの設定値による差の検証
まずは各APサーバごとに、設定値の変更による差を検証しました。
## Puma
|caption=Puma検証
|
|ワーカプロセス数, スレッド数, サンプル数, 平均値(msec), 中間値(msec), 90%LINE (msec), 最小値(msec), 最大値(msec), スループット(/sec), KB/sec, エラー数
|
|1, 500, 43079, 2090, 1978, 2379, 129, 2519, 141.68, 414.67, 多発
|100, 8, 241811, 365, 57, 1071, 6, 46414, 799.43, 2339.49, 0
|100, 32, 243620, 362, 60, 1136, 6, 16890, 805.37, 2356.83, 32
|64, 32, 242825, 365, 51, 1355, 7, 103157, 802.35, 2347.94, 1
|32, 32, 241934, 366, 46, 760, 6, 252504, 798.24, 2335.89, 0
|16, 64, 237270, 373, 31, 339, 6, 298546, 782.00, 2288.34, 0
|16, 96, 231996, 382, 37, 644, 6, 241748, 762.49, 2231.26, 0
- ワーカプロセス数1のケースでは全くCPUを使い切れていませんが、他のケースではCPUを使い切った状態で動いていました。
- スレッド数を増加させてもリソースの使用量はあまり変化しませんでしたが、プロセス数を増やすと使用量がかなり増えます。
- メモリに余裕があってもCPUが振り切る方が早いです。
## Unicorn
|caption=Unicorn検証
|
|ワーカプロセス数, サンプル数, 平均値(msec), 中間値(msec), 90%LINE (msec), 最小値(msec), 最大値(msec), スループット(/sec), KB/sec, エラー数
|
|1, 4294, 11345, 572, 50553, 77, 176247, 11.74, 34.54, 433
|8, 8686, 6026, 562, 13721, 77, 141193, 22.15, 65.15, 389
|32, 42943, 1465, 325, 2844, 51, 183552, 103.48, 304.38, 280
|64, 191254, 440, 170, 375, 26, 115819, 498.54, 1466.46, 48
|96, 247191, 360, 170, 261, 49, 82454, 773.58, 2275.69, 3
|128, 256050, 343, 252, 452, 13, 20587, 836.00, 2459.82, 0
|160, 254117, 347, 291, 537, 15, 8825, 835.54, 2458.54, 0
|192, 252158, 349, 302, 588, 13, 5746, 833.94, 2453.87, 0
|224, 229723, 383, 275, 592, 9, 299799, 697.12, 2045.15, 2
- ワーカプロセス数が1の場合は1CPUしか使われない
- ワーカプロセス数をコア数と同じ8にした場合でも、すべてのコアが使われるわけではない
- ワーカプロセス数をコア数の4倍の64まで増やすとかなりCPUが使われるようになってきて、128まで増やすとCPUはほぼ使い切られて、メモリの使用率も高くなる。
- ワーカプロセス数を192まで増やした時点でメモリもほぼ使い切られ、224まで増やすとメモリが足りなくなり、Swapが発生して遅くなる。
## Passenger
|caption=Passenger検証
|
|プロセス数, サンプル数, 平均値(msec), 中間値(msec), 90%LINE (msec), 最小値(msec), 最大値(msec), スループット(/sec), KB/sec, エラー数
|
|1, 65188, 1376, 1282, 1325, 1224, 8690, 215.15, 646.07, 0
|8, 245153, 360, 355, 368, 297, 2441, 810.57, 2433.99, 0
|16, 246737, 357, 353, 381, 279, 2339, 815.77, 2448.77, 0
|24, 241165, 366, 360, 391, 281, 2414, 797.27, 2392.30, 0
- プロセス数が1だと4コアしか使われていない
- プロセス数をコア数と同じにするとどのコアもかなり使われるようになる。メモリは余裕あり。
- プロセス数をコア数以上に増やしていってもCPU, メモリの使用率はあまり変化しない。CPUは80%まで使われることが多いが、アイドルが10%前後残っていて、使い切ることはない
# 各APサーバの最適値同士を比較
各APサーバの最適値同士を比較して、使用するAPサーバを決定する
|caption=総合
|
|APサーバ, サンプル数, 平均値(msec), 中間値(msec), 90%LINE (msec), 最小値(msec), 最大値(msec), スループット(/sec), KB/sec
|
|Puma, 241811, 365, 57, 1071, 6, 46414, 799.43, 2339.49
|Unicorn, 252158, 349, 302, 588, 13, 5746, 833.94, 2453.87
|Passenger, 246737, 357, 353, 381, 279, 2339, 815.77, 2448.77
- Pumaはスループットが一番低いのに加えて最大値、90%LINEの遅さ、不安定さで候補から除外。
- Passengerは安定しているが最小値が他の2つに比べて大きい。90%LINE、最大値は優秀。
- Unicornはスループットが一番大きいのと、最小値、平均値も悪くない。最大値、90%LINEがPassengerと比 べると遅いのが懸念点。
|caption=定数スループットタイマを使って実際のアクセス数に近い負荷でUnicornとPassengerを比較
|
|APサーバ, サンプル数, 平均値(msec), 中間値(msec), 90%LINE (msec), 最小値(msec), 最大値(msec), スループット(/sec), KB/sec
|
|Unicorn, 23075, 12, 10, 13, 4, 116, 75.32, 221.50
|Passenger, 23092, 11, 9, 12, 5, 118, 75.38, 226.67
- UnicornとPassengerでほぼ差はなし
- Passengerは最初の比較時に他と比べて最小値が遅いことが懸念だったが、実際の状況に近いアクセス数では 問題なく、Unicornが負荷が低い状態でもメモリリソースを消費するのに対してPassengerはリソースの消費が少ないこと から、Passengerを第一候補と考える。
# 検証時の問題点
検証時に発生した問題点を参考までに記載しておきます。
## Passenger
- デフォルトの状態で負荷をかけたところ、下記のようなエラーが多発してHTTPステータスコード 503が返される。
sh>>
[ 2013-11-20 12:21:13.0578 17419/7fbbbc9da700 Pool2/Group.h:331 ]: Request queue is full. Returning an error
<<--
リクエストのQueueがあふれたことによるもののようです。Queue のサイズのデフォルトは100なので、無制限にするために passenger_max_request_queue_size を0に設定したところ、エラーは出なくなりました。
## Unicorn
- Nginxとの連携をUnixドメインソケットで行う設定で負荷をかけたところ、下記エラーが多発。
sh>>
2013/11/20 11:32:35 [error] 27462#0: *622742 connect() to unix:///home/test_user/server-proto/unicorn.sock failed (11: Resource temporarily unavailable) while connecting to upstream, client: 192.168.51.234, server: loadtest01.test.com, request: "GET /fibonacci?n=1000 HTTP/1.1", upstream: "http://unix:///home/test_user/server-proto/unicorn.sock:/fibonacci?n=1000", host: "loadtest01.test.com:3090"
<<--
下記サイトなどを参考に、TCPポートによる連携に変更しました。
[http://www.faultserver.com/q/answers-need-to-increase-nginx-throughput-to-an-upstream-unix-soc ket-linux-kernel-tun-398972.html](http://www.faultserver.com/q/answers-need-to-increase-nginx-throughput-to-an-upstream-unix-soc ket-linux-kernel-tun-398972.html)
変更後に負荷をかけたところ、エラーは概ね解消しました。負荷をかけ続けていると、件数は少なくなったもののエラーが発生しました。HTTPステータスコードは 502 Bad Gateway
sh>>
2013/11/20 14:45:29 [error] 25979#0: *254606 upstream prematurely closed connection while reading response header from upstream, client: 192.168.51.232, server: loadtest01.nubee.com, request: "GET /fibonacci?n=1000 HTTP/1.1", upstream: "http://127.0.0.1:3070/fibonacci?n=1000", host: "loadtest01.test.com:3090"
<<--
sh>>
[2013-11-20 14:44:09.109250] ERROR worker=114 PID:26737 timeout (31s > 30s),
killing
[2013-11-20 14:44:09.119595] ERROR reaped #<Process::Status: pid 26737 SIGKILL
(signal 9)> worker=114
[2013-11-20 14:44:10.140398] INFO worker=114 ready
<<--
検証のためにUnicorn側のタイムアウト設定を大幅に増やしました。30秒 → 300秒に変更。Nginxの設定にも下記を追加しました。
sh>>
send_timeout 300;
proxy_connect_timeout 300;
proxy_read_timeout 300;
<<--
これでひとまずエラーは回避できました。。スループットの検証のためにタイムアウト値を大きくしていますが、実際の環境ではタイムアウト値は小さく設定する必要があります。