- Published on
PostgreSQL バージョンアップグレード戦略 — pg_upgrade と論理レプリケーション
- Authors

- Name
- Youngju Kim
- @fjvbn20031
- はじめに
- メジャーアップグレードが難しい理由
- 3つの戦略の概要
- ダウンタイムのトレードオフ比較
- pg_upgrade の詳細手順
- 論理レプリケーションによる無停止アップグレード
- 拡張、シーケンス、大容量データの扱い
- 検証とロールバック
- マネージド環境: RDS と Aurora
- 陥りやすい落とし穴
- おわりに
- 参考資料
はじめに
PostgreSQL は毎年新しいメジャーバージョンをリリースします。そして各メジャーバージョンには5年という明確な寿命があります。リリースから5年が経過すると、セキュリティパッチを含むすべてのサポートが終了します。つまり、データベースを運用している限り、メジャーバージョンアップグレードは避けられない日常的な運用課題なのです。
ところが現実には、メジャーバージョンアップグレードは先延ばしにされ続け、結局はサポート終了(EOL)の直前になって慌てて実施されるケースが多くあります。理由は単純です。アップグレードが怖いからです。数テラバイトの本番データベースに手を入れること、そしてその過程で発生しうるダウンタイムが恐ろしいからです。
この記事では、PostgreSQL メジャーバージョンアップグレードの代表的な3つの戦略を比較します。最もシンプルな dump/restore、ファイルレベルで高速に動作する pg_upgrade、そしてほぼ無停止に近い論理レプリケーション(logical replication)方式です。それぞれの動作原理、ダウンタイムのトレードオフ、実際のコマンド手順、そして陥りやすい落とし穴まで扱います。RDS や Aurora といったマネージド環境での考慮点もあわせて見ていきます。
この記事が扱うのは「メジャー」バージョンアップグレードです。15.3 から 15.8 へ向かうマイナーアップグレードは、バイナリを入れ替えて再起動すれば終わる単純な作業です。データディレクトリのフォーマットが変わらないからです。しかし 15 から 16 へ向かうメジャーアップグレードは、内部カタログ構造とデータディレクトリのレイアウトが変わるため、別途の戦略が必要になります。
メジャーアップグレードが難しい理由
まず、なぜメジャーアップグレードが単純な再起動で終わらないのかを理解する必要があります。PostgreSQL はメジャーバージョンごとに、データファイルの内部フォーマット、システムカタログ構造、WAL フォーマットなどが変更されることがあります。
核心は PG_VERSION ファイルとカタログバージョンです。データディレクトリには、どのメジャーバージョンが作成したかを示す情報が入っており、新しいバージョンのサーバーは、自分より低いバージョンが作成したデータディレクトリをそのまま開くことができません。マイナーバージョンはこのフォーマットを維持することを保証しますが、メジャーバージョンはそうではありません。
したがってメジャーアップグレードは、本質的に「旧バージョンのデータを新バージョンのフォーマットへ移す」作業です。この移し方が何であるかによって、dump/restore、pg_upgrade、論理レプリケーションに分かれます。
[マイナーアップグレード] 15.3 -> 15.8
バイナリ入れ替え + 再起動 -> 完了 (データフォーマット同一)
[メジャーアップグレード] 15 -> 16
データを新バージョンのフォーマットへ変換/移行が必要
- dump/restore : 論理的にすべてエクスポートして再ロード
- pg_upgrade : カタログのみ変換、データファイルを再利用
- logical replication: 新バージョンを構築し変更分をリアルタイム同期
3つの戦略の概要
1. dump/restore
最も古典的でシンプルな方式です。旧バージョンから pg_dump または pg_dumpall で全データを論理的に抽出し、新バージョンに pg_restore または psql でロードします。
利点は明確です。最も安全で移植性が高く、データが完全に書き直されるため、テーブルの肥大化(bloat)が自然に整理されます。新旧バージョン間でアーキテクチャが異なっても(例: 32ビットから64ビット)動作します。
欠点も明確です。遅いのです。全データを1行ずつ INSERT し、インデックスを再ビルドしなければならないため、データサイズに比例して時間が爆発的に増えます。数百GBでもダウンタイムが数時間単位になります。
2. pg_upgrade
PostgreSQL が公式に提供するインプレース(in-place)アップグレードツールです。核心となる考え方は「データファイルはほとんどそのままにして、システムカタログだけを変換する」というものです。
特に --link モードを使うと、新バージョンが旧バージョンのデータファイルにハードリンクを張って再利用します。つまり数テラバイトのデータをコピーしません。そのため、データサイズとほぼ無関係に、通常は数分以内にアップグレードが終わります。
欠点は、同じサーバー(同じファイルシステム)で動作しなければならない点、そして --link 使用時には旧バージョンへの即時ロールバックが難しくなる点です。
3. 論理レプリケーション (logical replication)
PostgreSQL 10 から導入された publication/subscription ベースの論理レプリケーションを活用します。新バージョンのインスタンスをあらかじめ構築しておき、旧バージョンを publisher、新バージョンを subscriber として設定し、データをリアルタイムに同期します。初期データがすべて複製され、変更分まで追いついたら、短い瞬間にアプリケーションの接続だけを新バージョンへ切り替えます。
利点は、ダウンタイムが数秒から数十秒のレベルにまで縮まることです。切り替えまで旧バージョンが稼働し続けているため、十分に検証する時間も確保できます。欠点は、手順が複雑で、論理レプリケーションがサポートしない項目(シーケンスの自動同期など)を手動で処理しなければならない点です。
ダウンタイムのトレードオフ比較
3つの方式の最大の違いは、結局のところダウンタイムと運用の複雑さのトレードオフです。
| 項目 | dump/restore | pg_upgrade (--link) | 論理レプリケーション |
|---|---|---|---|
| ダウンタイム | 非常に長い (データサイズ比例) | 短い (通常数分) | ほぼなし (数秒〜数十秒) |
| データサイズの影響 | 非常に大きい | ほぼなし | 初期同期にのみ影響 |
| ディスク追加使用 | 新バージョン分追加 | ほぼなし (ハードリンク) | 新バージョン分追加 |
| ロールバックの難易度 | 容易 (旧バージョンそのまま) | 困難 (link 時) | 容易 (切り替え前まで) |
| 手順の複雑さ | 低い | 中程度 | 高い |
| バージョン間ジャンプ | 自由 | 隣接/多段階可能 | publisher は10以上必要 |
| bloat 整理効果 | あり | なし | あり |
選択基準を単純化すると、次のようになります。
データが小さく(数GB)ダウンタイムに余裕がある
-> dump/restore (最もシンプル、最も安全)
データが大きく短いメンテナンス枠(数分)を取れる
-> pg_upgrade --link
ダウンタイムをほとんど許容できない / 24x7 サービス
-> 論理レプリケーション
pg_upgrade の詳細手順
それでは、最も広く使われている pg_upgrade を実際のコマンドレベルで見ていきます。前提は、旧バージョン(例: 15)と新バージョン(例: 16)のバイナリが同じサーバーに両方インストールされていることです。
事前準備
まず新バージョンのバイナリをインストールし、新しいデータディレクトリを初期化します。このとき、旧バージョンと同じロケール/エンコーディングで初期化しなければなりません。
# 旧バージョンと新バージョンのデータディレクトリパス
OLD_BIN=/usr/lib/postgresql/15/bin
NEW_BIN=/usr/lib/postgresql/16/bin
OLD_DATA=/var/lib/postgresql/15/main
NEW_DATA=/var/lib/postgresql/16/main
# 新バージョンのデータディレクトリ初期化 (旧と同じロケール/エンコーディング)
"$NEW_BIN/initdb" -D "$NEW_DATA" \
--encoding=UTF8 --locale=en_US.UTF-8
互換性の事前チェック (--check)
実際のアップグレードの前に、必ず --check モードで互換性をまず検証します。このステップはデータにまったく触れず、アップグレードを妨げうる問題を事前に報告してくれます。
# 両サーバーが停止している状態で実行
"$NEW_BIN/pg_upgrade" \
--old-bindir="$OLD_BIN" \
--new-bindir="$NEW_BIN" \
--old-datadir="$OLD_DATA" \
--new-datadir="$NEW_DATA" \
--check
--check が報告する代表的な問題は、削除されたデータ型(例: かつての abstime)、互換性のない拡張モジュール、新バージョンに未インストールの拡張などです。ここで出たすべての警告を解消する前には、実際のアップグレードを進めません。
実際のアップグレード (--link)
チェックが通過したら、実際のアップグレードを進めます。--link を付けると、データファイルをコピーする代わりにハードリンクを作成し、時間を大幅に短縮します。
# 必ず旧バージョン/新バージョンのサーバーを両方停止した状態で!
"$NEW_BIN/pg_upgrade" \
--old-bindir="$OLD_BIN" \
--new-bindir="$NEW_BIN" \
--old-datadir="$OLD_DATA" \
--new-datadir="$NEW_DATA" \
--link
アップグレードが終わると、2つのスクリプトが生成されます。1つは統計情報を再収集するスクリプトで(新バージョンはプランナ統計を引き継げないため ANALYZE が必要)、もう1つは旧バージョンのデータディレクトリを削除するスクリプトです。
# 新バージョンのサーバー起動後、必ず統計を再収集
"$NEW_BIN/vacuumdb" --all --analyze-in-stages
# 検証が終わった後でのみ旧バージョンのデータを削除
./delete_old_cluster.sh
--link モードの重要な注意点
--link は強力ですが、その分だけ危険です。最も重要なルールは、新バージョンのクラスタを起動した瞬間、旧バージョンはもはや安全ではないということです。
ハードリンクで同じデータファイルを共有しているため、新バージョンが書き込みを始めると、旧バージョンのデータも一緒に変更されます。つまり新バージョンを一度でも正常に起動して書き込みが発生すると、旧バージョンへのロールバックは事実上不可能になります。
[--link モードの危険区間]
pg_upgrade --link 完了
|
v
新バージョン起動 (書き込み発生) --- この時点から旧バージョンのロールバック不可
|
v
検証 OK -> 本番運用
検証 NG -> バックアップから復旧が必要 (旧バージョンをただ起動できない)
そのため、--link アップグレードの前には、必ず信頼できるバックアップ(またはスナップショット)を確保しなければなりません。もしディスクに余裕があるなら、--link の代わりにデフォルトのコピーモードを使うのも安全な選択です。コピーモードは遅いですが、旧バージョンがそのまま残るため、ロールバックが自由です。
もう1つ、--link は旧バージョンと新バージョンのデータディレクトリが同じファイルシステム上になければなりません。ハードリンクはファイルシステムの境界を越えられないからです。
論理レプリケーションによる無停止アップグレード
ダウンタイムをほとんど許容できない環境なら、論理レプリケーションが答えです。全体像は次のとおりです。
[論理レプリケーション アップグレードの流れ]
旧(15) Publisher 新(16) Subscriber
| |
| 1. publication 作成 |
| ---------------------------> | 2. スキーマを事前作成
| | 3. subscription 作成
| 4. 初期データコピー -------> |
| 5. 変更分ストリーミング ---> | (リアルタイム追従)
| |
| 6. lag ~= 0 を確認 |
| 7. 書き込み遮断 + シーケンス同期 |
| 8. アプリケーション切替 ---> | (新バージョンが稼働)
ステップ1: 旧バージョンの準備
publisher 側で論理レプリケーションを有効にする必要があります。wal_level を logical に上げ、再起動が必要です。
# postgresql.conf (旧バージョン)
wal_level = logical
max_replication_slots = 10
max_wal_senders = 10
ステップ2: スキーマの事前移行
論理レプリケーションはデータ(行)のみを複製します。テーブル定義、インデックス、制約、関数といったスキーマは自動で作ってくれません。したがって新バージョンにスキーマをあらかじめ作成しておく必要があります。
# 旧バージョンからスキーマのみ抽出 (データ除外)
pg_dump -h OLD_HOST -U postgres -d appdb \
--schema-only --no-owner -f schema.sql
# 新バージョンへスキーマをロード
psql -h NEW_HOST -U postgres -d appdb -f schema.sql
ステップ3: publication と subscription
次に、旧バージョンに publication を、新バージョンに subscription を作成します。
-- 旧バージョン(Publisher)で実行
CREATE PUBLICATION pub_all FOR ALL TABLES;
-- 新バージョン(Subscriber)で実行
CREATE SUBSCRIPTION sub_all
CONNECTION 'host=OLD_HOST dbname=appdb user=repl password=secret'
PUBLICATION pub_all
WITH (copy_data = true);
copy_data = true で作成すると、subscription 作成と同時に初期データコピーが始まり、コピーが終わると自動的に変更分のストリーミングへ切り替わります。
ステップ4: 同期の進捗確認
レプリケーションが追いついているかをモニタリングします。publisher 側でレプリケーション遅延(lag)を確認できます。
-- 旧バージョン(Publisher)で: レプリケーション遅延を確認
SELECT
slot_name,
pg_size_pretty(
pg_wal_lsn_diff(pg_current_wal_lsn(), confirmed_flush_lsn)
) AS lag
FROM pg_replication_slots;
-- 新バージョン(Subscriber)で: テーブルごとの同期状態
SELECT srrelid::regclass AS table_name, srsubstate
FROM pg_subscription_rel;
srsubstate が r(ready)であれば、そのテーブルは初期コピーを終えてストリーミング段階に入ったことを意味します。
ステップ5: シーケンスの同期
ここが論理レプリケーションの最大の落とし穴です。論理レプリケーションはシーケンス値を複製しません。 テーブルデータは追いついても、シーケンスの現在値(last_value)は新バージョンで初期状態のまま残ります。この状態で切り替えると、PK の衝突が発生します。
切り替え直前、書き込みを遮断した状態で、すべてのシーケンスを手動で合わせる必要があります。
-- 旧バージョンで各シーケンスの現在値を抽出する SQL を生成
SELECT
'SELECT setval(' || quote_literal(schemaname || '.' || sequencename)
|| ', ' || COALESCE(last_value, 1) || ');'
FROM pg_sequences;
この結果として出た setval 文を新バージョンで実行すると、シーケンスが合います。安全のため、少し余裕分(例: 現在値に1000を足す)を持たせるのも良い方法です。
ステップ6: カットオーバー
いよいよ切り替えです。手順は短く決然と進めます。
[カットオーバーの順序]
1. アプリケーションの書き込みを遮断 (read-only 切替またはトラフィック停止)
2. レプリケーション lag = 0 を最終確認 (残りの変更分をすべて反映待ち)
3. シーケンス値の同期 (ステップ5)
4. 新バージョンで subscription を無効化/削除
5. アプリケーションの接続文字列を新バージョンへ変更
6. トラフィック再開、新バージョンの正常動作をモニタリング
このカットオーバー区間でのみ書き込みが止まるため、実質的なダウンタイムは数秒から数十秒にとどまります。
拡張、シーケンス、大容量データの扱い
アップグレード方式に関係なく、気を配るべき共通項目があります。
拡張モジュール (extensions)
pg_upgrade は拡張の SQL オブジェクトは移してくれますが、拡張の共有ライブラリ(.so)は新バージョンにあらかじめインストールされている必要があります。たとえば PostGIS を使っているなら、新バージョン向けの PostGIS パッケージが先に入っていなければなりません。
アップグレード後には、拡張のバージョンを新バージョンに合わせて上げる作業が追加で必要になることがあります。
-- アップグレード後に拡張バージョンを更新
ALTER EXTENSION postgis UPDATE;
-- インストール済み拡張と利用可能な最新バージョンを比較
SELECT name, default_version, installed_version
FROM pg_available_extensions
WHERE installed_version IS NOT NULL;
大容量データとインデックス
dump/restore や論理レプリケーションは、ターゲット側でインデックスを再ビルドします。数億行のテーブルのインデックス再生成は、時間とリソースを大きく消費します。初期ロード時にはインデックスを後で作り、maintenance_work_mem を大きく取ってビルド速度を上げるのが良いでしょう。
# 初期ロード/インデックスビルド中だけ一時的に大きく (新バージョン)
maintenance_work_mem = 2GB
max_wal_size = 8GB
論理レプリケーションで初期コピーが遅すぎる場合は、大きなテーブルを publication から除外して別途移行し、後で追加する分割戦略も検討できます。
検証とロールバック
アップグレードは「終わった」ではなく「検証まで終わった」が本当の完了です。
検証チェックリスト
[アップグレード後の検証項目]
- すべてのデータベース/スキーマ/テーブル数が一致
- 主要テーブルの row count が新旧で一致
- 統計の再収集(ANALYZE)が完了
- 拡張が正常に動作しバージョンを確認
- シーケンス値が正常 (論理レプリケーション時は必須)
- 主要クエリの実行計画の回帰チェック
- アプリケーションのスモークテストが通過
特に実行計画の回帰はよく見落とされます。メジャーバージョンが変わると、プランナの動作が微妙に変わることがあり、よく動いていたクエリが突然遅くなることがあります。アップグレード直後に統計を再収集し、主要クエリの実行計画を比較するのが良いでしょう。
ロールバック戦略
方式ごとにロールバックの難易度が大きく異なります。
| 方式 | ロールバック方法 |
|---|---|
| dump/restore | 旧バージョンがそのまま生きているので接続を戻すだけ |
| pg_upgrade (コピー) | 旧データディレクトリ保存 -> 旧バージョン再起動 |
| pg_upgrade (--link) | 新バージョン起動後は事実上バックアップ復旧のみ |
| 論理レプリケーション | 切替前は自由、切替後は逆方向レプリケーションの設計が必要 |
論理レプリケーションでカットオーバー後のロールバックまで備えるには、切り替え後も新バージョン -> 旧バージョン方向の逆レプリケーションをあらかじめ構成しておく高度な戦略が必要です。こうすれば問題が起きたときに旧バージョンへトラフィックを戻せますが、構成の複雑さが大きく上がります。
マネージド環境: RDS と Aurora
クラウドのマネージド PostgreSQL では、アップグレードのメカニズムが抽象化されています。
RDS for PostgreSQL
RDS はコンソールや API でメジャーバージョンアップグレードをトリガーでき、内部的に pg_upgrade を使います。アップグレード前に自動でスナップショットを取るため、ロールバックの安全性がある程度確保されます。ただしその間インスタンスが利用不可の状態になるため、ダウンタイムが発生します。
ブルー/グリーンデプロイ(Blue/Green Deployment)を使えば、ダウンタイムを大きく減らせます。グリーン環境に新バージョンをあらかじめ作り、論理レプリケーションで同期した後、短い切り替えだけで昇格します。
Aurora PostgreSQL
Aurora もインプレースのメジャーアップグレードをサポートし、同様にブルー/グリーンデプロイを提供します。Aurora はストレージがコンピュートと分離されているため、アップグレードモデルが通常の RDS とはやや異なる動作をします。マネージド環境では、自分で pg_upgrade を回す代わりに、提供されるアップグレード経路とブルー/グリーン機能を優先して活用するほうが運用の負担が少なくなります。
共通して、マネージドであれセルフホストであれ、アップグレード前のバックアップ/スナップショット確保と事前の互換性チェックは同じように重要です。
陥りやすい落とし穴
最後に、実務でよく出会う落とし穴を整理します。
第一に、ロケールとエンコーディングの不一致です。新バージョンのデータディレクトリを旧バージョンと異なるロケールで初期化すると、pg_upgrade が拒否するか、通過したとしてもソート順が微妙に変わってインデックスが壊れることがあります。glibc collation バージョンの変更もインデックス破損を引き起こしうるので、アップグレード後にテキストインデックスの再ビルドを検討するのが安全です。
第二に、拡張の未インストールです。新バージョンに拡張ライブラリを入れないまま pg_upgrade を回すと、--check 段階で失敗します。すべての拡張を事前に新バージョン向けにインストールする必要があります。
第三に、論理レプリケーションのシーケンス漏れです。先に強調したとおり、シーケンスは自動で同期されません。カットオーバー直前に setval を漏らすと、新バージョンで最初の INSERT から PK 衝突が発生します。
第四に、PK のないテーブルの論理レプリケーション制約です。論理レプリケーションで UPDATE/DELETE を複製するには、対象テーブルに主キーまたは REPLICA IDENTITY が必要です。これがないと INSERT のみ複製されるか、エラーになります。
第五に、統計の再収集漏れです。pg_upgrade はプランナ統計を引き継ぎません。アップグレード直後に ANALYZE を回さないと、すべてのクエリが誤った実行計画で動作し、データベース全体が遅くなります。
第六に、--link 後の旧バージョン起動です。検証のためと言って新バージョンと旧バージョンの両方を起動した瞬間、データが破損することがあります。--link モードでは、両者のうち一方だけが生きていなければなりません。
おわりに
PostgreSQL メジャーバージョンアップグレードは、結局のところ「ダウンタイムをどれだけ減らせるか」と「運用の複雑さをどれだけ引き受けられるか」の間の選択です。データが小さいなら dump/restore のシンプルさが最善であり、データは大きいが短いメンテナンス枠を取れるなら pg_upgrade --link が強力です。ダウンタイムをほとんど許容できないなら論理レプリケーションが答えですが、その分だけ手間がかかります。
どの方式を選んでも変わらない原則があります。事前に --check で互換性を検証し、信頼できるバックアップを確保し、アップグレード後に統計を再収集し、そして本番に載せる前に十分に検証することです。アップグレードは先延ばしにするほどリスクが大きくなるものです。EOL に追われる前に、平穏な時期に一度でもリハーサルを回してみることが、最良の戦略です。
参考資料
- pg_upgrade 公式ドキュメント
- Logical Replication 公式ドキュメント
- pg_dump 公式ドキュメント
- CREATE PUBLICATION リファレンス
- CREATE SUBSCRIPTION リファレンス
- PostgreSQL Versioning Policy
- Upgrading the PostgreSQL DB engine for Amazon RDS
- Amazon RDS Blue/Green Deployments
- Upgrading Amazon Aurora PostgreSQL DB clusters
- PostgreSQL Wiki: Upgrading