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

この２つでパフォーマンス比較してみます。
パフォーマンス計測には 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<br>min=13.62s<br>med=29.06s<br>max=36.98s
|jruby + puma|c4.large|5|25|avg=37.94s<br>min=35.87s<br>med=37.93s<br>max=40.14s<br>


JRubyのほうが遅くなってしまう結果になりました。
はっきり原因はわかりませんが、サーバ負荷（CPU使用率）はかなり高かったので、処理しきれない状態でもスレッド処理によってリクエスト数が増え逆に重くなった的な...？

### AWSインスタンス: c5.xlarge

|構成|ec2|vus|iterations|結果|
|---|---|---|---|---|
|cruby + unicorn|c5.xlarge|5|25|avg=18.19s<br>min=7.14s<br>med=16.99s<br>max=25.12s
|jruby + puma|c5.xlarge|5|25|avg=18.12s<br>min=15.76s<br>med=18.28s<br>max=20.15s

JRuby + Pumaのパフォーマンスが結構よくなってきました。
ただこのときもサーバ負荷はかなり高くなっていたので、 c4.large でのテストと一緒の現象で遅くなってしまっていたかもしれません。

### AWSインスタンス: c5.2xlarge

|構成|ec2|vus|iterations|結果|
|---|---|---|---|---|
|cruby + unicorn|c5.xlarge|5|25|avg=17.83s<br>min=7.03s<br>med=16.44s<br>max=24.67s  
|jruby + puma|c5.xlarge|5|25|avg=9.86s<br>min=8.43s<br>med=9.56s<br>max=11.43s
|cruby + puma|c5.xlarge|5|25|avg=35.34s<br>min=33.7s<br>med=34.35s<br>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<br>min=7.12s<br>med=8.52s<br>max=16.55s

だいぶ早くなりました。

## パフォーマンス計測まとめ

ある程度負荷のある大量アクセスをさばく場面でPumaを使うのであれば、Rubyの処理系に関しても少し意識をする必要があるかなと思いました。
場合によってはUnicornを普通に使ったほうがパフォーマンスが良い場合があるので、注意です。