【実務・中級編】 軽量ロック (LWLock) – PostgreSQL

おい、ちょっといいか? 君がPostgreSQLの運用や開発に携わっているなら、そろそろ「見えないところで頑張ってる縁の下の力持ち」みたいな仕組みにも目を向けてほしいんだ。今日は、PostgreSQLのコアな部分を支える「軽量ロック (LWLock)」について話そう。

「ロック」と聞くと、デッドロックとかパフォーマンス劣化とか、ちょっと身構えちゃうかもしれないな。でもな、このLWLockは、PostgreSQLが共有メモリ内で効率的に動くための、本当に重要な土台なんだ。正直言って、これを理解しているかどうかで、君がPostgreSQLを「ただ使う人」から「深く理解して使いこなす人」にレベルアップできるかどうかが決まる、と言っても過言じゃない。

—

ぶっちゃけ、LWLockって何なの?

シンプルに言うと、LWLockはPostgreSQLの共有メモリ内にあるデータ構造へのアクセスを保護するための、めちゃくちゃ低オーバーヘッドなロック機構だよ。

PostgreSQLは複数のプロセス(バックエンドプロセス、WALライター、チェックポインターなど)が、同じ共有メモリ領域を読み書きして協調動作している。例えば、データブロックをキャッシュする共有バッファとか、WAL(Write-Ahead Log)バッファとか、ロック情報を管理する構造体とか、色々あるよな。

これらの共有データ構造に複数のプロセスが同時にアクセスしたら、どうなると思う? そう、データが破壊されたり、一貫性が失われたりする。それを防ぐためにロックが必要なんだけど、普通にOSのセマフォとかを使うと、コンテキストスイッチのオーバーヘッドが馬鹿にならないんだ。PostgreSQLは高頻度でこれらの共有データにアクセスするから、そこを極限まで軽量化したのがLWLockってわけだ。

「軽量」ってどういうこと? スピンロックとの違いは?

「軽量」のニュアンスは、主に以下の点にある。

1. 低オーバーヘッド: OSのシステムコールを介さずに、共有メモリ上のアトミック操作(CPU命令レベルでの不可分な操作)を駆使してロックの取得・解放を行う。
2. 待機処理の工夫:

  • スピンロック:ロックが取れないとき、CPUを消費しながらひたすら「ロック取れた?取れた?」ってループし続ける。短時間のロックには向いているけど、ロック保持時間が長くなるとCPUを無駄に食いつぶす。
  • LWLock:最初はスピンロックのように短時間スピンするんだけど、それでもロックが取れなかった場合、ちゃんと待機キューに入って、OSに「俺、今寝るわ」って伝えてCPUを解放するんだ。そして、ロックが解放されたときに、キューの先頭にいるプロセスが起こされてロックを取りに行く。

この「スピンし続けるのをやめて、CPUを他の仕事に譲る」っていうのがミソなんだ。これによって、ロック競合が発生してもCPUリソースを無駄にせず、効率的に処理を進められる。PostgreSQLの内部では、数え切れないほどの場所でこのLWLockが使われているんだよ。

—

LWLockの基本動作とモード

LWLockには、主に2つのモードがある。これは、一般的な排他ロックと共有ロックと同じ考え方だ。

1. 排他モード (Exclusive Mode):

  • これは「俺だけが使わせてもらうぜ!」っていうモード。
  • あるプロセスが排他モードでロックを取得したら、他のどのプロセスも、排他モードでも共有モードでも、そのロックを取得できない。
  • 主にデータ構造を書き換える時や、一貫性を完全に保証したい時に使う。

2. 共有モード (Shared Mode):

  • これは「みんなで一緒に読み込もうぜ!」っていうモード。
  • あるプロセスが共有モードでロックを取得しても、他のプロセスも共有モードならロックを取得できる。
  • ただし、排他モードのプロセスはロックを取得できない。
  • 主にデータ構造を読み込む時や、複数プロセスからの並行読み取りを許可したい時に使う。

プロセスは、必要なLWLockを「取得」し、用が済んだら「解放」する。この繰り返しによって、共有メモリ内のデータ構造の整合性が保たれているんだ。

—

LWLockが活躍する現場(具体的な使用例)

じゃあ、具体的にPostgreSQLのどこでLWLockが使われているのか、いくつか例を挙げよう。これを知っておくと、ボトルネック調査の時に「あ、ここのLWLockで詰まってるな」ってアタリをつけやすくなるからな。

1. バッファキャッシュ (Buffer Manager)

PostgreSQLのパフォーマンスの要、共有バッファ。ここにデータページを読み書きする際に、LWLockが多用されている。

  • `BufMappingLock`: どのデータページが共有バッファのどのスロットにロードされているかを管理するハッシュテーブルを守るロック。ページIDからバッファスロットのインデックスを検索する際に使われる。もしこれがなかったら、異なるページが同じスロットに入ったり、存在しないページを参照したりする。
  • `buffer_strategy_lock`: 共有バッファの置き換え戦略(LRUなど)を管理するリスト構造を守るロック。新しいページをバッファに読み込む際に、どのスロットを解放するか選ぶ時に使う。

2. WAL (Write-Ahead Log)

永続性とクラッシュリカバリの要、WAL。ここもLWLockの重要な利用箇所だ。

  • `WALInsertLock`: WALバッファにログレコードを書き込む際に、複数のプロセスが同時に書き込まないように保護するロック。これがなかったら、WALレコードがごちゃ混ぜになって、リカバリ不能な状態になる。重要なロックだ。
  • `WALWriteLock`: WALバッファの内容をディスク上のWALファイルに書き出す際に使うロック。

