Railsを動かして理解するDBコネクションプール

AWS運用自動化サービス「Cloud Automator」

データベースの文脈でコネクションプールという用語は昔から聞くのですが、私はあまり理解できていませんでした。
アプリケーションサーバの代表的な実装の一つであるRuby on Railsで実験し、理解を深めてみました。

検証環境の構築

今回は1台のEC2インスタンスを使います。
アプリ(Rails)とDB(MariaDB)は、同じEC2インスタンス(Amazon Linux 2)で動かします。

検証用Railsの準備

Railsのインストールまで

おそらく環境依存の話はないと思うのですが、今回は
Ruby on Rails 6をAmazon Linux 2で動かすの手順でインストールしました。

DB接続設定

Railsのデフォルトではsqlite3を使うようになっていますが、今回はMariaDBを使うようにします。

mysql2(gem)のインストール

Gemfileに以下を追記します。

config/database.yml

まずはこん感じの接続設定にしてみました。
poolは2と少なめにしてみました。

MariaDBの準備

MariaDBのインストール

DBのユーザー作成

DB作成

今回は丁寧に工夫して作った時の動作ではなく、「なんとなく普通に動かした時の動作」を知りたかったので、scaffoldで作ってみました。

テスト

rails server

この時点では、TCP:3306の接続は待ち受け用に1つ見えるだけでした。

なるほど、Railsガイド:データベース接続をプールするには、 最初はデータベース接続のプールは空で、必要に応じて追加接続が作成され、接続プールの上限に達するまで接続が追加されます。 と書いてありました。
アクセス無いと増えないんですね。

DBアクセスの発生するページにブラウザでアクセス

http://x.x.x.x:3333/employees/ にアクセスしてみます。
コントローラのコード(app/controllers/employees_controller.rb)は以下のようになっています。
Employee.all の部分でDBアクセスが発生します。

コンソールのログを見ると、SQL(SELECT文)が発行されているのがわかります。

TCP接続が2つ増えてますね。
1アクセスなのに2つな理由は、DB側(mysqld)とアプリ側(puma)で1つずつ必要だからです。

ブラウザのアクセスは即座にレスポンスがきましたが、その後もこのアプリとDB間の接続は残っています。
コネクションを1つプールしているということなんでしょう。

次にブラウザのリロードボタンを連打したり、他のブラウザや端末からアクセスしてみました。
結果、アプリとDBの接続は増えも減りもしませんでした。
これはリクエスト処理が瞬間的に完了してしまうためと思われます。

時間のかかる処理を試す

コントローラーの処理で時間がかる場合

30秒間sleepする処理を入れてみました。

ブラウザの複数タブから並列にリクエストを出してみると、確かに config/database.yml で指定したpool 2 が効いているようです。
2セットのTCP接続が上限となっています。

  • リクエスト1 成功
  • リクエスト2 成功
  • リクエスト3 成功
  • リクエスト4 all pooled connections were in use
  • リクエスト5 all pooled connections were in use
  • リクエスト6 all pooled connections were in use

エラーログ(コンソール)

エラー画面(ブラウザ)

poolが2しかないのに、3つ目のリクエストが成功したのが意外でした。
timeout: 5000などのなんらかのタイムアウト値が関係している可能性も考えられます。
詳細は理解できていないですが、pool数は同時アクセスに重要なようです。

ただ、実際のDBへのリクエストはsleepの前の @employees = Employee.all だけな気もするので、その後はDBコネクションを解放するかと予想したのですが、コントローラー全体の処理が終わるまで(ブラウザへレンダリングするまで?)維持する感じですね。

ここでpoolを5にしてみて同じアクセスをすると、全てのリクエストが成功し、下記のような接続になりました。

DBトランザクションで時間がかかる場合

先ほどはController内に単純にsleepを入れただけでしたが、少しだけ変えて、DBトランザクション内でsleepしてみました。

結果は以下の通りです。
なぜ3つ目が失敗し、4つ目が成功しているのかわからないのですが、最初のテスト結果とだいたい同じですね。

  • リクエスト1 成功
  • リクエスト2 成功
  • リクエスト3 all pooled connections were in use
  • リクエスト4 成功
  • リクエスト5 all pooled connections were in use
  • リクエスト6 all pooled connections were in use

まとめ

  • Railsは、DBへのリクエストにコネクションプールを高速に使い回すので、軽い処理なら少ないpool数でも同時リクエスト可能。
  • DBへのアクセスを必要とする処理は、概ねpool数だけ並列処理ができる。

色々試しましたが、結果的には、Railsガイド:データベース接続をプールするの内容を確認してみたという感じになりました。

細かいところはさておき、DBコネクションの基本は理解できました。

なお、私は普段はRailsエンジニアでもなんでも無いので、何か色々間違っていたらゴメンナサイ。

追記

弊社メンバーからコメントをいただきました。

なるほどですね〜。
コネクションプール作成のタイミングとSQL実行のタイミングについて、確認してみました。

Rails起動直後

sleep 30を入れた状態でRailsを起動しました。
前述したように、この時点ではTCP接続はありません。

ブラウザでアクセス直後

初回アクセス時のみ、 SELECT schema_migrations が実行されます。
これが何か調べてないのですが、2回目のアクセスからは発生しませんでした。
私がsleepを入れたindexメソッドとは別の場所にコードが記載されているものと思われます。
そしてこれが実行されたためだと思いますが、この時点でコネクションプールが1つ作成されます。

レンダリング時

長い処理(今回はsleepですが)の時間も経過し、ブラウザに表示される段階になって、 SELECT employees が実行されてました。

AWS運用自動化サービス「Cloud Automator」