Re: Configuring sql.DB for Better Performance
Configuring sql.DB for Better Performance という記事を知りました。 コネクションプールの大きさを制御する3つの設定を丁寧に解説されたとても良い記事です。
しかし、この記事で推奨されている設定については同意することができません。私が推奨する設定とその理由を解説していきたいと思います。
Limit ConnMaxLifetime instead of MaxIdleConns
Allowing just 1 idle connection to be retained and reused makes a massive difference to this particular benchmark — it cuts the average runtime by about 8 times and reduces memory usage by about 20 times. Going on to increase the size of the idle connection pool makes the performance even better, although the improvements are less pronounced.
この、 "to this particular benchmark" というのが問題です。このベンチマークでは、8並列で常にDBにクエリを投げ続けています。1つのクエリが終了するとすぐに次のクエリを投げるので、 DB.SetMaxIdleConns(1)
で大きな効果が現れました。
このベンチマークの動作は、例えばDBに大量のデータを挿入するバッチ処理などに当てはまりますが、Web アプリケーションなどには当てはまりません。
1秒間に1000回クエリを実行するアプリケーションを想定した簡単なシミュレータを書いてみました。クエリは一様分布でランダムなタイミングで実行され、各クエリと新規接続には10msかかるとします。 (このシミュレータのgist)
MaxOpenConns(20) の時、 MaxIdleConns(4) と MaxIdleConns(10) の動作を比べてみましょう。オレンジの線は総接続数、青い線は使用中の接続数、緑の線は接続が利用可能になるのを待っている時間の最大値をミリ秒で表しています。
1000回のクエリを実行するのに、 MaxIdleConns(4) だと 285 回接続していますが、 MaxIdleConns(10) だとそれを 69 回まで減らすことができています。一方で、負荷が止まった後もずっと維持し続ける接続も増えてしまっています。
今度は SetMaxIdleConns(100); SetConnMaxLifetime(time.MilliSecond * 300)
のシミュレーション結果を見てください。
20x4=80 回の接続をしています。 MaxIdleConns(10) のときの 69 回よりも多いですが、これは動作をわかりやすくするために lifetime を短く設定しているためです。シミュレーション時間を100秒に伸ばしたら、MaxIdleConns(10) の場合では接続回数はおよそ 690 回になり、 SetConnMaxLifetime(time.Second * 30) の場合の接続回数は 80 回になるでしょう。
このグラフで、再接続が特定のタイミングに集中し、そのタイミングでレイテンシが伸びてしまっているのが気になるかもしれません。これはシミュレーションが完全に一様分布になっていて、最初に全ての接続がほぼ同時に作られてしまっているからです。時間によって負荷が変動するアプリケーションでは、接続が作られるタイミングがもっと分散するので、このスパイクは発生しにくいはずです。次のグラフは、200msかけて段階的に負荷が増えた後に、上のグラフと同じ1000msの負荷がかかったときのものです。
SetConnMaxLifetime を使う他の理由
DB.SetConnMaxLifetime()
を提案し実装したのは私です。このAPIはアイドルな接続を減らす SetMaxIdleConns()
よりも良い方法ですが、それだけではありません。
"Configuring sql.DB for Better Performance" で紹介されたとおり、 MySQL では wait_timeout
という設定で接続がサーバーから切られる恐れがあります。また、OSやルーターが長時間利用されていないTCP接続を切断することもあります。どのケースでも、 go-sql-driver/mysql
はクエリを送信した後、レスポンスを受信しようとして初めてTCPが切断されたことを知ります。切断を検知するのに何十秒もかかるかもしれませんし、送信したクエリが実行されたかどうかを知ることもできないので安全なリトライもできません。
こういった危険をなるべく避けるためには、長時間使われていなかった接続を再利用せずに切断し、新しい接続を使うべきです。 SetConnMaxLifetime()
は接続の寿命を設定するAPIですが、寿命を10秒に設定しておけば、10秒使われていなかった接続を再利用することもありません。
接続の寿命を設定することで、他にも幾つかの問題に対処することができます。
- DBサーバーがロードバランスされているとき、サーバーの増減をしやすくする
- DBサーバーのフェイルオーバーをしやすくする
- MySQL でオンラインで設定変更したとき、古い設定で動作するコネクションが残り続けないようにする
接続のアイドル時間を制限するAPIを別に追加しなかったのは、現実的な環境における性能への影響と、 sql.DB の実装の複雑さを天秤にかけた結果です。
推奨する sql.DB の設定
- SetMaxOpenConns() は必ず設定する。負荷が高くなってDBの応答が遅くなったとき、新規接続してさらにクエリを投げないようにするため。できれば負荷試験をして最大のスループットを発揮する最低限のコネクション数を設定するのが良いが、負荷試験をできない場合も
max_connection
やコア数からある程度妥当な値を判断するべき。 - SetMaxIdleConns() は SetMaxOpenConns() 以上に設定する。アイドルな接続の解放は SetConnMaxLifetime に任せる。
- SetConnMaxLifetime() は最大接続数 × 1秒 程度に設定する。多くの環境で1秒に1回接続する程度の負荷は問題にならない。1時間以上に設定したい場合はインフラ/ネットワークエンジニアによく相談すること。
@methane