データベース接続のタイムアウト設定の重要性
アプリケーションを開発・運用する上で、データベース接続のタイムアウト設定は重要です。
多くのリレーショナルデータベースやJavaのJDBCドライバーは、デフォルト設定のままではタイムアウトが無制限、もしくは非常に長い時間待機してしまう設定になっています。
開発環境では問題なく動いていても、本番環境でデータ量が増加したり、インデックスの効かない非効率なクエリが発生したりすると、この長い待機時間が大きな問題を引き起こすことがあります。
異常に重い一つのSQLがタイムアウトせずに実行され続けると、
- CPUやメモリの過剰消費
- テーブルや行のロックの長期取得による他の正常なトランザクションのブロック
といった一つのプロセスの被害では済まない問題が発生し、アプリケーション全体のカスケード障害(連鎖的ダウン)を引き起こしてしまいます。
このような事態を防ぐためには、問題のある処理を特定の時間で強制的に打ち切り、リソースを即座に解放するフェイルセーフの仕組みが不可欠です。
タイムアウトはそういったフェイルセーフの仕組みを担います。
この記事ではPostgreSQLとJavaを用いた環境で、どのようなタイムアウトが設定でき、それぞれどんな用途に使えるのかを記載します。
設定箇所の色々
データベース側での設定
statement_timeout
設定内容
1つのSQLステートメントの実行にかかる最大許容時間を設定します。
用途
重い集計処理や、インデックスが効いていない非効率なクエリが長時間実行され、データベースのCPUやメモリリソースを過剰に消費し続けるのを防ぐ。
lock_timeout
設定内容
テーブルや行に対するロックの獲得を待機する最大時間を設定します。
用途
他のトランザクションが保持しているロックの解放を延々と待ってしまう状態を防ぐ。
idle_in_transaction_session_timeout
設定内容
トランザクションが開かれたまま、何も処理を行わずに待機(アイドル状態)している最大許容時間を設定します。
用途
アプリケーションのバグやネットワーク切断により、トランザクションが COMMIT も ROLLBACK もされずに放置される状態を防ぐ。
idle_session_timeout
設定内容
トランザクションの外で、単に何もしていない(アイドル状態の)コネクションが保持される最大時間を設定します。
用途
アプリケーションから切断されずに放置されたコネクションを強制終了させ、データベースのコネクションスロットが枯渇するのを防ぐ。
transaction_timeout
設定内容
トランザクション全体の最大許容時間を設定します。個別のクエリが短くても、BEGIN から COMMIT までの合計時間で評価されます。PostgreSQL 17から導入された新しいパラメータです。
用途
1つのトランザクション内で大量の短いSQLを発行したり、SQLの間にアプリケーション側の外部API呼び出しを挟んだりするような処理において、結果的に長引いてしまったトランザクション全体をタイムアウトさせる。
アプリケーション側での設定(JDBC)
データベースサーバー側ではなく、クライアントであるアプリケーション側(JDBCドライバー層)で制御されるタイムアウトです。
QueryTimeout
設定内容
クエリーの実行時間の最大値を設定します。
時間を超過すると、JDBCドライバーが裏側で新しいネットワーク接続を開き、PostgreSQLに対して「キャンセル要求(CancelRequest)」のシグナルを送信して処理を打ち切らせます。
用途
アプリケーション側のスレッド(Webサーバーのワーカースレッドなど)が、いつまでもDBの応答を待ち続けて枯渇するのを防ぐ。
connectTimeout
設定内容
データベースサーバーに対して、最初のTCP接続を確立する(コネクションを張る)までの最大待機時間を設定します。
用途
DBサーバー自体がダウンしていたり、ファイアウォールの設定ミスなどで通信が届かない場合に、アプリケーションが接続を延々と待機してしまうのを防ぐ。
socketTimeout
設定内容
確立されたネットワークコネクション上で、データの送受信(読み取り)を待機する最大時間を設定します。
用途
DB側から応答が全く返ってこなくなった場合に、強制的にTCP接続を切断してアプリケーションのスレッドを解放する。
通常、QueryTimeout よりも長い時間を設定します(例:クエリタイムアウトが10秒なら、ソケットタイムアウトは30秒など)。
cancelSignalTimeout
設定内容
QueryTimeout が発動してキャンセル要求(CancelRequest)を別接続で送信する際、その送信処理自体にかける最大待機時間を設定します。
用途
ネットワーク障害が起きている最中は、そもそも「キャンセル要求」を送ることすら詰まってしまう可能性があるため、その処理を切り上げるために使用する。
アプリケーション側での設定(Connection Pool)
コネクションプールを利用している場合は、そこにもタイムアウト設定ができます。
connectionTimeout
設定内容
アプリケーションがDBにアクセスしようとした際、プール内に空きコネクションがなく、他の処理が終わってコネクションが返却されるのを待機する最大時間を設定します。
用途
例えばトラフィックが急増してDBの同時接続数上限に達した際に、後続のリクエストを打ち切るために使用する。
maxLifetime / idleTimeout (コネクションの寿命管理)
設定内容
プール内に保持されているコネクションの最大生存時間(maxLifetime)や、使用されずに放置される最大時間(idleTimeout)を設定します。
用途
古くなったネットワーク接続は、途中のルーターやファイアウォールによって勝手に切断されるリスクが高まります。これらのタイムアウトを設定することで、定期的に新しい安全なコネクションに入れ替え、通信エラーを予防します。
アプリケーション側とデータベース側の両方で設定する必要性
タイムアウトを設定する際、「データベース側で設定しておけば、アプリケーション側は設定しなくてもいいのでは?」「逆にアプリ側で設定すれば十分では?」と考えるかもしれません。
しかし、本番環境において確実な障害対策を行うためには、アプリケーション側(クライアントサイド)とデータベース側(サーバーサイド)の両方でタイムアウトを設定する「多層防御(Defense in Depth)」が必須となります。
その理由は、通信を行う仕組みであるため、それぞれに「単独では防ぎきれないケース」が存在するからです。
-
アプリケーション側のみで設定した場合のケース
アプリケーション側のタイムアウトは、時間が来るとデータベースに対して「処理を止めて(CancelRequest)」というキャンセル通信を別途送信する仕組みです。
しかし、ネットワーク自体が断線していると、データベースには「止めて」という合図が届きません。
結果として、アプリ側はタイムアウトして先に進んでいるのに、データベース側では誰にも結果を返さない重いクエリが永遠に実行され続け、CPUや行ロックを食いつぶすという大事故に繋がります。 -
データベース側のみで設定した場合のケース
逆に、データベース側だけを設定していると、指定時間で確実にデータベースの処理は打ち切られます。
しかし、ここでも通信障害が起きていた場合、データベースが処理を終了したという「エラー応答」がアプリケーション側に届きません。すると、Javaのワーカースレッドはいつまでも来ない応答を無限に待ち続けてしまい最終的にシステム全体がダウンしてしまう可能性があります。
そのため、どちらか単独の設定で終わるのではなく、両者を組み合わせた設定を行う必要があります。
システム全体を落とさない堅牢なデータベースアクセスを実現するために、適切なタイムアウト設定を行いましょう。
おわり。