PostgreSQL 9.6→10アップグレードのダウンタイムをpglogicalで最小化(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

PostgreSQL 9.6->10アップグレードのダウンタイムをpglogicalで最小化(翻訳)

PostgreSQL 10がリリースされ、9.6クラスタを最新バージョンにアップグレードしてみたくなりました。しかし、以前アップグレードしたときのようにメンテナンス画面を開いてマイグレーションを実行するのに膨大な段取りが必要になるのでしょうか。そのときは、アプリをメンテナンスモードに切り替え、新しいダンプを取って新しいクラスタでリストアし、メンテナンスモードをオフにしました。

この方法だと、アプリは1時間ほど、もしかするともっと長い時間使えなくなるかもしれません。pglogicalを再読した後、9.6から10への切り替えをわずか数秒で完了できるpglogicalを試す決心を固めました。

概要

pglogicalは論理レプリケーションを実装していて、バージョンの異なるデータベース同士でもレプリケーションを行えます。これはPostgreSQL自身が提供するバイナリレプリケーションでは不可能です。PostgreSQL 10では論理レプリケーション機能のサポートがいくつか追加されましたが、9.6からのレプリケーションを行いたいので、いくつかの外部拡張に頼る必要があります。

pglogicalの必要条件のひとつは、レプリケーションされるすべてのテーブルに主キーが設定されていることです。主キーは単独カラムでなくても構いませんが、主キーの存在は必須です。また、レプリケーションエージェントが動作するために、両方のデータベースにsuperuserでアクセスできる必要もあります。DDLレプリケーションはサポートされず、TRUNCATE CASCADEはレプリケーションされません。特殊な条件ではないので、ほとんどのデータベースについてレプリケーションできるはずです。

ただし、主キーの必須要件には特別な注意が必要です。特に移行前のデータベースで、ActiveRecord gemでデータベースマイグレーションを管理していた場合、以前はschema_migrationsテーブルに主キーがありませんでした。その場合は次を実行します。

alter table schema_migrations add primary key (version);

アップグレードの方針は、PostgreSQLパッケージをpglogical拡張のサポート付きでインストールしてから、新しいPostgreSQL 10クラスタを作成して、新しいクラスタだけにスキーマをリストアするというものです。現在のクラスタの停止/再起動は、pglogicalを有効にしてインストールしたPostgreSQLを使って行うべきです。このクラスタはTCP/IP経由で他方に接続可能になっている必要があります。プロバイダ側(アップグレード前の9.6データベースを指す)とサブスクライバ側(新しいPostgreSQL 10データベースを指す)には、接続先のIPアドレスとポートをそれぞれ指定する必要があります。pglogical拡張を両方のデータベースで作成し、postgresql.confとpg_hba.confで論理レプリケーションを有効にし、両方のデータベースを再起動します。最後に、プロバイダ、サブスクライバ、そしてサブスクリプションを作成するいくつかのpglogical文が発行されてレプリケーションが開始されます。レプリケーションが完了したら、新しいクラスタのポートを古いクラスタに合わせて変更して古いクラスタを停止し、新しいクラスタを再起動できます。最後にアプリも再起動するとよいでしょう。特にrow型などのカスタム型がある場合、OIDが変わる可能性が高く、そうしたrow型が登録されているとアプリを再起動するまで期待どおりに動作しないためです。たとえばSequel gemを用いたDB.register_row_typeが使われている場合、これに該当する可能性があります。

最後の切り替えは可能な限り数秒以内に行われるので、ダウンタイムを最小限にできます。

手順のハンズオン

私たちのサーバーではアプリの他にDockerでPostgreSQLを実行しているので、本記事では手順の説明にもDockerを使いますが、他のセットアップでの手順にも簡単に応用できるはずです。Dockerでデモを行う利点は、(Dockerコンテナを)そのまま簡単に複製できることと、データベースの作成や実行にも対応できる点です。

本記事では、PostgreSQLクライアントがホストにもインストールされていることが前提です。

Dockerイメージと起動スクリプトの準備

以下のDockerfileをpg96/とpg10/サブディレクトリにそれぞれ作成します(Dockerコンテナ内でPostgreSQLを実行していない場合、自分の環境で複製するにはDockerfile内の手順をご覧ください)。

# pg96/Dockerfile
FROM postgres:9.6

RUN apt-get update && apt-get install -y wget gnupg
RUN echo "deb [arch=amd64] http://packages.2ndquadrant.com/pglogical/apt/ jessie-2ndquadrant main" > /etc/apt/sources.list.d/2ndquadrant.list\
  && wget --quiet -O - http://packages.2ndquadrant.com/pglogical/apt/AA7A6805.asc | apt-key add -\
  && apt-get update\
  && apt-get install -y postgresql-9.6-pglogical

RUN echo "host    replication          postgres                172.18.0.0/16   trust" >> /usr/share/postgresql/9.6/pg_hba.conf.sample
RUN echo "host    replication          postgres                ::1/128         trust" >> /usr/share/postgresql/9.6/pg_hba.conf.sample
RUN echo "shared_preload_libraries = 'pglogical'" >> /usr/share/postgresql/postgresql.conf.sample
RUN echo "wal_level = 'logical'" >> /usr/share/postgresql/postgresql.conf.sample
RUN echo "max_wal_senders = 20" >> /usr/share/postgresql/postgresql.conf.sample
RUN echo "max_replication_slots = 20" >> /usr/share/postgresql/postgresql.conf.sample
# pg10/Dockerfile
FROM postgres:10

RUN rm /etc/apt/trusted.gpg && apt-get update && apt-get install -y wget
RUN echo "deb [arch=amd64] http://packages.2ndquadrant.com/pglogical/apt/ stretch-2ndquadrant main" > /etc/apt/sources.list.d/2ndquadrant.list\
  && wget --quiet -O - http://packages.2ndquadrant.com/pglogical/apt/AA7A6805.asc | apt-key add -\
  && apt-get update\
  && apt-get install -y postgresql-10-pglogical

RUN echo "host    replication          postgres                172.18.0.0/16   trust" >> /usr/share/postgresql/10/pg_hba.conf.sample
RUN echo "host    replication          postgres                ::1/128         trust" >> /usr/share/postgresql/10/pg_hba.conf.sample
RUN echo "shared_preload_libraries = 'pglogical'" >> /usr/share/postgresql/postgresql.conf.sample
RUN echo "wal_level = 'logical'" >> /usr/share/postgresql/postgresql.conf.sample
RUN echo "max_wal_senders = 20" >> /usr/share/postgresql/postgresql.conf.sample
RUN echo "max_replication_slots = 20" >> /usr/share/postgresql/postgresql.conf.sample

両方のサーバーはIPアドレス10.0.1.10の同じコンピュータ上で実行されることを前提とします。9.6のインスタンスはポート5432で、新しいクラスタは当初(切り替え前)ポート5433で実行されます。

cd pg96 && docker build . -t postgresql-pglogical:9.6 && cd -
cd pg10 && docker build . -t postgresql-pglogical:10 && cd -

本記事はDockerのチュートリアルではありませんが、Dockerを実際に使っている場合は、これらのDockerイメージをプライベートなレジストリにpushするとよいかもしれません。

最初の手順では、古い9.6クラスタを停止し、古いデータを持つクラスタのpglogicalを有効にして起動します(作業前には常にバックアップを取っておくようにしましょう)。クラスタデータは/var/lib/postgresql/9.6/main/に、設定ファイルは/etc/postgresql/9.6/main/にそれぞれ置かれているとします。/etc/postgresql/9.6や/var/lib/postgresql/9.6がない場合もスクリプトが新しいクラスタを作成してくれるので心配無用です(なお、最初に新しいデータベースを試してみたい場合は一時ディレクトリをいくつかマップしておくとよいでしょう)。

以下のスクリプトを/sbin/pg-scripts/start-pgに作成し、ファイルに実行可能属性を与えます。このスクリプトはコンテナのデータベースを実行します。

#!/bin/bash
version=$1
net=$2
setup_db(){
  pg_createcluster $version main -o listen_addresses='*' -o wal_level=logical\
        -o max_wal_senders=10 -o max_worker_processes=10 -o max_replication_slots=10\
        -o hot_standby=on -o max_wal_senders=10 -o shared_preload_libraries=pglogical -- -A trust
  pghba=/etc/postgresql/$version/main/pg_hba.conf
  echo -e "host\tall\tappuser\t$net\ttrust" >> $pghba
  echo -e "host\treplication\tappuser\t$net\ttrust" >> $pghba
  echo -e "host\tall\tpostgres\t172.17.0.0/24\ttrust" >> $pghba
  echo -e "host\treplication\tpostgres\t172.17.0.0/24\ttrust" >> $pghba
  pg_ctlcluster $version main start
  psql -U postgres -c '\du' postgres|grep -q appuser || createuser -U postgres -l -s appuser
  pg_ctlcluster $version main stop
}
[ -d /var/lib/postgresql/$version/main ] || setup_db
exec pg_ctlcluster --foreground $version main start

クラスタが存在しない場合は、このスクリプトによって新しいクラスタが作成されます。なお実際のレプリケーションに必要というわけではありませんが、このスクリプトは作業をシンプルにするため「trust」でsuperuser認証された新しいappuserデータベース作成もサポートします。これはテスト目的で新しいデータベースを生成したい場合に役立つことがあります。必要な場合はこのスクリプトを適宜調整してユーザー名や認証方法を変更します。

コンテナを実行する

9.6クラスタをポート5432で実行します(試すだけなら、別のポートで実行したり一時ディレクトリにマッピングしたりしても構いません)。

docker run --rm -v /sbin/pg-scripts:/pg-scripts -v /var/lib/postgresql:/var/lib/postgresql\
    -v /etc/postgresql:/etc/postgresql -p 5432:5432 postgres-pglogical:9.6\
    /pg-scripts/start-pg 9.6 10.0.1.0/24
# since we're running in the foreground with the --rm option, run this in another terminal:
docker run --rm -v /sbin/pg-scripts:/pg-scripts -v /var/lib/postgresql:/var/lib/postgresql\
    -v /etc/postgresql:/etc/postgresql -p 5433:5432 postgres-pglogical:10\
    /pg-scripts/start-pg 10 10.0.1.0/24

start-pgの第1引数はPostgreSQLのバージョン、第2引数と最後の引数はpg_hba.confが存在しない場合の作成に使うネットワークです。これは、appuserが「trust」認証方法で接続するのに使われます。

Dockerコンテナをsystemdサービスとして動かしてみたい方は、原文記事末尾のコメントでお知らせいただければ時間のあるときに補足します(実は難しくありません)。ドキュメントはネット上にいろいろありますが、私たちの独自サービスユニットファイルはこうしたチュートリアルと少し違っている部分があります。違いは、サービス起動時にポートが接続を受け付けるかどうかをチェックしているのと、(Docker)イメージが既にローカルにある場合はレジストリからpullしないようになっている点です。

PostgreSQL設定を編集する

Postgresql-pglogicalコンテナを使うファイルが古いクラスタで実行できるようになったら、postgresql.confファイルを更新してコンテナを再起動します。以下の設定は9.6クラスタと10クラスタのどちらの設定のベースとしても使えます。

wal_level = logical
max_worker_processes = 10
max_replication_slots = 10
max_wal_senders = 10
shared_preload_libraries = 'pglogical'

pg_hba.confには以下の行を含めてください(Dockerを使っていない場合や、デフォルト以外のネットワークでコンテナを実行している場合は、ネットワーク設定を変更してください)。

host    all     postgres        172.17.0.0/24   trust
host    replication     postgres        172.17.0.0/24   trust

サーバーを再起動すればレプリケーション開始の準備が整います。

データベースのレプリケーションを実行する

プロバイダのセットアップ

PostgreSQL 9.6データベースで以下を実行します。

# PostgreSQLでリストアするスキーマをダンプする
pg_dump -Fc -s -h 10.0.1.10 -p 5432 -U appuser mydb > mydb-schema.dump
psql -h 10.0.1.10 -p 5432 -c 'create extension pglogical;' -U appuser mydb
psql -h 10.0.1.10 -p 5432 -c "select pglogical.create_node(node_name := 'provider', dsn := 'host=10.0.1.10 port=5432 dbname=mydb');" -U appuser mydb
psql -h 10.0.1.10 -p 5432 -c "select pglogical.replication_set_add_all_tables('default', ARRAY['public']);" -U appuser mydb

# 私の場合シーケンスレプリケーションが動かなかったので、データベース切り替えの直前には別の方法をおすすめします
# psql -h 10.0.1.10 -p 5432 -c "select pglogical.replication_set_add_all_sequences('default', ARRAY['public']);" -U appuser mydb

これで、パブリックなスキーマのテーブルとシーケンスがレプリケーション対象としてマーキングされます。

サブスクライバとサブスクリプションのセットアップ

PostgreSQL 10データベースで以下を実行します。

# データベーススキーマの作成とリストア
createdb -U appuser -h 10.0.1.10 -p 5433 mydb
pg_restore -s -h 10.0.1.10 -p 5433 -U appuser -d mydb mydb-schema.dump
# pglogical拡張をインストールしてサブスクライバとサブスクリプションをセットアップ
psql -h 10.0.1.10 -p 5433 -c 'create extension pglogical;' -U appuser mydb
psql -h 10.0.1.10 -p 5433 -c "select pglogical.create_node(node_name := 'subscriber', dsn := 'host=10.0.1.10 port=5433 dbname=mydb');" -U appuser mydb
psql -h 10.0.1.10 -p 5433 -c "select pglogical.create_subscription(subscription_name := 'subscription', provider_dsn := 'host=10.0.1.10 port=5432 dbname=mydb');" -U appuser mydb

これで、レプリケーションのステータスを以下でチェックできるようになります。

select pglogical.show_subscription_status('subscription');

初期化が完了してデータベースが同期およびレプリケーションされると(データベースのサイズによってそれなりに時間がかかります)、切り替えを開始できるようになります。

シーケンス値のレプリケーション

この時点でデータベースのレプリケーションはほぼ完了です。シーケンス値のレプリケーション方法については見つけられませんでした。シーケンスに依存するシリアル整数主キーカラムを使っている場合は正しいシーケンス値も設定したいはずであり、さもないとシリアルシーケンスの次の値に依存する新しいレコードをINSERTできなくなってしまいます。その方法についてここで説明します。set-valueステートメントを生成した後で古いサーバーを停止する時間を確保できるよう、データベースへの大量書き込み集中に備えてギャップ値5000を挿入しています。おそらくこのギャップ値は、スクリプト実行してからサーバーが停止までの間にデータベースがどのぐらい急速に増大するかに応じて適宜調整が必要です。

psql -h 10.0.1.10 -p 5432 -U appuser -c "select string_agg('select ''select setval(''''' || relname || ''''', '' || last_value + 5000 || '')'' from ' || relname, ' union ' order by relname) from pg_class where relkind ='S';" -t -q -o set-sequences-values-generator.sql mydb
psql -h 10.0.1.10 -p 5432 -U appuser -t -q -f set-sequences-values-generator.sql -o set-sequences-values.sql mydb
# 新しいシーケンス値を新しいデータベース(例ではポート5433)に設定する
psql -h 10.0.1.10 -p 5433 -U appuser -f set-sequences-values.sql mydb

最後の切り替え手順

次は基本的に、PostgreSQL 10クラスタのポートを5432に変更します(古いクラスタのポートであれば何でも構いません)。続いて9.6クラスタを停止し(上の例ではCtrl-Cで停止)、新しいクラスタを再起動します。最後に、変換ルールでrow型のOIDに依存しているカスタム型がある場合は、データベースを使っているアプリも再起動するとよいでしょう。

ここでは、SQLステートメント発行の前に何らかの接続バリデーションを用いてプール内のコネクションを安全に切断できるアプリを仮定しています。そうでない場合は、postgresql.confやpg_hba.confをいじった後にデータベースを再起動したら、必ずアプリも再起動するのがよいでしょう。

クリーンアップ

新しいデータベースでの正常な動作を確認できたら、次のようにして片付けられます。

select pglogical.drop_subscription('subscription');
select pglogical.drop_node('subscriber');
drop extension pglogical;

この記事がダウンタイム最小限のデータベースアップグレードに役立つことを願っています。

関連記事

Rails開発者のためのPostgreSQLの便利技(翻訳)

PostgreSQL 10の使って嬉しい5つの機能(翻訳)

PostgreSQLの機能と便利技トップ10(2016年版)(翻訳)

デザインも頼めるシステム開発会社をお探しならBPS株式会社までどうぞ 開発エンジニア積極採用中です! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

hachi8833

Twitter: @hachi8833、GitHub: @hachi8833 コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。 これまでにRuby on Rails チュートリアル第2版の半分ほど、Railsガイドの初期翻訳ではほぼすべてを翻訳。その後も折に触れてそれぞれ一部を翻訳。 かと思うと、正規表現の粋を尽くした日本語エラーチェックサービス enno.jpを運営。 実は最近Go言語が好き。 仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。

hachi8833の書いた記事

週刊Railsウォッチ

インフラ

ActiveSupport探訪シリーズ