Posted at

Pumaの本当の力を引き出す

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アプリケーションでページを表示させるだけです


app/controllers/top_controller.rb

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


app/views/top/index.erb

<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を普通に使ったほうがパフォーマンスが良い場合があるので、注意です。