ElasticBeanstalkでのPuma, Rails, MySQLの接続など

ElasticBeanstalkで動いているRailsアプリでPumaのワーカー、スレッド、DBへの接続などの動きを把握した際の記録

## 環境
* ElasticBeanstalk Ruby2.7 Amazon Linux2

## Pumaの特徴

### マルチスレッドである
* スレッドプールを使ってリクエストを処理する
* 各リクエストは別々のスレッドで処理される
* 複数のリクエストに対して、あるスレッドがブロックされリクエスト処理中の場合、別のスレッドで新しいリクエストを並行処理(concurrency)できる

### クラスターモード
* マスタープロセスからworker（子プロセス）をforkするクラスターモードがある
* workerはそれぞれ独自のスレッドプールをもつ
* これにより並列処理(parallel)が可能になるが、forkはメモリ空間全体を複製するため、より多くのメモリを消費することになる
* アプリケーションの並行処理数(concurrency)は、最大で `threads` * `workers` となる
* 例えばスレッド数最小8,最大32,ワーカー数3で起動する場合
```bash
puma -t 8:32 -w 3
```
* Puma はトラフィックの量に応じて、最小値から最大値に達するまでスレッド数を自動的に調整する

### Puma.stats

Pumaには状態を出力する標準インターフェース[Puma.stats](https://github.com/puma/puma/blob/master/docs/stats.md)が用意されている。現在のワーカー数、スレッド数、処理待ちのリクエスト数(backlog)など。

## Railsにおける処理の流れ
![puma-flow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260824/7159f374-c7f7-d515-cc40-cfe201a97965.png)

* PumaはWebサーバとしての機能を兼ねることもできるが、通常の本番運用ではPumaはアプリケーションサーバとしての機能に特化させて、NginxやApache等のWebサーバを前段におくことが多い
* HTTP リクエストを web サーバーである NginxやApacheで受け取り、Nginx はそのリクエストを Puma に Socket 通信で渡し、Pumaはそのリクエストを Rackに渡し、最終的に Railsのrouterに渡される。

## Railsのデータベースコネクション
この辺の話。[Railsガイド - データベース接続をプールする](https://railsguides.jp/configuring.html#%E3%83%87%E3%83%BC%E3%82%BF%E3%83%99%E3%83%BC%E3%82%B9%E6%8E%A5%E7%B6%9A%E3%82%92%E3%83%97%E3%83%BC%E3%83%AB%E3%81%99%E3%82%8B)

### コネクションプール
Active Recordではコネクションプールが提供されている。リクエストを処理するときにDB接続・切断を毎回行うのではなく、処理が終わっても接続を保持しておき、以降のリクエストではコネクションプールから接続を使い回す。そのため、単純にリクエストが増えたからといってDBへの接続数は増えたりしない。

> The basic idea is that each thread checks out a database connection from the pool, uses that connection, and checks the connection back in. -[ActiveRecord::ConnectionAdapters::ConnectionPool](https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/ConnectionPool.html)

コネクションがすべて使われていて、さらに他のスレッドが接続を取得しようとした場合、他のスレッドが接続を返却するまで待ちが発生する。

> if all connections have been checked out, and a thread tries to checkout a connection anyway, then ConnectionPool will wait until some other thread has checked in a connection.

### コネクションプール数の設定
Railsは**ワーカー単位で**コネクションプールを保持する。database.yml内の`pool`はワーカーごとのプールが管理できる最大接続数を設定する。アプリケーションがマルチプロセスで動作している場合、**それぞれのプロセスで**デフォルトでは最大5つの接続を持つことになる。`RAILS_MAX_THREADS`が設定されていれば、ワーカーあたりのMAXのスレッド数 = ワーカーあたりの最大接続数となる。

```config/database.yml
default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  timeout: 5000
```

実際に適用されている値を`rails c`で確認したい場合
```
irb(main):002:0> Rails.application.config.database_configuration
```
コネクションプールの使用状況は[stat()](https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/ConnectionPool.html#method-i-stat)で確認できる
```
irb(main):002:0> ActiveRecord::Base.connection_pool.stat
=> {:size=>5, :connections=>0, :busy=>0, :dead=>0, :idle=>0, :waiting=>0, :checkout_timeout=>5.0}
```

利用可能な数を超えるコネクションを使おうとすると、Active Recordはコネクションをブロックし、プールのコネクションが空くのを待つ。コネクションを取得できない場合は`ActiveRecord::ConnectionTimeoutError`が発生する。

まとめると、DBの最大接続数を確認しておき

```sql
SHOW GLOBAL VARIABLES LIKE 'max_connections';
```
その上で以下を維持する必要がある

```
ワーカー数 * コネクションプール設定数 * インスタンス台数 <= DBの最大接続数
```

    
## ElasticBeanstalkでのPumaの設定値
### デフォルト設定
デフォルトではElasticBeanstalkが生成する`/opt/elasticbeanstalk/config/private/pumaconf.rb`が参照され、Railsのディレクトリ内の`config/puma.rb`は参照されない。[詳細はこちら参照](https://docs.aws.amazon.com/ja_jp/elasticbeanstalk/latest/dg/ruby-platform-procfile.html)

```bash
[ec2-user@ip-xxx-xx-xx-xx ~]$cd /opt/elasticbeanstalk/config/private/
[ec2-user@ip-xxx-xx-xx-xx private]$cat pumaconf.rb 
directory '/var/app/current'
threads 8, 32
workers %x(grep -c processor /proc/cpuinfo)
bind 'unix:///var/run/puma/my_app.sock'
stdout_redirect '/var/log/puma/puma.log', '/var/log/puma/puma.log', true
```
### ワーカー数を確認する
* ワーカー数はインスタンスのコア数とイコールになるよう設定されている
* 以下の通りlinuxにおいてCPU情報が格納されている`/proc/cpuinfo`を参照してワーカー数を決定している
* 私の環境ではコア数2なのでワーカーが2個起動している
![スクリーンショット 2022-06-11 17.31.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260824/49a7ca27-e2c4-3c63-ad40-8bec3275f624.png)
* 最大スレッド数は`2workers`×`32threads`で64threadsということになる

### ワーカー内のスレッド数を確認する
ワーカーごとのプロセスIDを確認し、それをもとに`procfs`を参照する
```bash
[ec2-user@ip-xx-x-x-xxx ~]$ps auxf | grep puma
webapp    5950  0.0  0.4 289928 33016 ?        Ss    6月10   0:09 puma 5.6.4 (unix:///var/run/puma/my_app.sock) [current]
webapp    6183  1.0  6.9 3301504 548528 ?      Sl    6月10  31:13  \_ puma: cluster worker 0: 5950 [current]
webapp    6184  1.0 13.5 3338368 1068240 ?     Sl    6月10  30:37  \_ puma: cluster worker 1: 5950 [current]
```

```bash
[ec2-user@ip-xx-x-x-xxx ~]$grep Threads /proc/6183/status
Threads:        17
```

psコマンドでスレッド情報をつけてみることもできる。`-L`をつければOK。LWP（Light Weight Process）がスレッドID、NLWP(Number of LWPs)がスレッド数なのでNLWPの数がプロセス内のスレッド数。（※`head -1`~の部分はpsの結果をgrepするとヘッダ行が入らなくて見づらいので入れてる）
```
ps auxww -L |head -1 && ps auxww -L |grep puma: |grep -v grep
```
![スクリーンショット_2022-06-12_15_54_28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260824/c90ed9b1-497b-30b1-db17-df627ba57b60.png)


## 参考
* [Deploying Rails Applications with the Puma Web Server](https://devcenter.heroku.com/articles/deploying-rails-applications-with-the-puma-web-server)（Herokuの例だけどわかりやすい）
* [Concurrency and Database Connections in Ruby with ActiveRecord](https://devcenter.heroku.com/articles/concurrency-and-database-connections#threaded-servers)
* [What is the difference between Workers and Threads in Puma](https://stackoverflow.com/questions/24280743/what-is-the-difference-between-workers-and-threads-in-puma)
* [How rails database connection pool works](https://stackoverflow.com/questions/34350406/how-rails-database-connection-pool-works)
* [Railsを動かして理解するDBコネクションプール](https://blog.serverworks.co.jp/tech/2020/01/23/rails_connection_pool/)
* [Nginx, Puma, Rails, MySQLのパラメータ設定/チューニング](https://qiita.com/walkers/items/80b170031cbba59550c5)
* [https://blog.kasei-san.com/entry/2017/05/21/162837](https://blog.kasei-san.com/entry/2017/05/21/162837)