Rails APサーバの比較検証(Puma, Unicorn, Passenger)

 仕事でRailsを使うことになり、APサーバの選定にあたってPuma, Unicorn, Passenger の比較検討を行いました。方法としてはJMeterでAPサーバにデプロイしたRailsアプリケーションに対して負荷をかけられるだけかけるというやり方です。


試験環境


試験の環境としては下記の構成です。

サーバ構成

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サーバのアーキテクチャの比較は下記の通りです。

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

ワーカプロセス数 スレッド数 サンプル数 平均値(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

ワーカプロセス数 サンプル数 平均値(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

プロセス数 サンプル数 平均値(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サーバを決定する

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と比 べると遅いのが懸念点。

定数スループットタイマを使って実際のアクセス数に近い負荷で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が返される。
[ 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ドメインソケットで行う設定で負荷をかけたところ、下記エラーが多発。
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ステータスコードは 502 Bad Gateway

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"
[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の設定にも下記を追加しました。

send_timeout 300;
proxy_connect_timeout 300;
proxy_read_timeout 300;

これでひとまずエラーは回避できました。。スループットの検証のためにタイムアウト値を大きくしていますが、実際の環境ではタイムアウト値は小さく設定する必要があります。