Skip to content

필사 모드: PostgreSQL バージョンアップグレード戦略 — pg_upgrade と論理レプリケーション

日本語
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

はじめに

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 公式ドキュメント](https://www.postgresql.org/docs/current/pgupgrade.html)

- [Logical Replication 公式ドキュメント](https://www.postgresql.org/docs/current/logical-replication.html)

- [pg_dump 公式ドキュメント](https://www.postgresql.org/docs/current/app-pgdump.html)

- [CREATE PUBLICATION リファレンス](https://www.postgresql.org/docs/current/sql-createpublication.html)

- [CREATE SUBSCRIPTION リファレンス](https://www.postgresql.org/docs/current/sql-createsubscription.html)

- [PostgreSQL Versioning Policy](https://www.postgresql.org/support/versioning/)

- [Upgrading the PostgreSQL DB engine for Amazon RDS](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_UpgradeDBInstance.PostgreSQL.html)

- [Amazon RDS Blue/Green Deployments](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/blue-green-deployments.html)

- [Upgrading Amazon Aurora PostgreSQL DB clusters](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/USER_UpgradeDBInstance.PostgreSQL.html)

- [PostgreSQL Wiki: Upgrading](https://wiki.postgresql.org/wiki/Upgrading)

현재 단락 (1/192)

PostgreSQL は毎年新しいメジャーバージョンをリリースします。そして各メジャーバージョンには5年という明確な寿命があります。リリースから5年が経過すると、セキュリティパッチを含むすべてのサポー...

작성 글자: 0원문 글자: 11,593작성 단락: 0/192