Goのdatabase/sqlのコネクションプールの設定は必ず検討しよう

はじめに

はじめまして、ALTURA X株式会社でバックエンドエンジニアを担当している尾越です。
 
今回のテーマは、Goの標準ライブラリdatabase/sqlにおけるコネクションプールの設定です。
当社が運用していく中で実際に起こった事例2つについて、その原因、対処方法について書いていきたいと思います。

ケース1:Too many connections

DBにコネクションを張ろうとした時に、DB側でコネクション数が最大に達してしまい、コネクションを張ることができなかったことによるエラーです。
この原因を理解するために、まず基本的なdatabase/sqlの使用手順を確認します。
  1. Open()し、DBインスタンスを取得する
  1. 取得したDBインスタンスのQuery()Exec()にSQLを渡し、SQLを実行する
  1. 返却されたRowsResultを使い、後続処理を行う
ここで上記の2.では、コネクションを取得するconn()が呼ばれ、以下の手順でコネクションを取得します。(大まかな流れのみ表現しています。詳細はconn()を参照してください)
図1. コネクションの取得手順
図1. コネクションの取得手順
本ケースの原因は、2つ目の分岐「コネクション数が最大コネクション数未満か?」が必ずYesとなり、同時に大量の新規コネクションを作成しようとしたことになります。
この2つ目の分岐の「最大コネクション数」とは、DBインスタンスに設定されるもので、SetMaxOpenConns()で指定できるものです。そしてこの最大コネクション数、指定しない場合のデフォルト値は0、つまり無限であり、指定しなかった結果Too many connectionsが発生してしまいました。
よって本ケースの対処方法としては、SetMaxOpenConns()で、DB側の最大コネクション数に到達しないように設定することになります。
設定する上での注意点として、アプリケーション側で負荷に応じてコンテナを増やす等、オートスケーリングを設定している場合は、コンテナが複数ある場合を想定した設定値とする必要があります。

ケース2:接続試行回数が跳ね上がる

ある時、DBのモニタリング情報を確認していると、1分間当たりのDBの接続試行回数が数百単位で跳ね上がる瞬間があることに気づき、これに対処しました。
事象について調査すると、コネクションの作成と切断を一瞬の内に繰り返していることが分かりました。これはつまり、コネクションをうまく使いまわせていないということになります。
database/sqlにおいて、Query()Exec()でコネクションを使い終わると、putConn()が呼ばれ、その時点でのidleなコネクションの数が上限に達していればコネクションを切断し、そうでなければをidleにするという動きをします。(大まかな流れのみ表現しています。詳細はputConn()を参照してください)
図2. コネクションの解放手順
図2. コネクションの解放手順
ここで「idleなコネクションの数の上限」とは、DBインスタンスに設定されるもので、SetMaxIdleConns()で設定できるものです。そしてこのデフォルト値は2であり、idleなコネクションは2つまで存在できるということになります。
このデフォルト値で、複数のSQLを実行するようなgoroutineを100個同時に実行したらどうなるでしょうか。それぞれのgoroutineでコネクションの取得と解放を繰り返し、その過程で以下のようにidleなコネクションの数が最大値の2に達し、コネクションの切断が発生し、また切断した分新たにコネクションを作成する、といったことが発生します。これを繰り返した結果、接続試行回数が跳ね上がっていました。
  1. goroutine①がコネクション開放 → idleなコネクションの数が1に
  1. goroutine②がコネクション開放 → idleなコネクションの数が2に
  1. goroutine③がコネクション開放 → idleなコネクションの数が上限に達しているので、コネクションを切断
  1. goroutine①がコネクション取得 → idleなコネクションを使用 → idleなコネクションの数が1に
  1. goroutine②がコネクション取得 → idleなコネクションを使用 → idleなコネクションの数が0に
  1. goroutine③がコネクション取得 → idleなコネクションが無いので新規コネクション作成
本ケースの対処方法としては、まずSetMaxIdleConns()での設定値をケース1のSetMaxOpenConns()での設定値と同値にすることで、idleなコネクションの数が確実に上限を超えないようにしました。但しSetMaxIdleConns()を大きくするだけだと、コネクションを使わない時もidleとして保持し続けることになるのでリソースの浪費に繋がります。これについてはSetConnMaxIdleTime()でコネクションがidleで存在できる時間を制限することで対処しました。

最後に

Goの公式ドキュメントにManaging connectionsがあり、冒頭に以下が書かれています。
For the vast majority of programs, you needn’t adjust the  connection pool defaults. But for some advanced programs, you might need to tune the connection pool parameters or work with connections explicitly.
直訳:
ほとんどのプログラムでは、sql.DB 接続プールのデフォルトを調整する必要はありません。 ただし、一部の高度なプログラムでは、接続プールのパラメーターを調整したり、接続を明示的に操作したりする必要がある場合があります。
本記事で取り上げた事例に遭遇した身としては、これを読んで「ほとんどのプログラムでデフォルトを調整する必要があるのでは…」という感想を抱いてしまいました(個人の感想です)。
内容は注意点含め分かりやすく、かつ簡潔に書かれているので、database/sqlを使う方は一読することをお勧めします。
database/sqlを使う方が本記事を読み、コネクションプールを適切に設定するための一助となれば幸いです。