PostgreSQLの「ロック」でハマらないために。ロックマネージャの裏側を覗いてみよう
現場でPostgreSQLを触っていると、たまに「なぜかクエリがブロックされる」「デッドロックが発生した」という壁にぶつかるよね。そんな時、`pg_stat_activity`を見て溜息をつく前に、PostgreSQLが裏でどうやってロックを管理しているのかを知っておくと、トラブルシューティングの景色がガラリと変わるはずだ。
今日は、PostgreSQLの心臓部の一つ、「ロックマネージャ」のアーキテクチャについて、少しだけ深掘りしてみようか。
—
そもそも「ロック」は何のためにあるのか?
データベースは複数のプロセスが同時に動く場所だ。みんなが同じ行を書き換えようとしたら、データは一瞬で壊れてしまう。それを防ぐための「交通整理」がロックの役割だね。
PostgreSQLのロックマネージャは、共有メモリ(Shared Memory)上に鎮座している。すべてのプロセスが参照できる巨大なハッシュテーブルをイメージしてほしい。これが「ロックテーブル」だ。
1. 「重量級ロック」と「LWLock」:役割分担の妙
PostgreSQLのロックには大きく分けて2種類ある。ここを混同していると、トラブルの原因が見えなくなるから注意が必要だよ。
- 重量級ロック(Heavyweight Locks / LockManager)
- SQLレベルで意識するもの。`SELECT FOR UPDATE`や`LOCK TABLE`などで発生する。
- デッドロックの検出機能がある。
- `pg_locks`ビューで確認できるのがこれだね。
- LWLock(Lightweight Locks)
- DB内部の共有データ構造(バッファプールやWALバッファなど)を守るための、いわば「裏方の門番」。
- 非常に高速だけど、デッドロック検出のような気の利いた機能はない。
- 基本的にはデータベースのコア開発者や、かなり深いパフォーマンスチューニングをする人以外は、あまり気にしなくていい領域だ。
先輩からのアドバイス:
「なんかDBが重いな?」と思った時、`pg_stat_activity`を見て `wait_event_type` が `Lock` になっていたら重量級ロック、`LWLock` になっていたら内部データ構造の競合を疑う。まずはこの切り分けが鉄則だ。
—
ロックテーブルの仕組み:ハッシュ構造の恩恵
重量級ロックは、共有メモリ上のハッシュテーブルで管理されている。このハッシュテーブルには、どのトランザクションが、どのオブジェクト(テーブルや行)に対して、どのモードのロックを持っているかという情報が詰まっているんだ。
例えば、あるトランザクションがテーブルを更新する時、PostgreSQLは以下の手順を踏む。
1. ロックの要求: プロセスがロックマネージャに「このOIDのテーブルを排他ロックしたい」と申請する。
2. ハッシュ値の計算: オブジェクトIDからハッシュ値を計算し、ロックテーブルの該当バケットにアクセスする。
3. 競合チェック: 既に他のプロセスが互換性のないロックを持っていれば、自プロセスを「待機状態」にし、プロセスリストに自分を登録する。
4. ロックの付与: 競合がなくなれば、ロックを獲得して処理を進める。
コードレベルで見ると、`src/backend/storage/lmgr/lock.c` あたりがこのロジックの拠点だ。興味があるなら、ぜひソースコードの海に潜ってみてほしい。
—
実践的なトラブルシューティング:`pg_locks`の読み方
現場でロック待ちが発生したとき、僕はよくこんなクエリを叩く。
SELECT
a.pid,
a.usename,
a.query,
l.locktype,
l.mode,
l.granted
FROM pg_stat_activity a
JOIN pg_locks l ON a.pid = l.pid
WHERE NOT l.granted;
`granted = false` の行が、「今まさにロックを待ち続けているプロセス」だ。これを見れば、誰がどのテーブルをロックしていて、誰が待たされているのかが一目瞭然になる。
よくある「やらかし」例:
- 長すぎるトランザクション: 明示的に `BEGIN` してから、外部APIを叩いたりして処理を中断させてしまう。その間、行ロックが解放されず、後続の更新処理が全滅するパターン。
- DDLの実行: `ALTER TABLE` などは強力な排他ロックを要求する。実行中のクエリが多いタイミングでこれを行うと、一瞬でロック待ちの行列ができる。
—
最後に:ロックは「最小限」に
僕が後輩にいつも言っているのは、「ロックを避けるデザインこそが最高」だということだ。
- トランザクションは可能な限り短く保つ。
- バルク更新は小分けにする。
- 読み取り専用なら、なるべく `SELECT` だけで済ませる。
ロックマネージャは非常に優秀だけど、魔法ではない。競合が発生すれば、どんなに速いストレージを使っていてもデータベースは止まってしまうんだ。
アーキテクチャの奥底を知ることは、単なる知識の蓄積じゃない。それは「なぜシステムが止まったのか」という問いに対して、自信を持って答えを出せるようになるためのパスポートだ。
もし今のプロジェクトでロックに悩まされているなら、まずは今日紹介した`pg_locks`と向き合ってみてくれ。何か行き詰まったら、いつでも相談してくれよ。現場からは以上だ!
コメント