3. ロックマネージャ (Lock Manager)

トランザクションレベルのロック(テーブルロック、行ロックなど)を管理する構造体も、共有メモリ上にある。

  • `LockMgrLock`: トランザクションロックのハッシュテーブルなど、ロックマネージャの内部構造を守るロック。ロックの取得・解放の際に、この内部構造が同時に変更されないように保護する。

4. 共有カタログキャッシュ (Shared Catalog Cache)

システムカタログ(テーブル定義など)をキャッシュする領域も、共有メモリに存在する。

  • `SharedCacheLock`: 共有カタログキャッシュの読み書きを保護する。

他にもたくさんあるけど、これらは特に代表的なものだ。これらのLWLockが競合している場合、それはPostgreSQLの内部動作そのものがボトルネックになっている可能性が高い。

—

LWLockの実践的な見方:`pg_stat_activity` で待機イベントを追う

さて、ここまでLWLockの仕組みを説明してきたけど、実際の運用でどう役立つか、だ。一番実践的なのは、`pg_stat_activity` ビューを使って、LWLockによる待機イベントを特定することだ。

もしPostgreSQLのパフォーマンスが低下している場合、あるプロセスが特定のLWLockで長時間待機していることがある。それが分かれば、「なぜそのLWLockで待機が発生しているのか?」という次のステップに進めるわけだ。

以下のSQLクエリで、現在どのようなLWLockで待機が発生しているかを確認できる。

SELECT
pid,
datname,
usename,
client_addr,
application_name,
backend_type,
state,
wait_event_type,
wait_event,
query_start,
query
FROM
pg_stat_activity
WHERE
state = ‘waiting’
AND wait_event_type = ‘LWLock’
ORDER BY
query_start;

このクエリの結果を見てくれ。`wait_event_type` が `’LWLock’` で、`wait_event` に具体的なLWLockの名前が表示される。

例えば、こんな結果が出たら:

| pid | datname | usename | … | wait_event_type | wait_event | query_start | query |
| :– | :—— | :—— | :– | :————– | :————— | :———————- | :————————- |
| 123 | mydb | myuser | … | LWLock | buffer_mapping | 2023-10-27 10:00:01 | SELECT FROM large_table; |
| 456 | mydb | myuser | … | LWLock | wal_insert | 2023-10-27 10:00:05 | INSERT INTO log_table … |
| 789 | mydb | myuser | … | LWLock | lock_manager | 2023-10-27 10:00:10 | UPDATE accounts … |

  • `buffer_mapping` で待機しているなら、共有バッファのページマッピングに競合が発生している可能性がある。大量のテーブルスキャンやインデックススキャンが同時に走っているか、非常に多くのテーブルやインデックスに頻繁にアクセスしているか、といった状況が考えられる。
  • `wal_insert` で待機しているなら、WALの書き込み処理がボトルネックになっている。大量のDML操作(INSERT/UPDATE/DELETE)が同時に発生しているか、WAL書き込み先のディスクI/Oが遅いか、などが考えられる。
  • `lock_manager` で待機しているなら、トランザクションレベルのロック(テーブルロック、行ロック)の管理自体に競合が発生している。これは、非常に多くの同時トランザクションが、頻繁にロックを取得・解放している場合に起こりうる。

このように、`wait_event` の値から、PostgreSQLのどの内部コンポーネントがボトルネックになっているかを推測できるんだ。

チューニングのヒント

LWLockの競合がパフォーマンスボトルネックになっている場合、直接LWLockのパラメータをいじることはほとんどできない(というか、いじるべきじゃない)。LWLockはPostgreSQLの内部で最適なように設計されているからね。

代わりに、LWLockの競合を減らす方向でチューニングを考えるんだ。

  • `buffer_mapping` 競合:
  • `shared_buffers` のサイズを適切に調整する。
  • 不要なインデックスを削除したり、インデックスを効率化したりする。
  • クエリを最適化して、アクセスするデータ量を減らす。
  • `wal_insert` 競合:
  • `max_wal_size` や `checkpoint_timeout` を調整して、チェックポイントの頻度を最適化する。
  • WAL書き込み先のディスクI/O性能を改善する(SSD化など)。
  • 大量のDML操作をバッチ処理にまとめるなど、アプリケーション側でWALへの書き込み頻度を減らす工夫をする。
  • `lock_manager` 競合:
  • トランザクションの粒度を見直し、ロック保持時間を短くする。
  • デッドロックを避け、ロック取得の順番を統一する。
  • アプリケーション側の並行処理戦略を見直す。

—

まとめ

LWLockは、普段は意識しないかもしれないけど、PostgreSQLのパフォーマンスと安定性を陰で支える重要な仕組みだ。共有メモリ内のデータ構造を効率的かつ安全に保護するために、低オーバーヘッドかつスマートな待機処理を実現している。

LWLockの存在と、それがPostgreSQLのどこで使われているかを理解しておけば、パフォーマンス問題に直面したときに、`pg_stat_activity` の `wait_event` を見て、的確なボトルネック特定とチューニング方針の立案に繋げられるはずだ。

これで君も、一歩上のPostgreSQLエンジニアに近づいたな。見えないところで頑張ってる彼ら(LWLock)に感謝しつつ、これからもPostgreSQLを深く探求していこうぜ! 何か困ったら、いつでも相談に乗るからな。

コメント

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