【実務・中級編】 ロックマネージャのアーキテクチャ – PostgreSQL

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`と向き合ってみてくれ。何か行き詰まったら、いつでも相談してくれよ。現場からは以上だ!

コメント

タイトルとURLをコピーしました