Prismaのコネクションプール関連のパラメータについて確認してみた
リテールアプリ共創部@大阪の岩田です。
先日Prismaのコネクションプール周りのパラメータについて調査する機会がありました。せっかくなので調査した内容についてご紹介します。以後の内容は全てMySQL利用の前提となります。
環境
今回利用したPrismaのバージョンは以下の通りです。
- clientVersion: 5.15.0
- engineVersion: 12e25d8d06f6ea5a0252864dd9a03b1bb51f3022
Prismaでコネクションプール関連の設定を調整するには?
Prismaでコネクションプール関連の設定を調節する場合は接続文字列にパラメータを指定します。イメージはこんな感じです。
'mysql://prisma:[email protected]:3306/mysql?<パラメータ1>=<パラメータ1の値>&<パラメータ2>=<パラメータ2の値>'
大抵の場合は環境変数DATABASE_URL
を使って設定していると思います。
さっそく具体的なパラメータ値について見ていきましょう。
connection_limit
このパラメータはコネクションプールのサイズ上限を指定します。デフォルト値は物理CPU数×2 + 1
です。
デフォルト値を計算しているのはこの辺です。
プール内のコネクション数がconnection_limitに達している状態でさらにクライアントからコネクションのチェックアウトがリクエストされると、対象のリクエストはブロックされることになります。
コンビニやスーパーのレジでイメージするとこんな感じです。
余談ですが、今回の調査のきっかけはあるシステムに対して実施した負荷テストです。レスポンスタイムの悪化や500エラーの発生が見られたもののDBセッションはわずかしか消費していなかったため、コネクションプール周りの設定が怪しそうだと考えました。そのシステムではconnection_limit
が特に設定されておらずデフォルト値が採用していたため、重めのSQLが実行されるとすぐに処理が詰まっていたようです。
以下のコードを実行して簡単に検証してみましょう。Promise.allSettled
を使って1秒SleepするSQLの発行を5並行で実行するような処理になります。connection_limit
には5を指定しているため、コネクションプールのサイズ上限は5です。
const prisma = require('@prisma/client');
const client = new prisma.PrismaClient();
process.env.DATABASE_URL='mysql://prisma:[email protected]:3306/mysql?connection_limit=5'
const query = async () => {
return client.$queryRaw`SELECT SLEEP(1)`
.then(()=> console.log((new Date()).toISOString()))
}
Promise.allSettled([
query(),
query(),
query(),
query(),
query(),
]).then(console.log)
実行結果は以下の通りです。
2024-11-12T11:16:25.974Z
2024-11-12T11:16:25.986Z
2024-11-12T11:16:25.987Z
2024-11-12T11:16:25.987Z
2024-11-12T11:16:25.987Z
[
{ status: 'fulfilled', value: undefined },
{ status: 'fulfilled', value: undefined },
{ status: 'fulfilled', value: undefined },
{ status: 'fulfilled', value: undefined },
{ status: 'fulfilled', value: undefined }
]
5つのクエリがほぼ同時に完了していることがわかります。
該当時間帯のinformation_schema.processlist;
の出力は以下の通りでした。
mysql> select id,user,info from information_schema.processlist;
+----+-----------------+---------------------------------------------------------+
| id | user | info |
+----+-----------------+---------------------------------------------------------+
| 8 | root | select id,user,info from information_schema.processlist |
| 16 | prisma | SELECT SLEEP(1) |
| 17 | prisma | SELECT SLEEP(1) |
| 5 | event_scheduler | NULL |
| 13 | prisma | SELECT SLEEP(1) |
| 14 | prisma | SELECT SLEEP(1) |
| 15 | prisma | SELECT SLEEP(1) |
+----+-----------------+---------------------------------------------------------+
7 rows in set (0.00 sec)
PrismaのコネクションプールからDBに対して5つのセッションが張られていることがわかります。
今度は接続文字列を指定している箇所のconnection_limit
を1に書き換えて再実行してみましょう。
出力は以下の通りでした。
2024-11-12T11:19:53.884Z
2024-11-12T11:19:54.890Z
2024-11-12T11:19:55.895Z
2024-11-12T11:19:56.899Z
2024-11-12T11:19:57.904Z
[
{ status: 'fulfilled', value: undefined },
{ status: 'fulfilled', value: undefined },
{ status: 'fulfilled', value: undefined },
{ status: 'fulfilled', value: undefined },
{ status: 'fulfilled', value: undefined }
]
今度は約1秒ごとに日時が出力されていることがわかります。コネクションプールのサイズが小さいためうまく並行にDBアクセスできず、直列実行のような状態になっていることが分かります。
information_schema.processlist
の確認結果は以下の通りでした。
mysql> select id,user,info from information_schema.processlist;
+----+-----------------+---------------------------------------------------------+
| id | user | info |
+----+-----------------+---------------------------------------------------------+
| 8 | root | select id,user,info from information_schema.processlist |
| 18 | prisma | SELECT SLEEP(1) |
| 5 | event_scheduler | NULL |
+----+-----------------+---------------------------------------------------------+
3 rows in set (0.00 sec)
mysql> select id,user,info from information_schema.processlist;
+----+-----------------+---------------------------------------------------------+
| id | user | info |
+----+-----------------+---------------------------------------------------------+
| 8 | root | select id,user,info from information_schema.processlist |
| 18 | prisma | SELECT SLEEP(1) |
| 5 | event_scheduler | NULL |
+----+-----------------+---------------------------------------------------------+
3 rows in set (0.00 sec)
mysql> select id,user,info from information_schema.processlist;
+----+-----------------+---------------------------------------------------------+
| id | user | info |
+----+-----------------+---------------------------------------------------------+
| 8 | root | select id,user,info from information_schema.processlist |
| 18 | prisma | SELECT SLEEP(1) |
| 5 | event_scheduler | NULL |
+----+-----------------+---------------------------------------------------------+
3 rows in set (0.01 sec)
PrismaのコネクションプールからDBに対して1つのセッションしか張られていないことがわかります。
pool_timeout
このパラメータはコネクションプールからコネクションをチェックアウトする際の待ち時間上限を指定します。クライアントからconnection_limit
を超える接続要求があった場合、対象のリクエストはコネクションが利用可能になるまでブロックされるのですが、この値を超えるとあきらめてErrorがthrowされます。
イメージはこんな感じ
デフォルト値は10
となっており、以下の箇所で指定されています。
検証用コードの接続文字列にpool_timeout=1
を追加してテストしてみましょう。結果は以下のようになります。
node test
2024-11-12T11:49:48.630Z
[
{
status: 'rejected',
reason: PrismaClientKnownRequestError:
Invalid `prisma.$queryRaw()` invocation:
Timed out fetching a new connection from the connection pool. More info: http://pris.ly/d/connection-pool (Current connection pool timeout: 1, connection limit: 1)
at In.handleRequestError (/Users/...略/node_modules/@prisma/client/runtime/library.js:122:6877)
at In.handleAndLogRequestError (/Users/...略/node_modules/@prisma/client/runtime/library.js:122:6211)
at In.request (/Users/...略/node_modules/@prisma/client/runtime/library.js:122:5919)
at async l (/Users/...略/node_modules/@prisma/client/runtime/library.js:127:11167)
at async Promise.allSettled (index 1) {
code: 'P2024',
clientVersion: '5.15.0',
meta: [Object]
}
...略
5つ並行で呼び出したクエリのうち1つだけが成功し、残り4つはTimed out fetching a new connection from the connection pool
というエラーで失敗しています。
ドキュメントに記載されていないパラメータ
ここまではPrismaのドキュメントに記載のあるパラメータなのですが、実は記事執筆時点ではドキュメントに記載されていないパラメータも存在します。これらのパラメータについては以下のissueでも言及されています。
Prisma Enginesは内部的にmobcを利用しているのですが、Prismaの接続文字列に渡されたオプションは最終的にmobcのコネクションプールを作成する際のパラメータとして利用されます。MySQLの場合は以下の箇所で接続文字列がパースされ...
パースされたパラメータを使って以下の箇所でmobcのコネクションプールがビルドされます
ということで以後はドキュメントに記載されていないパラメータについて検証してみます。
max_idle_connection_lifetime
このパラメータはプール内でアイドル状態になっているコネクションの生存期間を設定するパラメータで、指定した値がmobcのmax_idle_lifetimeに設定されます。イメージはこんな感じ。
mobcのソースコードのコメントによると生存期間を過ぎた過ぎたコネクションは即座にクローズされるわけではなく、クローズ処理は遅延実行されるようです。
NodeのインタプリタからPrismaClientを利用してクエリを発行しながら動作確認してみましょう。
まずインタプリタから以下を入力します。max_idle_connection_lifetime
には1を指定しています。
const prisma = require('@prisma/client');
const client = new prisma.PrismaClient();
process.env.DATABASE_URL='mysql://prisma:[email protected]:3306/mysql?max_idle_connection_lifetime=1'
const query = async () => {
return client.$queryRaw`SELECT SLEEP(1)`
.then(()=> console.log((new Date()).toISOString()))
}
この状態でinformation_schema.processlist
を確認すると以下の通りでした。まだPrismaClientからの接続は存在しません。
mysql> select now(),id,user,info from information_schema.processlist where user = 'prisma';
Empty set (0.00 sec)
Nodeのインタプリタからquery()
を実行してから再度information_schema.processlist
を確認してみましょう。
mysql> select now(),id,user,info from information_schema.processlist where user = 'prisma';
+---------------------+----+--------+------+
| now() | id | user | info |
+---------------------+----+--------+------+
| 2024-11-13 02:33:24 | 14 | prisma | NULL |
+---------------------+----+--------+------+
1 row in set (0.00 sec)
id=14でPrismaClient向けの接続がオープンしています。
数秒経過後に再度確認してみます。
mysql> select now(),id,user,info from information_schema.processlist where user = 'prisma';
+---------------------+----+--------+------+
| now() | id | user | info |
+---------------------+----+--------+------+
| 2024-11-13 02:33:49 | 14 | prisma | NULL |
+---------------------+----+--------+------+
max_idle_connection_lifetime
で指定した秒数経過後ですが、相変わらずid=14の接続がオープンしている状態です。
再度query()
を実行してから確認します。
mysql> select now(),id,user,info from information_schema.processlist where user = 'prisma';
+---------------------+----+--------+------+
| now() | id | user | info |
+---------------------+----+--------+------+
| 2024-11-13 02:34:08 | 15 | prisma | NULL |
+---------------------+----+--------+------+
1 row in set (0.00 sec)
今度はidが15となっており、先程までとは別の接続がオープンしていることが分かります。query()
実行時の裏でプール内のコネクションのクローズと新規オープンが動いていることが分かります。
max_connection_lifetime
このパラメータはプール内のコネクションの生存期間を設定するパラメータで、指定した値がmobcのmax_lifetime
に設定されます。max_idle_connection_lifetime
との違いは対象のコネクションが非アイドル状態であってもクローズ対象となることです。
イメージはこんな感じ。
こちらのパラメータについても以下のコードで動作確認してみましょう。
const prisma = require('@prisma/client');
const client = new prisma.PrismaClient();
process.env.DATABASE_URL='mysql://prisma:[email protected]:3306/mysql?max_idle_connection_lifetime=15&max_connection_lifetime=50'
const query = async () => {
return client.$queryRaw`SELECT SLEEP(1)`
.then(()=> console.log((new Date()).toISOString()))
}
setInterval(query, 10000)
max_idle_connection_lifetime
には15を設定していますが、setInterval
を使って10秒毎にクエリが発行されるためアイドル時間は約10秒となり、15秒以上アイドル状態になることはありません。このコードを実行しながらinformation_schema.processlist
を確認してみましょう。
mysql> select now(),id,user,info from information_schema.processlist where user = 'prisma';
+---------------------+----+--------+------+
| now() | id | user | info |
+---------------------+----+--------+------+
| 2024-11-13 02:46:59 | 16 | prisma | NULL |
+---------------------+----+--------+------+
1 row in set (0.00 sec)
まずid=16の接続がオープンしました。
mysql> select now(),id,user,info from information_schema.processlist where user = 'prisma';
+---------------------+----+--------+------+
| now() | id | user | info |
+---------------------+----+--------+------+
| 2024-11-13 02:47:40 | 16 | prisma | NULL |
+---------------------+----+--------+------+
1 row in set (0.00 sec)
約40秒後の状況です。max_connection_lifetime
で指定された50秒は経過していないため、id=16の接続がオープンし続けています。
mysql> select now(),id,user,info from information_schema.processlist where user = 'prisma';
+---------------------+----+--------+------+
| now() | id | user | info |
+---------------------+----+--------+------+
| 2024-11-13 02:47:58 | 17 | prisma | NULL |
+---------------------+----+--------+------+
1 row in set (0.01 sec)
約1分後の状況です。max_connection_lifetime
で指定された期間を経過しているためid=16の接続はクローズされ、id=17の接続がオープンしていることが分かります。
まとめ
ドキュメントに記載されていないパラメータ値についてもmobcのコード等を参考に動作確認してみました。今回紹介したパラメータ以外にも接続文字列で色々なパラメータが指定できるので、興味がある方はPrisma Enginesのソースコードを覗いてみるのも楽しいと思います。