Docker SwarmでPostgreSQLストリーミングレプリケーションを実装する

# はじめに

DockerのSwarmモードを使ってPostgreSQLストリーミングレプリケーションを実装します。Docker Composeファイルとレプリケーション設定用のスクリプトを組み合わせて実現しています。PostgreSQLのサンプルデータベースとして公開されているDVDレンタル予約データベースを構築し、レプリケーションの動作確認をしています。

# ソフトウェア構成

| ソフトウェア   | バージョン |
| -------------- | ---------- |
| Cent OS        | 8.1        |
| Docker         | 20.10.5    |
| Docker Compose | 1.28.5     |
| PostgreSQL     | 12.6       |

# インフラ準備

#### Dockerインストール

- レポジトリを最新化後、Dockerレポジトリを追加する。
- コンテナランタイム(containerd.io)をインストール後、Dockerをインストールし、バージョンを確認する。
- サービス起動と自動起動を有効化し、ステータスを確認する。

```
# dnf -y update
# dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo

# wget https://download.docker.com/linux/centos/8/x86_64/stable/Packages/containerd.io-1.4.3-3.1.el8.x86_64.rpm
# dnf install -y containerd.io-1.4.3-3.1.el8.x86_64.rpm
# dnf install -y docker-ce docker-ce-cli
# docker --version
Docker version 20.10.5, build 55c4c88

# systemctl start docker
# systemctl enable docker
# systemctl status docker
```

#### Docker Composeインストール

- Docker Composeモジュールをダウンロードする。
- ダウンロードしたモジュールに実行権限を追加し、バージョンを確認する。

```
# curl -L "https://github.com/docker/compose/releases/download/1.28.5/docker-compose-Linux-x86_64" -o /usr/local/bin/docker-compose
# chmod +x /usr/local/bin/docker-compose
# docker-compose --version
docker-compose version 1.28.5, build c4eb3a1f
```

#### Swarmクラスタ構築

- マスタノードでSwarmクラスタを初期化する。
- マルチノードで構成する場合、追加したいホストのIPアドレスを指定して、クラスタに参加させる。なお、クラスタノード間で名前解決ができるようにしておく必要がある。

```
# docker swarm init --advertise-addr <master-node-ip-address>
# ssh <slave-node-ip-address> `docker swarm join-token worker | grep "docker swarm"`
```

# イメージ・構成ファイル準備

#### Dockerイメージダウンロード

- PostgreSQLの公式Dockerイメージをダウンロード、イメージを確認する。

```
# docker pull postgres:12.6
# docker images
REPOSITORY   TAG       IMAGE ID       CREATED         SIZE
postgres     12.6      fd94a7538179   4 days ago      314MB
```

#### ディレクトリ作成

- 任意の作業ディレクトリ配下でmaster、replicaディレクトリを作成し、ディレクトリを移動する。ここでは作業ディレクトリをdvdrentalとしている。

```
# mkdir -p dvdrental/master dvdrental/replica
# cd dvdrental
# find .
.
./master
./replica
```

#### Composeファイル作成

- PostgreSQLストリーミングレプリケーション用のComposeファイルを作成する。
  - PostgreSQLの公式イメージを使用
  - environmentで必要な環境変数を設定
  - レプリカDBがマスタDBの後に起動するように依存関係を指定
  - レプリカDBの起動サービス数を3に指定
  - DB初期化スクリプト配置ディレクトリに作業ディレクトリをマウント

```
# cat <<EOF > ./docker-compose.yml
version: "3"
services:
  master:
    image: postgres:12.6
    ports:
      - 5432:5432
    environment:
      POSTGRES_USER: "postgres" # Specify the database user name
      POSTGRES_PASSWORD: "P0stgres" # Specify the user password
      POSTGRES_DB: "dvdrental" # Specify the database name
      POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --locale=C" # Specify encoding and locale
    volumes:
      - ./master:/docker-entrypoint-initdb.d
  replica:
    image: postgres:12.6
    depends_on:
      - master # Specify the order of service startup
    deploy:
      replicas: 3 # Specify the number of replicas
      restart_policy:
        condition: on-failure
    ports:
      - 5433:5432
    environment:
      POSTGRES_USER: postgres # Specify the database user name
      POSTGRES_PASSWORD: P0stgres # Specify the user password
    volumes:
      - ./replica:/docker-entrypoint-initdb.d
EOF
```

#### レプリケーション設定用スクリプト作成（マスタ側）

- マスタDB側で必要なレプリケーション関連設定を行うスクリプトを作成する。WAL関連の設定値とレプリケーション用ユーザの名前とパスワードは必要に応じて変更する。
  - WAL_LEVEL、MAX_WAL_SENDERS、WAL_KEEP_SEGMENTSパラメータを設定
  - レプリケーション用ユーザからのリモート接続を許可
  - レプリケーション用ユーザを作成

```
# cat <<EOF > ./master/0000_configure_replication_master.sh
#!/bin/bash

# Replication settings
sed -i -e 's/#wal_level = replica/wal_level = replica/g' /var/lib/postgresql/data/postgresql.conf
sed -i -e 's/#max_wal_senders = 10/max_wal_senders = 10/g' /var/lib/postgresql/data/postgresql.conf
sed -i -e 's/#wal_keep_segments = 0/wal_keep_segments = 256/g' /var/lib/postgresql/data/postgresql.conf
echo "host replication replication 0.0.0.0/0 trust" >> "/var/lib/postgresql/data/pg_hba.conf"

# Create users for replication
psql -U postgres -c "CREATE ROLE replication WITH REPLICATION PASSWORD 'replication' LOGIN"
EOF
```

