ロックの「沈黙」を聴く:PostgreSQLにおける競合とスループットの深淵
データベースのチューニングにおいて、インデックスの張り方やクエリの書き換えに頭を悩ませるエンジニアは多い。しかし、ある一定の規模を超えたシステムで、本当に我々を苦しめるのは「目に見えない鎖」、つまりロック競合だ。
PostgreSQLはMVCC(多版同時実行制御)という極めて洗練されたアーキテクチャを採用しているおかげで、読み取りと書き込みが互いにブロックすることはほとんどない。しかし、その「楽園」は、一度ロックの粒度を見誤ると一瞬で崩壊する。今日は、ロック競合が我々のシステムにどのような爪痕を残すのか、そしてそれをどう解き明かすのかについて、少し深い話をしよう。
「行レベル」という幻想と、その裏側にあるコスト
PostgreSQLの行レベルロック(Row-level locks)は非常に強力だ。`SELECT … FOR UPDATE` や `UPDATE` が実行されると、対象となる行のヘッダにある `t_infomask` フィールドにフラグが立ち、トランザクションID(XID)が刻まれる。
一見すると「特定の行だけをロックしているから安全」に見えるが、ここで考慮すべきは「ロック・テーブルのメモリ消費」だ。
- ロックの爆発: トランザクションが大量の行を更新する際、PostgreSQLはそれら全ての行にロックをかける。もしメモリ上のロック領域(`max_locks_per_transaction`)が溢れると、PostgreSQLはテーブル全体へのロック(ページレベルやテーブルレベル)へエスカレーションを試みる。
- 性能への影響: 多くの人が忘れがちだが、ロックの競合が起きると、バックエンドプロセスはスリープし、`LWLock`(Lightweight Lock)の待ち行列に並ぶ。この時、CPU使用率は一見下がって見えるが、実際にはシステム全体のスループットが極端に低下している「隠れた停滞」が発生している。
なぜ「ロック待ち」は複雑怪奇なのか
トラブルシューティングで最も厄介なのは、「どのクエリが原因か」を特定する瞬間だ。特に `Lock: ShareLock` や `ExclusiveLock` が絡むとき、原因は往々にしてアプリケーションのロジックの奥深くに隠れている。
最近の案件で遭遇したのは、あるバッチ処理がテーブル全体をスキャンする際、別のトランザクションがDDL(`ALTER TABLE`など)を発行し、それがデッドロックを引き起こしてシステムが凍りついた事例だった。
こうした事態を防ぐための私の定石を紹介しよう。
1. `pg_stat_activity` との対話
まず、ロック待ちが発生した瞬間に見るべきは、このビューだ。
SELECT pid, wait_event_type, wait_event, query
FROM pg_stat_activity
WHERE wait_event_type = ‘Lock’;
ここで重要なのは `wait_event` の値だ。`tuple` なのか `transactionid` なのか。これを見極めるだけで、問題が「特定の行の取り合い」なのか、「トランザクション完了待ち(あるいは準備待ち)」なのかを即座に判断できる。
2. `lock_timeout` の戦略的活用
本番環境で「永遠に待たされる」ことほど恐ろしいものはない。私は重要なバッチ処理や、競合が避けられないクエリに対しては、明示的に `SET lock_timeout = ‘5s’;` を指定するようにしている。失敗したときに自動リトライを入れる設計にしておけば、システム全体がロックの連鎖で共倒れするリスクを劇的に下げられる。
ロック粒度とクエリ実行時間の相関
ロックの粒度を細かくしすぎると、今度は管理コスト(オーバーヘッド)が無視できなくなる。逆に、粒度を粗くしすぎると並列性が損なわれる。この「トレードオフの境界線」を見極めることこそが、シニアエンジニアの腕の見せ所だ。
私の経験則として、以下の指針を提唱したい。
- Writeの頻度が高いテーブルでは、トランザクションを極限まで短く保つ: ネットワーク越しに重い処理を挟まないこと。DBのロックは「DBの中だけで完結させる」のが鉄則だ。
- インデックスを活用してロック範囲を限定する: 更新対象を特定するための検索条件がインデックスにヒットしていない場合、PostgreSQLは全件走査を行い、その過程で不要な行までロックしてしまうことがある。`EXPLAIN ANALYZE` で、無駄な行をロックしていないかを確認するのは、もはや義務だ。
最後に:ロックは「調整」であり「障害」ではない
ロックを悪者扱いしてはいけない。ロックは、データベースの整合性を守るための気高い盾だ。問題なのは、その盾の使い方が下手な我々自身である。
PostgreSQLの内部アーキテクチャを理解し、クエリがどのような粒度で世界を切り取っているのかを想像できるようになれば、ロック競合はトラブルではなく「チューニングのヒント」へと変わる。
もし今、あなたのデータベースが「重い」と感じているなら、まずはロックの統計情報を眺めてみてほしい。そこには、CPUやメモリのメトリクスには決して表れない、アプリケーションの「呼吸」が刻まれているはずだ。
さて、次はどのクエリを解剖しようか。また現場でお会いしよう。
コメント