ElasticBeanstalkで動いているRailsアプリでPumaのワーカー、スレッド、DBへの接続などの動きを把握した際の記録
環境
- ElasticBeanstalk Ruby2.7 Amazon Linux2
Pumaの特徴
マルチスレッドである
- スレッドプールを使ってリクエストを処理する
- 各リクエストは別々のスレッドで処理される
- 複数のリクエストに対して、あるスレッドがブロックされリクエスト処理中の場合、別のスレッドで新しいリクエストを並行処理(concurrency)できる
クラスターモード
- マスタープロセスからworker(子プロセス)をforkするクラスターモードがある
- workerはそれぞれ独自のスレッドプールをもつ
- これにより並列処理(parallel)が可能になるが、forkはメモリ空間全体を複製するため、より多くのメモリを消費することになる
- アプリケーションの並行処理数(concurrency)は、最大で
threads
*workers
となる - 例えばスレッド数最小8,最大32,ワーカー数3で起動する場合
puma -t 8:32 -w 3
- Puma はトラフィックの量に応じて、最小値から最大値に達するまでスレッド数を自動的に調整する
Puma.stats
Pumaには状態を出力する標準インターフェースPuma.statsが用意されている。現在のワーカー数、スレッド数、処理待ちのリクエスト数(backlog)など。
Railsにおける処理の流れ
- PumaはWebサーバとしての機能を兼ねることもできるが、通常の本番運用ではPumaはアプリケーションサーバとしての機能に特化させて、NginxやApache等のWebサーバを前段におくことが多い
- HTTP リクエストを web サーバーである NginxやApacheで受け取り、Nginx はそのリクエストを Puma に Socket 通信で渡し、Pumaはそのリクエストを Rackに渡し、最終的に Railsのrouterに渡される。
Railsのデータベースコネクション
この辺の話。Railsガイド - データベース接続をプールする
コネクションプール
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
コネクションがすべて使われていて、さらに他のスレッドが接続を取得しようとした場合、他のスレッドが接続を返却するまで待ちが発生する。
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のスレッド数 = ワーカーあたりの最大接続数となる。
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()で確認できる
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の最大接続数を確認しておき
SHOW GLOBAL VARIABLES LIKE 'max_connections';
その上で以下を維持する必要がある
ワーカー数 * コネクションプール設定数 * インスタンス台数 <= DBの最大接続数
ElasticBeanstalkでのPumaの設定値
デフォルト設定
デフォルトではElasticBeanstalkが生成する/opt/elasticbeanstalk/config/private/pumaconf.rb
が参照され、Railsのディレクトリ内のconfig/puma.rb
は参照されない。詳細はこちら参照
[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個起動している
- 最大スレッド数は
2workers
×32threads
で64threadsということになる
ワーカー内のスレッド数を確認する
ワーカーごとのプロセスIDを確認し、それをもとにprocfs
を参照する
[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]
[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
参考
- Deploying Rails Applications with the Puma Web Server(Herokuの例だけどわかりやすい)
- Concurrency and Database Connections in Ruby with ActiveRecord
- What is the difference between Workers and Threads in Puma
- How rails database connection pool works
- Railsを動かして理解するDBコネクションプール
- Nginx, Puma, Rails, MySQLのパラメータ設定/チューニング
- https://blog.kasei-san.com/entry/2017/05/21/162837