rails5からは標準のWebサーバがPumaになって、Unicornの代わりにPumaを使う人が増えてきました。
ただせっかくPumaを使うのであれば、Unicornとの違いを意識して使いたい
というわけで、ありがちではありますが、PumaとUnicornの比較をしてみたいと思います。
マルチプロセスとマルチスレッド
Unicornはマルチプロセス
Pumaはマルチスレッドで動きます
Unicornはプロセスごとに通信処理をおこなうので、重たい通信があった場合そこでブロックされて全体が重くなってしまいます。
これをさばけるようにするにはUnicornのworker数を増やす必要があるのですが、CPUのコアの数による処理制限があるためむやみには増やせません。
Pumaはマルチスレッドで動くので、プロセスを増やす必要がないため、リソースが少ない中でも効率的にリクエストをさばくことが可能になります。
プロセスとスレッドの正しい違いについてはこちらを御覧ください!
https://moro-archive.hatenablog.com/entry/2014/09/11/013520
rubyの処理系による違い
Pumaはマルチスレッドで動くことはわかりました。
ただここで問題があります。
現在の実装では Ruby VM は Giant VM lock (GVL) を有しており、同時に実行される ネイティブスレッドは常にひとつです。 ただし、IO 関連のブロックする可能性があるシステムコールを行う場合には GVL を解放します。
https://docs.ruby-lang.org/ja/2.5.0/doc/spec=2fthread.html
つまりデフォルトではスレッドは1つ、CPUのI/O待ちの時のみ複数スレッドになる、とのこと。
意味はなくはないけど、Pumaの力を存分に発揮しているわけではないのです。
Ruby と JRuby
Rubyの処理系による違いによって、上記のようなスレッドの扱いの違いがあります。
処理系とは JRubyとかmrubyとか呼ばれているものたち。
https://qiita.com/takeyuweb/items/ea7b42746152f03efdaa
普段何も意識せずに使っているのはCRubyです。
ここでPumaのリポジトリを覗いてみましょう。
https://github.com/puma/puma
Since each request is served in a separate thread, truly concurrent Ruby implementations (JRuby, Rubinius) will use all available CPU cores.
JRubyやRubiniusのような処理系を使えば、PumaでCPUパワーを存分に引き出せるよって言ってます。
そこで今回はJRubyを使って検証してみることにしました。
JRubyは JavaVM上で動くRubyのことです。
JRubyはGVLの影響を受けずに、マルチスレッドができるのです。
パフォーマンス比較してみる
Pumaの力を引き出す方法がわかったところで
- Ruby + Unicorn
- JRuby + Puma
この2つでパフォーマンス比較してみます。
パフォーマンス計測には k6 というコマンドつかってみます。
https://qiita.com/szk3/items/c1172ef3d182d7fe6868
railsアプリケーションでページを表示させるだけです
class TopController < ApplicationController
def index
sample_array = []
input_txt = ""
100000.times do |i|
sample_array << i
end
sample_array.each do |i|
input_txt += i.to_s + "\n"
end
File.open( "./tmp/test/" + SecureRandom.hex(8) + ".txt", 'w') do |file|
file.puts(input_txt)
end
end
end
<h1>ほげ</h1>
結果発表!!
worker数: 2
スレッド数: 5
で共通です。
AWSインスタンス: c4.large
構成 | ec2 | vus | iterations | 結果 |
---|---|---|---|---|
cruby + unicorn | c4.large | 5 | 25 | avg=28.51s min=13.62s med=29.06s max=36.98s |
jruby + puma | c4.large | 5 | 25 | avg=37.94s min=35.87s med=37.93s max=40.14s |
JRubyのほうが遅くなってしまう結果になりました。
はっきり原因はわかりませんが、サーバ負荷(CPU使用率)はかなり高かったので、処理しきれない状態でもスレッド処理によってリクエスト数が増え逆に重くなった的な...?
AWSインスタンス: c5.xlarge
構成 | ec2 | vus | iterations | 結果 |
---|---|---|---|---|
cruby + unicorn | c5.xlarge | 5 | 25 | avg=18.19s min=7.14s med=16.99s max=25.12s |
jruby + puma | c5.xlarge | 5 | 25 | avg=18.12s min=15.76s med=18.28s max=20.15s |
JRuby + Pumaのパフォーマンスが結構よくなってきました。
ただこのときもサーバ負荷はかなり高くなっていたので、 c4.large でのテストと一緒の現象で遅くなってしまっていたかもしれません。
AWSインスタンス: c5.2xlarge
構成 | ec2 | vus | iterations | 結果 |
---|---|---|---|---|
cruby + unicorn | c5.xlarge | 5 | 25 | avg=17.83s min=7.03s med=16.44s max=24.67s |
jruby + puma | c5.xlarge | 5 | 25 | avg=9.86s min=8.43s med=9.56s max=11.43s |
cruby + puma | c5.xlarge | 5 | 25 | avg=35.34s min=33.7s med=34.35s max=37.49s |
だいぶJRuby + Pumaのパフォーマンスがあがってきました。
CPUパワーが結構キモになりそうです。
ここだけ CRuby + Pumaの構成のパフォーマンス確認をしてみましたが、パフォーマンスが結構悪くなってしまいました。
ただUnicornと比べてこんなに差がでるかな...?
おまけでworker数を4にして cruby + unicornで計測してみました
構成 | ec2 | vus | iterations | 結果 |
---|---|---|---|---|
cruby + unicorn | c5.xlarge | 5 | 25 | avg=9.96s min=7.12s med=8.52s max=16.55s |
だいぶ早くなりました。
パフォーマンス計測まとめ
ある程度負荷のある大量アクセスをさばく場面でPumaを使うのであれば、Rubyの処理系に関しても少し意識をする必要があるかなと思いました。
場合によってはUnicornを普通に使ったほうがパフォーマンスが良い場合があるので、注意です。