これは自宅で運用している おうちKubernetesを約3年運用してみた で紹介したクラスターにおける事象とその復旧記録です。
おうちKubernetesでは、PostgreSQLのプライマリとリードレプリカの運用を効率化できる CloudNativePG を採用しています。
商用環境ではなく、ダウンタイムを許容した環境での対応ですので、参考にされる際はご留意ください。
Summary
-
CloudNativePG クラスタ上の複数アプリで
pq: invalid page in block 0 of relation global/2697 (XX001)エラーが多発し、突然アプリの動作が停止した -
pg_filenode_relation()で破損オブジェクトがヒープではなくインデックス(pg_tablespace_oid_index)であることを特定し、REINDEXのみでデータロスなく復旧できた -
共有カタログ(
pg_globalテーブルスペース)の破損は全データベース横断で波及するため、一部アプリが無事でも「問題なし」と即断してはならない
やらないこと
-
根本原因(ストレージ障害・ノード異常終了等)の詳細な特定手順(本記事では発生事象・調査・復旧の流れに集中する)
-
CloudNativePG のインストール・クラスタ構築手順の解説
-
PostgreSQL のページ管理やバッファキャッシュのアーキテクチャ詳解
本記事における課題
発生事象
CloudNativePG クラスタに接続するアプリケーションから、ある日突然、以下のエラーが報告された。
pq: invalid page in block 0 of relation global/2697 (XX001)
エラーコード XX001 は PostgreSQL の data_corrupted に分類され、ページヘッダの整合性チェック失敗を意味する。
発生当初は特定のアプリだけが影響を受けているように見えたため、アプリ固有の設定差異や軽微な接続エラーと誤認しかねない状況だった。しかし調査を進めると、これはクラスタ全体に影響しうる 共有カタログの破損 であることが判明した。
共有カタログ破損が「全体に波及する」理由
PostgreSQL の pg_global テーブルスペース(global/ ディレクトリ配下)には、全データベースから共有参照されるカタログオブジェクトが格納されている。具体的には pg_tablespace、pg_authid、pg_database、pg_auth_members 等がここに属しており、いずれもクラスタ内の全データベースから参照される。
今回破損した global/2697 は pg_tablespace_oid_index に相当し、このインデックスは全データベースから参照される。そのため、たまたま当該ブロックを参照するクエリを実行したアプリではエラーが顕在化し、そうでないアプリでは表面上エラーが出なかった、と判断した。
やったこと
破損オブジェクトの種別特定(ヒープ vs インデックス)
まず最初に確認すべきは、破損しているのがテーブル本体(ヒープ)なのか、インデックスなのか である。この判別で対応方針とデータロスの有無が大きく変わる。
| 破損対象 | 対応方針 | データロス |
|---|---|---|
| インデックス |
REINDEX で再構築 |
なし |
| ヒープ(テーブル本体) | バックアップからのリストア等、より深刻な措置が必要 | あり得る |
Primary Pod に psql で接続し、エラーメッセージ中の filenode(2697)から実オブジェクトを特定した。
-- filenode から対象オブジェクトを特定する
-- 第1引数は pg_global テーブルスペースの OID (1664)
SELECT pg_filenode_relation(1664, 2697);
-- 結果: pg_tablespace_oid_index
続いて pg_class でオブジェクト種別を確認する。
SELECT relname, relkind, relnamespace::regnamespace
FROM pg_class
WHERE oid = pg_filenode_relation(1664, 2697);
relname | relkind | relnamespace
------------------------+---------+--------------
pg_tablespace_oid_index| i | pg_catalog
relkind = i(index)を確認。ヒープではなくインデックスのみの破損 であり、REINDEX によるデータロスなしの復旧が可能と判定できた。
pg_filenode_relation(tablespace_oid, filenode) は、ファイルシステム上の filenode 番号から論理オブジェクトを逆引きできる PostgreSQL 組み込み関数である。
pg_global テーブルスペースの OID は 1664 で固定されている。
pg_authid_rolname_index も予防的に REINDEX を実施
今回は pg_tablespace_oid_index の破損が顕在化したが、pg_authid_rolname_index に対しても確認なしに予防的 REINDEX を実施した。
pg_global ディレクトリには pg_tablespace と pg_authid の両テーブルおよびそれらのインデックスが物理的に隣接して配置されている。ストレージ障害やノードの異常終了など物理的な要因でページ破損が発生した場合、同一ストレージ領域上に隣接するファイルが連鎖的に破損している可能性がある。
さらに、市中の事例 では global/2676 や global/2697 に類似した共有カタログインデックスの複数同時破損が報告されており、1 つの破損が確認された場合は隣接するオブジェクトも同様に疑うことにした。
バックアップも取得済みでダウンタイムも許容できる状況であったため、個別の破損確認よりも 予防的に REINDEX を実施した。インデックスの REINDEX はデータロスを引き起こさないため、不確かなら実施するというアプローチをとった。
REINDEX による復旧
破損インデックスおよびリスクのある隣接インデックスに対して REINDEX を実行した。
REINDEX INDEX pg_catalog.pg_authid_rolname_index;
REINDEX INDEX pg_tablespace_oid_index;
いずれも正常終了。その後、Primary への接続試験でエラーが消滅したことを確認した。
REINDEX INDEX は対象インデックスに AccessExclusiveLock を取得する。本番環境での実行はロックによるクエリブロックへの影響を事前に考慮すること。
ただし今回のような状態で当該インデックスが既に利用不能である場合、実質的な影響は限定的である。
REINDEX 後のレプリカ確認(再発防止)
REINDEX は Primary への適用であるが、レプリカが同一の破損ページをストリーミングレプリケーションで保持し続けている可能性がある。
-
レプリカ Pod を再作成し、CloudNativePG による完全な再同期(ベースバックアップからの再構築)を実施する
-
再同期後、レプリカ側でも同様のエラーが発生しないことを確認する
まとめ・所感
今回のインシデントを通じて得た知見を以下にまとめる。
まず「何が壊れているか」を特定する
invalid page in block N of relation global/XXXX というエラーを見た際、「データが壊れた=データロス」と反射的に考えてしまいがちである。
しかし、破損対象がインデックスであればデータロスなしで復旧できる ことを知っているだけで、初動の冷静さが大きく変わる。pg_filenode_relation() による種別特定は最初に実行してみるとよい。
共有カタログの破損を「特定アプリの問題」と誤認しない
global/ 配下のオブジェクトは全データベースが共有参照する。一部のアプリしかエラーを報告していなかったとしても、それは「たまたまそのクエリパスを踏んでいないだけ」に過ぎない。
インシデント発生時に影響範囲を正しく把握するために、global/ を含むかどうかを早期に確認する。
破損は 1 箇所と決めつけない
今回は顕在化した pg_tablespace_oid_index に加え、pg_authid_rolname_index も予防的 REINDEX の対象とした。物理的な障害に起因する場合、隣接する領域が連鎖的に破損している可能性がある。
インデックスの REINDEX はデータロスのリスクがなく、バックアップ取得済みでダウンタイムが許容できる状況であれば、疑わしいオブジェクトは予防的に対処するという判断は有効。