#### レプリケーション設定用スクリプト作成（レプリカ側）

- レプリカDB側で必要なレプリケーション関連設定を行うスクリプトを作成する。
  - データベースを停止
  - データベースファイルを削除
  - マスタDBのバックアップを取得
  - HOT_STANDBYパラメータを設定
  - データベースを起動

```
# cat <<EOF > ./replica/0000_configure_replication_replica.sh
#!/bin/bash

# Database shutdwon
/usr/lib/postgresql/12/bin/pg_ctl -D "/var/lib/postgresql/data" -m fast -w stop
sleep 20

# Delete database directories
rm -rf /var/lib/postgresql/data/*

# Master database backup 
pg_basebackup -R -h master -U replication -D /var/lib/postgresql/data -P

# Replication settings
sed -i -e 's/#hot_standby = on/hot_standby = on/g' /var/lib/postgresql/data/postgresql.conf

# Database startup
/usr/lib/postgresql/12/bin/pg_ctl -D "/var/lib/postgresql/data" -w start
EOF
```

#### サンプルデータロードスクリプト作成（マスタ側）

- PostgreSQLのサンプルデータベースを作成するためのスクリプトを作成する。

```
# cat <<EOF > ./master/0100_load_sample_data.sh
#!/bin/bash

# Loading sample data
pg_restore -U postgres -d dvdrental /docker-entrypoint-initdb.d/dvdrental.tar
EOF
```

#### サンプル資材格納

- デプロイ後にレプリケーション動作を確認するため、サンプル資材をダウンロードし、コンテナから参照可能なディレクトリに配置する。

```
# wget https://sp.postgresqltutorial.com/wp-content/uploads/2019/05/dvdrental.zip
# unzip dvdrental.zip -d ./master/
# rm -f dvdrental.zip
```

#### 構成ファイルコピー

マルチノードでSwarmクラスタを構成している場合、SCPで構成ファイルを他ノードにコピーする。

```
# scp -pr `pwd` <slave-node-ip-address>:`pwd`
```

------

## デプロイ実行

Swarmクラスタにコンテナサービスをデプロイする。

```
# docker stack deploy -c ./docker-compose.yml dvdrental
# docker service ls
ID             NAME                MODE         REPLICAS   IMAGE           PORTS
suosdfmse9sf   dvdrental_master    replicated   1/1        postgres:12.6   *:5432->5432/tcp
xowrzwwtq7sm   dvdrental_replica   replicated   3/3        postgres:12.6   *:5433->5432/tcp
```

# レプリケーション動作確認

#### サンプルデータベース更新

- マスタ側でサンプルデータベースにログインし、テーブルが作成されていることを確認する。
- staffテーブルにレコードを追加し、commitする。

```
# docker ps -a
# docker exec -it <master-container-name> /bin/bash

Container> # su - postgres
Container> $ psql -d dvdrental
Container> dvdrental=# \dp
Container> dvdrental=# select * from staff;
 staff_id | first_name | last_name | address_id |            email             | store_id | active | username |                 password                 |        last_update        |      picture
----------+------------+-----------+------------+------------------------------+----------+--------+----------+------------------------------------------+---------------------------+--------------------
        1 | Mike       | Hillyer   |          3 | Mike.Hillyer@sakilastaff.com |        1 | t      | Mike     | 8cb2237d0679ca88db6464eac60da96345513964 | 2006-05-16 16:13:11.79328 | \x89504e470d0a5a0a
        2 | Jon        | Stephens  |          4 | Jon.Stephens@sakilastaff.com |        2 | t      | Jon      | 8cb2237d0679ca88db6464eac60da96345513964 | 2006-05-16 16:13:11.79328 |
(2 rows)

Container> dvdrental=# insert into staff(staff_id,first_name,last_name,address_id,store_id,username) values(3,'Bob','Ludwig','2','2','Bob');
INSERT 0 1
Container> dvdrental=# commit;
Container> dvdrental=# select * from staff;
 staff_id | first_name | last_name | address_id |            email             | store_id | active | username |                 password                 |        last_update         |      picture
----------+------------+-----------+------------+------------------------------+----------+--------+----------+------------------------------------------+----------------------------+--------------------
        1 | Mike       | Hillyer   |          3 | Mike.Hillyer@sakilastaff.com |        1 | t      | Mike     | 8cb2237d0679ca88db6464eac60da96345513964 | 2006-05-16 16:13:11.79328  | \x89504e470d0a5a0a
        2 | Jon        | Stephens  |          4 | Jon.Stephens@sakilastaff.com |        2 | t      | Jon      | 8cb2237d0679ca88db6464eac60da96345513964 | 2006-05-16 16:13:11.79328  |
        3 | Bob        | Ludwig    |          2 |                              |        2 | t      | Bob      |                                          | 2021-03-24 09:48:55.856577 |
(3 rows)
Container> dvdrental=# \q
Container> $ exit
Container> # exit
```

#### レプリケーション確認

- レプリカ側でサンプルデータベースにログインし、マスタ側のテーブル更新が反映されていることを確認する。
- レプリカ側で更新操作ができないことを確認する。

```
# docker ps -a
# docker exec -it <replica-container-name> /bin/bash
Container> # su - postgres
Container> $ psql -d dvdrental
Container> dvdrental=# \dp
Container> dvdrental=# select * from staff;
Container> dvdrental=# delete from staff where staff_id='3';
ERROR:  cannot execute DELETE in a read-only transaction
Container> dvdrental=# \q
Container> $ exit
Container> # exit
```