PostgreSQLの「LWLock」と仲良くなる:共有メモリを守る最後の砦
PostgreSQLのパフォーマンスチューニングをしていると、必ずと言っていいほど直面するのが「LWLock(Lightweight Lock)」という単語です。
`pg_stat_activity` を眺めていて、やたらと `LWLock` で待機しているセッションが多い……そんなとき、君ならどうする? 「とりあえずリソース不足かな」とCPUやメモリを増やす前に、一度立ち止まって「LWLockが何をガードしているのか」に思いを馳せてみてほしい。
今日は、PostgreSQLの屋台骨を支えるこの仕組みについて、少し内側の世界を覗いてみよう。
—
LWLockって、結局なにもの?
一言で言えば、「共有メモリ上のデータ構造を守るための、超高速な門番」だ。
PostgreSQLはマルチプロセスアーキテクチャだよね。複数のバックエンドプロセスが同時に共有メモリ(Shared BufferやWALバッファなど)を読み書きする。ここで排他制御がないと、メモリの中身は一瞬で壊れてしまう。
そこで登場するのがLWLockなんだけど、ここで混同しがちなのが「スピンロック」との違いだ。
- スピンロック: 取得に失敗したら、ループして何度でも試し続ける(CPUを激しく消費する)。超短時間で終わる処理向け。
- LWLock: 取得に失敗したら、プロセスを「待機キュー」に入れて、眠りにつく(CPUを解放する)。スピンロックより少し時間がかかる処理向け。
要するに、「一瞬で済むならスピンロック、ちょっと時間がかかるかもしれないならLWLock」という住み分けだね。
LWLockが関わっている代表的な場所
現場のエンジニアとして知っておくべきなのは、LWLockがどこで使われているかだ。よく目にするのはこのあたり:
1. Buffer Content Lock: 共有バッファ上のデータページを読み書きするときに必要。ここが詰まると、クエリのレスポンスが全体的に低下する。
2. WAL Insert Lock: WAL(Write Ahead Log)を書き込むとき。高負荷な書き込み処理でここがボトルネックになることは珍しくない。
3. ProcArray Lock: トランザクションの状態を管理する場所。同時接続数が爆発的に増えると、ここが取り合いになる。
具体的な使用例(ソースコードの風景)
もし君がPostgreSQLの拡張モジュールを書いたり、コアのコードを読んだりするなら、こんな風に書かれているはずだ。
/ 排他モードでロックを取得 /
LWLockAcquire(MyLock, LW_EXCLUSIVE);
/ 共有メモリ上のデータをいじるクリティカルセクション /
do_something_with_shared_memory();
/ 解放 /
LWLockRelease(MyLock);
シンプルだろう? でも、現場で怖いのは「ロックの持ちすぎ」だ。ロックを取っている間に重い処理(ディスクI/Oやネットワーク通信など)を挟むと、他のプロセスが全員止まってしまう。
「ロックを取る範囲は最小限に」。これはメモリを扱うエンジニアの鉄則だよ。
「LWLock待ち」に遭遇したらどう動くか
`pg_stat_activity` で `wait_event` が `LWLock` になっているとき、君がまず確認すべきは「何が原因でロックが解放されていないのか」だ。
- Buffer Content Lockなら: 特定のテーブルに対するアクセスが集中していないか? インデックスは効いているか?(不要なデータページを読みすぎていないか?)
- WAL Insert Lockなら: WALの書き込みが追いついていない? ディスク性能がボトルネックかも。
- ProcArray Lockなら: 同時接続数が多すぎるかもしれない。コネクションプーラーの導入を検討すべき。
最後に:先輩からのアドバイス
LWLockは、PostgreSQLが「整合性を保ちながら並列で動く」ために避けては通れない仕組みだ。だから、完全に排除しようとする必要はない。
僕がいつも大事にしているのは、「何がボトルネックになっているかを正しく特定する」こと。ただ「ロック待ちが多いから性能が悪い」と切り捨てるのではなく、「なぜそのロックが必要で、どの処理がそれを握りしめているのか」という因果関係を想像できるようになると、一段上のエンジニアになれるはずだよ。
PostgreSQLは、中を知れば知るほど面白いデータベースだ。また何か詰まったら、いつでも聞きに来てくれ。一緒に深掘りしよう。
コメント