2020.04.08

DBクラスタをCentOS8にPostgreSQL12+Patroniで構築してみた

こんにちは。次世代システム研究室のデータベース と Hadoop を担当している M.K. です。 最近はずっとHadoopの検証を行っていましたが、今回はデータベースのテーマに戻ろうと思います。

前に書いた以下のブログ

でも検証しましたが、システムのアーキテクチャーを考える上でデータベースの高可用性を求められることは多いです。

特に更新対象のデータベースを単一障害点にしないことが大事になりますが、ただ単に複数のデータベースを立てれば良いわけではなく、データベース間の同期や死活監視およびフェイルオーバーなどを何でも簡単に完璧に(しかも低コストで)満たすソリューションというのはありません。

 

それで前々から色々試していますが、以前の検証ではMySQL系を試したので、PostgreSQL系でも何かないものかと思って探したところ、Patroniというのを見つけました。

 

Patroniはロードバランサーの一つのHAproxyと組み合わせて比較的簡単に自動フェイルオーバーを実現でき、恐らく日本ではまだあまり使われてなさそうで面白そうだったので、今回はPostgreSQLとPatroniを使ったDBクラスタ構築を検証することにしました。

目次

  1. システム構成
    1. 概要と構成図
  2. 環境準備
    1. 利用技術
    2. サーバースペック
  3. インストール
    1. 参考:CentOS7から8にアップデート
    2. 前準備
    3. PostgreSQL12をインストール
    4. etcdをインストール
    5. Patroniをインストール
  4. DBクラスタ構築&設定
    1. etcdクラスタ設定
    2. PostgreSQL設定
    3. Patroni設定
    4. おまけ:設定でハマったところ
  5. データ同期および自動フェイルオーバー検証
    1. HAproxyインストール&設定
    2. データ同期の検証
    3. 自動フェイルオーバーの検証
  6. まとめ
    1. Patroniの感想
    2. 今後の課題

1. システム構成

1. 概要と構成図

Patroniはetcdという信頼性の高い分散キーバリューストア(KVS)を使い、どのノードのPostgreSQLサーバーが利用可能なマスタかを状態監視して、マスタが故障した際に自動フェイルオーバーをしてくれるものです。Pythonで作られているのも良いですね。

 

PostgreSQLサーバーを複数台立てて、Patroniのサービスも1ノードに1つずつ起動します。各Patroniは同じノードにあるPostgreSQLを監視、etcdを介して状態を伝えます。マスタが故障するとそれがetcdを介して他のPatroniに伝わりマスタ昇格に適したスタンバイをマスタに昇格(フェイルオーバー)させます。

 

アプリケーション側はどのように切り替わったマスタに追随するか(どのノードのPostgreSQLがマスタかを知るか)というと、ロードバランサーのHAproxyを使います。HAproxyに接続して各ノードのPostgreSQLに振り分けてもらうのですが、その際Patroniをチェックして200のステータスを返したノードのみに接続します。Patroniは正常なマスタのノードのみ200を返します。

この仕組みでアプリケーション側は作り込むことなくマスタのみ接続することができるのでとても楽ちんですね。

 

それからPostgreSQL間はどのようにデータ同期するかと言うと、PostgreSQLが持っているストリーミングレプリケーションの機能を使います。昔使っていたPostgreSQL8系の頃は標準機能としてレプリケーションがなく、Slony-Iというトリガーベースのツールを使ってテーブルごとに同期を取っていたりしましたが、9系の頃からストリーミングレプリケーションが標準機能となりだいぶ進化してきました。今回PostgreSQLを使ってみようと思ったきっかけの一つでもあります。

 

システム構成図

PostgreSQL+Patroniクラスター

 

2. 環境準備

1. 利用技術

今回利用するミドルウェアは、以下のものでCentOS8.1で試しました。

  • PostgreSQL 12.2 ⇒現時点の最新の12系!
  • Python 3.6.8
  • python3-psycopg2 2.8.3
  • Patroni 1.6.4 ⇒現時点の最新!
  • etcd 2.3.8 ⇒実は最初はこの時点で最新の3.4.5で試しました。でも諸事情で結局etcd2系に
  • HAProxy 1.8.15

2. サーバースペック

検証に当たりお馴染みの GMO アプリクラウドのサーバーを使いました。スペックは以下です。

  • PostgreSQL+Patroni用のサーバー
    • OS : CentOS 8.1
    • 仮想 CPU : 2
    • メモリ容量 : 8GB
    • ディスク容量 : 160GB
  • HAProxy用のサーバー
    • OS : CentOS 8.1
    • 仮想 CPU : 1
    • メモリ容量 : 4GB
    • ディスク容量 : 80GB

参考までに、GMOアプリクラウドの同じ技術を使ったより汎用的な Z.com クラウドもあるので興味のある方はぜひ。

 

3. インストール

1. 参考:CentOS7から8にアップデート

今回の検証の本筋からは外れますが、自分の勉強も兼ねてCentOS7から8にアップデートしてみたので、そのときの記録を参考までに書いてみました。

CentOS7から8もだいぶ変わっていて、大きな違いとしてはパッケージをインストールする仕組みがyumからdnfに替わります。

 

dnfをインストールしてCentOS7系の最新に

yum -y install epel-release

/etc/yum.repos.d/epel.repoを編集
#--------------------
# baseurlのコメントを外し、
# metalinkをコメント
# --------------------

yum -y install yum-utils

rpmconf -a
package-cleanup --leaves
package-cleanup --orphans
yum -y install dnf --enablerepo=epel

yum -y update

dnfからCentOS8.1にアップデート

dnf -y remove yum yum-metadata-parser
rm -Rf /etc/yum

dnf upgrade
dnf makecache

dnf upgrade -y http://mirror.centos.org/centos/8/BaseOS/x86_64/os/Packages/{centos-release-8.1-1.1911.0.8.el8.x86_64.rpm,centos-gpg-keys-8.1-1.1911.0.8.el8.noarch.rpm,centos-repos-8.1-1.1911.0.8.el8.x86_64.rpm}

dnf upgrade -y epel-release
dnf makecache

rpm -e `rpm -q kernel`
rpm -e --nodeps sysvinit-tools

dnf -y --releasever=8 --allowerasing --setopt=deltarpm=false distro-sync
dnf -y install kernel-core
dnf -y groupupdate "Core" "Minimal Install"

systemctl reboot

Python3.6をインストールして、pythonコマンドとpipコマンドで呼び出すのを3系に切り替える

dnf -y install python36

alternatives --config python
# python3を選択してEnter

update-alternatives --install /usr/bin/pip pip /usr/bin/pip3 1

2. 前準備

systemdに登録された各種サービスのログを永続化できるようにします。それと今回はDNSを使わずホストファイルの設定を行います。

# systemdのログはjournaldが管理しますがデフォルト状態だと再起動するとログが消えてしまうので、永続化するために/var/log/journalディレクトリを作成します。
mkdir /var/log/journal
systemctl restart systemd-journald

# IPアドレスはマスクしてますがもちろん実際は違います
xx.xx.xx.97   kat-patroni-lb
xx.xx.xx.99   kat-patroni01
xx.xx.xx.100  kat-patroni02
xx.xx.xx.101  kat-patroni03
' >> /etc/hosts

3. PostgreSQL12をインストール

PostgreSQLの最新の12系を入れるため、そのためのレポジトリをダウンロードしてきてインストールします。

dnf -y install https://download.postgresql.org/pub/repos/yum/reporpms/EL-8-x86_64/pgdg-redhat-repo-latest.noarch.rpm
# レポジトリ確認
rpm -qi pgdg-redhat-repo

dnf -qy module disable postgresql
dnf -y install postgresql12 postgresql12-server

# HAproxyを入れるサーバーは接続するだけなので、クライアントだけインストールでもOK
dnf -y install postgresql12

4. etcdをインストール

最初dnfでインストールしようとしたものの、どうもレポジトリがないようで上手くいかず、結局etcdのバイナリーを落として展開しました。 バージョン2.3.8にしているのは2系の最新がこれだったためです。

mkdir /var/tmp/etcd
cd /var/tmp/etcd
ETCD_VER=v2.3.8
curl -L https://github.com/etcd-io/etcd/releases/download/${ETCD_VER}/etcd-${ETCD_VER}-linux-amd64.tar.gz -o etcd-${ETCD_VER}-linux-amd64.tar.gz
tar xzvf etcd-${ETCD_VER}-linux-amd64.tar.gz  --strip-components=1
rm -f etcd-${ETCD_VER}-linux-amd64.tar.gz
chown -R root:root /var/tmp/etcd
cd ..
# etcdを今回は/usr配下に以下の名前で配置
mv /var/tmp/etcd /usr/etcd-2.3.8

dnfでインストールしない場合、etcd用のユーザー作成や、サービス起動スクリプト作成とsystemdへの登録は自分で行わないといけなので、それを行います。

GitHubのドキュメントこちらのサイトを参考にしました。

後述していますが、etcdのクラスタ構築のための設定は環境変数で行うので、[Service]のセクションにEnvironmentFile=/etc/sysconfig/etcdと指定し、サービス起動時にその環境変数を書いたファイルを読み込ませるようにしました。

# etcdユーザーとグループを作成
groupadd --system etcd
useradd -s /sbin/nologin --system -g etcd etcd

# etcd用のディレクトリ作成
mkdir -p /var/lib/etcd
mkdir /etc/etcd
chown -R etcd:etcd /var/lib/etcd

# etcdのサービス起動スクリプトを作成
cat <<_EOF_ |tee /etc/systemd/system/etcd.service
[Unit]
Description=etcd service
Documentation=https://github.com/etcd-io/etcd

[Service]
EnvironmentFile=/etc/sysconfig/etcd
Type=notify
User=etcd
ExecStart=/usr/etcd-2.3.8/etcd

[Install]
WantedBy=multi-user.target
_EOF_

chmod 644 /etc/systemd/system/etcd.service

5. Patroniをインストール

もっとも手こずったのがPatroniのインストールです。公式ドキュメントを参考にpipでインストールするやり方を選んだんですが、怒られ続けて一つずつ足りないものがわかって試行錯誤した結果、以下のようにインストールと積み重ねればできました。

dnf -y install gcc
dnf -y install python36-devel
pip install --upgrade setuptools
pip install python-etcd
dnf -y install python3-psycopg2
# etcdを使うので[]でetcdを指定してインストール
pip install patroni[etcd]

 

4. DBクラスタ構築&設定

インストールが終わったらいよいよクラスタを構築するために各種設定を行います。

クラスタ構築&設定には、以下の記事を色々と参考にしました。

クラスタを組むサーバー同士は外部から直接見えないこともあり、今回の検証では先にSELinuxをFirawalldを無効化しておきます。また、今回は省いていますが、本番運用のときはクラスターを組む各ノード間で時刻がズレないように時刻同期の仕組みを入れておきます。CentOS8からはntpではなくchronyを使うようになりました。

1. etcdクラスタ設定

etcdの2系では各種設定を環境変数で行います。インストールのところで前述したように、サービス起動時に読み込ませる環境変数を書いたファイルを用意します。クラスタを組むノードごとに少し設定が違うのでそれぞれ設定を分けます。

1. etcdの設定とサービス起動

クラスタ1:kat-patroni01

cat <<_EOF_ |tee /etc/sysconfig/etcd
ETCD_NAME='etcd01'
ETCD_DATA_DIR='/var/lib/etcd'
ETCD_INITIAL_ADVERTISE_PEER_URLS='http://xx.xx.xx.99:2380'
ETCD_LISTEN_PEER_URLS='http://xx.xx.xx.99:2380'
ETCD_LISTEN_CLIENT_URLS='http://xx.xx.xx.99:2379,http://127.0.0.1:2379'
ETCD_ADVERTISE_CLIENT_URLS='http://xx.xx.xx.99:2379'
ETCD_INITIAL_CLUSTER_TOKEN='etcd-cluster-01'
ETCD_INITIAL_CLUSTER='etcd01=http://xx.xx.xx.99:2380,etcd02=http://xx.xx.xx.100:2380,etcd03=http://xx.xx.xx.101:2380'
ETCD_INITIAL_CLUSTER_STATE='new'
_EOF_

クラスタ2:kat-patroni02

cat <<_EOF_ |tee /etc/sysconfig/etcd
ETCD_NAME='etcd02'
ETCD_DATA_DIR='/var/lib/etcd'
ETCD_INITIAL_ADVERTISE_PEER_URLS='http://xx.xx.xx.100:2380'
ETCD_LISTEN_PEER_URLS='http://xx.xx.xx.100:2380'
ETCD_LISTEN_CLIENT_URLS='http://xx.xx.xx.100:2379,http://127.0.0.1:2379'
ETCD_ADVERTISE_CLIENT_URLS='http://xx.xx.xx.100:2379'
ETCD_INITIAL_CLUSTER_TOKEN='etcd-cluster-01'
ETCD_INITIAL_CLUSTER='etcd01=http://xx.xx.xx.99:2380,etcd02=http://xx.xx.xx.100:2380,etcd03=http://xx.xx.xx.101:2380'
ETCD_INITIAL_CLUSTER_STATE='new'
_EOF_

クラスタ3:kat-patroni03

cat <<_EOF_ |tee /etc/sysconfig/etcd
ETCD_NAME='etcd03'
ETCD_DATA_DIR='/var/lib/etcd'
ETCD_INITIAL_ADVERTISE_PEER_URLS='http://xx.xx.xx.101:2380'
ETCD_LISTEN_PEER_URLS='http://xx.xx.xx.101:2380'
ETCD_LISTEN_CLIENT_URLS='http://xx.xx.xx.101:2379,http://127.0.0.1:2379'
ETCD_ADVERTISE_CLIENT_URLS='http://xx.xx.xx.101:2379'
ETCD_INITIAL_CLUSTER_TOKEN='etcd-cluster-01'
ETCD_INITIAL_CLUSTER='etcd01=http://xx.xx.xx.99:2380,etcd02=http://xx.xx.xx.100:2380,etcd03=http://xx.xx.xx.101:2380'
ETCD_INITIAL_CLUSTER_STATE='new'
_EOF_

クラスタ1-3:kat-patroni01-03

設定準備が整ったら、etcdのサービスを有効化して起動します。クラスタ1のノードでetcdを起動したら他のクラスタのetcdが起動するまで待ち状態になるので、続けてクラスタ2と3でも起動します。

systemctl daemon-reload
systemctl enable etcd
systemctl start etcd

2. etcdのクラスタの挙動確認

クラスタが正しく組めているか確認します。クラスタ1のetcdに書き込んだ内容が他のクラスタのetcdで参照できるかをみてみます。

クラスタ1:kat-patroni01

# "Hellow World"という文字を/messageに書き込む
/usr/etcd-2.3.8/etcdctl set /message "Hello World"

クラスタ2-3:kat-patroni02-03

/usr/etcd-2.3.8/etcdctl get /message

“Hello World”が返ってきたらクラスタがきちんと組めていることがわかるので、これでetcdクラスタ構築は完了です!

2. PostgreSQL設定

etcdの次にPostgreSQLを設定します。データベースはインストールしてまずやることは初期化ですね。しかし、一番ハマったところなんですが、結論から言うとPostgreSQLの初期化や設定はPatroniの設定でまとめて行う方が良いです。

参考までに、PostgreSQLのレポジトリからdnfでインストールすると、postgresユーザーとグループが自動で作成され、postgresユーザーのホームディレクトリが/var/lib/pgsqlになります。PostgreSQLのデータディレクトリ(PGDATA)も/var/lib/pgsql/12/dataに決められて、.bash_profileも作られます。PostgreSQL起動スクリプトも作られてsystemdにサービス登録もされます。

とても便利なんですが、色々決め打ちされるのでデータディレクトリを変更すると思わぬ影響を受けるところもありました。。それもあり今回はデータディレクトリはそのままにしています。

 

この時点ではPostgreSQLの更新ログであるWALをアーカイブする先のディレクトリだけ作っておきます。WALアーカイブは必須ではないですが、本番運用のときにポイントタイムリカバリをする際には必要なので今回の検証ではアーカイブモードを有効にしました。場所はどこでも良いですが、データディレクトリと同じところにarchiveというディレクトリを作りました。

mkdir /var/lib/pgsql/12/archive
chown postgres:postgres /var/lib/pgsql/12/archive
chmod 700 /var/lib/pgsql/12/archive

3. Patroni設定

いよいよPatroniの設定です。まずサービス起動スクリプトを作成します。

クラスタ1-3:kat-patroni01-03

cat <<_EOF_ |tee /etc/systemd/system/patroni.service
[Unit]
Description=Runners to orchestrate a high-availability PostgreSQL
After=syslog.target network.target
[Service]
Type=simple
User=postgres
Group=postgres
ExecStart=/usr/local/bin/patroni /etc/patroni.yml
KillMode=process
TimeoutSec=30
Restart=no
[Install]
WantedBy=multi-user.target
_EOF_

chmod 644 /etc/systemd/system/patroni.service
systemctl enable patroni

サービス起動スクリプトが準備できたら、その中にで指定されているPatroniの設定ファイル(yaml)を準備します。

クラスタ1:kat-patroni01

cat <<_EOF_ |tee /etc/patroni.yml
scope: postgres-cluster
namespace: /db/
name: postgresql-patroni01
restapi:
  listen: 0.0.0.0:8008
  connect_address: xx.xx.xx.99:8008
etcd:
        hosts: 'xx.xx.xx.99:2379,xx.xx.xx.100:2379,xx.xx.xx.101:2379'
bootstrap: #Patroniが最初に行う諸々の設定
    dcs: #クラスタ構築関連の設定
        ttl: 30
        loop_wait: 10
        retry_timeout: 10
        maximum_lag_on_failover: 1048576
        synchronous_mode: on          #同期モードの設定
        synchronous_mode_strict: true #同期モードの設定
        postgresql:
            use_pg_rewind: true          #レプリケーションの設定
            use_slots: true              #レプリケーションの設定
            parameters:
                wal_level: replica       #レプリケーションの設定
                hot_standby: on          #レプリケーションの設定
                wal_keep_segments: 8     #レプリケーションの設定
                max_wal_senders: 5       #レプリケーションの設定
                max_replication_slots: 5 #レプリケーションの設定
                checkpoint_timeout: 30   #レプリケーションの設定
                synchronous_commit: on         #同期モードの設定
                synchronous_standby_names: '*' #同期モードの設定
                archive_mode: on     #アーカイブモードの設定
                archive_command: 'test ! -f /var/lib/pgsql/12/archive/%f && cp %p /var/lib/pgsql/12/archive/%f' #アーカイブモードの設定
                archive_timeout: 300 #アーカイブモードの設定
    initdb: #PostgreSQLの初期化
    - encoding: UTF8
    - data-checksums
    pg_hba: #PostgreSQLへのアクセス権限設定
    - host replication replicator 127.0.0.1/32 md5
    - host replication replicator xx.xx.xx.99/32 md5
    - host replication replicator xx.xx.xx.100/32 md5
    - host replication replicator xx.xx.xx.101/32 md5
    - local all all md5
    - host all all xx.xx.xx.0/24 md5
    users: #PostgreSQLユーザーの作成
        admin:
            password: adm-xxxxx
            options:
            - createrole
            - createdb
        replicator:
            password: rep-xxxxx
            options:
            - replication
postgresql: #PostgreSQlの追加設定(主に接続/動的パラメータ)
    listen: xx.xx.xx.99:5432
    connect_address: xx.xx.xx.99:5432
    data_dir: /var/lib/pgsql/12/data
    config_dir: /var/lib/pgsql/12/data
    bin_dir: /usr/pgsql-12/bin
    pgpass: /var/lib/pgsql/pgpass
    authentication: #PostgreSQLユーザーへの接続情報
        replication:
            username: replicator
            password: rep-xxxxx
        superuser:
            username: postgres
            password: pos-xxxxx
    parameters:
        unix_socket_directories: /var/run/postgresql
        shared_buffers: 2GB
        work_mem: 8MB
        effective_cache_size: 4GB
        autovacuum: on
        track_counts: on
tags: #Patroniのクラスタ操作の設定
    nofailover: false
    noloadbalance: false
    clonefrom: false
    nosync: false
log:
    level: INFO
_EOF_

ポイント

Patroniの設定は項目が多くて全部をしっかり理解するのは簡単ではないですが、少なくともどのセクションが何のための設定かは理解しないとなかなかうまくいきませんでした。上記のサンプルにセクションごとに何のための設定か簡単にコメントを付けましたが、少しずつ理解しながら設定した感じです。まだ全部の項目を理解できているわけでないのと、恐らく不要な記述もありそうなので、実際使うことになったらもっと深く見てみようと思います。

今回はPostgreSQLのレプリケーションを同期モードにする設定にしました。これは、PostgreSQL+Patroniのクラスタを利用したいケースが、大量トランザクションを捌くような性能を求めているのではなく、更新先データベースの高可用性を比較的容易に担保できるケースを想定しているためです。例えば、Hadoopクラスタを組むとき幾つかのサービスは実はデータベースを利用するのですが、そのデータベースを単一障害点(スタンドアロン構成)にしないようなケースです。

クラスタ2:kat-patroni02

クラスタ2の/etc/patroni.ymlの内容はクラスタ1の設定ファイルから以下の項目だけを変更して他は同じにしたまま準備します。

name: postgresql-patroni02
restapi:
  connect_address: xx.xx.xx.100:8008
etcd:
        hosts: xx.xx.xx.100:2379,xx.xx.xx.101:2379,xx.xx.xx.99:2379
postgresql:
    listen: xx.xx.xx.100:5432
    connect_address: xx.xx.xx.100:5432

クラスタ3:kat-patroni03

クラスタ3の/etc/patroni.ymlの内容もクラスタ2と同様に以下の項目だけを変更します。

name: postgresql-patroni03
restapi:
  connect_address: xx.xx.xx.101:8008
etcd:
        hosts: xx.xx.xx.101:2379,xx.xx.xx.99:2379,xx.xx.xx.100:2379
postgresql:
    listen: xx.xx.xx.101:5432
    connect_address: xx.xx.xx.101:5432

クラスタ1-3:kat-patroni01-03

Patroniの設定ファイルまで準備できたら、クラスタ1から順にPatroniを起動します。

# Patroni&amp;PostgreSQL起動
systemctl start patroni
# Patroniの状況確認
systemctl status --no-pager patroni

# Patroniのクラスタ状況確認
patronictl -c /etc/patroni.yml list

状況確認して問題なければ、これでようやくPatroniを使ったPostgreSQLクラスタの構築が完了です!

4. おまけ:設定でハマったところ

etcd、Patroni、PostgreSQLとそれぞれ設定項目が多く、理解して上手くいくまで手こずりました・・。それでも特にハマったことは、etcdのバージョン問題とPostgreSQLの初期化&ユーザー作成のところです。

今回はなるべく最新のものを組み合わせることを念頭に置いていたので、当初はetcdの最新版の3.4.5をインストールしていました。順調に構築を進めて最後にPatroniの設定をしてさあ起動というときにエラーで怒られてしまい、原因を詳しく調査したらPatroniの最新版の1.6.4は実はetcdの2系のAPIまでしか対応していなかったことがわかりました。2系のAPIが使えるようなオプションを探して試してみましたが上手くいかず、結局etcdを2系の最新の2.3.8に入れ替えることで解決しました。

もう一つ、PostgreSQLの初期化&ユーザー作成については、初めはPostgreSQLの初期化コマンド(/usr/pgsql-12/bin/postgresql-12-setup initdb)を使って初期化してからPatroniの設定をしていたんですが、どうしてもPatroniが上手く起動しませんでした。詳しく原因を調査すると、PostgreSQLのデータディレクトリがあるとどうやら、Patroniのbootstrapで行うPostgreSQLの処理がこちらの想定通りには行われないようでそれで上手くいっていませんでした。そこで一度PostgreSQLのデータディレクトリを削除し、今度はPostgreSQLの初期化をPatroniで行うようにしました。

これはこれで上手くいったんですがまた別のエラーが出てしまい、これも原因を調査していくと、Patroniの設定ファイルに、PostgreSQLのレプリケーション用のユーザーを作成する設定がなかったことに気が付いて、この設定を追記することで上手く行きました!

bootstrapセクションにあるusersの次の項目ですね。postgresqlセクションにあるauthenticationの項目と紛らわしいので要注意です。

    users: #PostgreSQLユーザーの作成

        replicator:
            password: rep-xxxxx
            options:
            - replication

上手く行くと、Patroniのサービスを起動したときに同時にPostgreSQLのデータベースも起動します。Patroniを使うときは、PostgreSQLの制御は全てPatroniに行わせた方が良さそうです。

 

5. データ同期および自動フェイルオーバー検証

クラスタ構築が終わったら、そのクラスタへの接続を制御できるようにHAProxyのインストールと設定を行って、一番の目的である自動フェイルオーバーを検証してみます。

1. HAproxyインストール&設定

クライアント:kat-patroni-lb

クラスタに接続するため用意したサーバーにHAProxyをインストールして、設定ファイルを作成します。

# HAProxyのインストール
dnf -y install haproxy

# HAProxyの設定
cat <<_EOF_ |tee /etc/haproxy/haproxy.cfg
global
    maxconn 100

defaults
    log global
    mode tcp
    retries 2
    timeout client 30m
    timeout connect 4s
    timeout server 30m
    timeout check 5s

listen stats
    mode http
    bind *:7000
    stats enable
    stats uri /

listen postgres
    bind *:5000
    option httpchk
    http-check expect status 200
    default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
    server patroni01_5432 xx.xx.xx.99:5432 maxconn 100 check port 8008
    server patroni02_5432 xx.xx.xx.100:5432 maxconn 100 check port 8008
    server patroni03_5432 xx.xx.xx.101:5432 maxconn 100 check port 8008
_EOF_

上記設定の意味は、ローカルホストの5000番ポートに接続すると、クラスタの中で利用可能なPostgreSQL1台(マスタ)にだけ繋いでくれるというものです。check port 8008と書かれている箇所がありますが、これはPatroniのAPIのポートにステータスをチェックしている箇所です。マスタになっているPostgreSQLがあるサーバーだけ200ステータスを返すので、このような振り分けができる仕組みとなっています。

設定ができたらHAProxyのサービスを有効化して起動します。

systemctl enable haproxy
systemctl start haproxy

2. データ同期の検証

フェイルオーバーの検証を行う前に、先ずはそもそもPostgreSQL同士がちゃんとデータ同期されるかを検証します。

HAProxyが起動しているサーバーで、PostgreSQLに接続します。

クライアント:kat-patroni-lb

psql -h localhost -p 5000 -U postgres

postgresユーザーのパスワードを聞かれるので、Patroniの設定ファイルでpostgresqlセクションのauthenticationのところに書いたパスワード(pos-xxxxx)を入力します。

HAProxy経由でPostgreSQLのマスタに繋がったら以下のCREATE文を実行、作成したhauserユーザーとhadbデータベースに全てのPostgreSQLから接続できるかを確認しました。

CREATE ROLE hauser WITH LOGIN PASSWORD 'xyz-xxxxx';
CREATE DATABASE hadb OWNER hauser;

クラスタ1-3:kat-patroni01-03

HAProxyを使わずクラスタ1-3のサーバーに直接入ってから、それぞれのサーバーで起動しているPostgreSQLへhauserユーザーとしてhadbデータベースに接続してみます。

# クラスタ1で接続するとき
psql -h xx.xx.xx.99 -p 5432 -U hauser -d hadb
# クラスタ2で接続するとき
psql -h xx.xx.xx.100 -p 5432 -U hauser -d hadb
# クラスタ3で接続するとき
psql -h xx.xx.xx.101 -p 5432 -U hauser -d hadb

いずれも成功したので、PostgreSQL間でデータ同期が取れていることが確認できました!

3. 自動フェイルオーバーの検証

それではいよいよ肝心のフェイルオーバーを検証します。

先ずどのPostgreSQLがマスタなのかを確認します。HAProxyを経由してどこに接続されたかがわかればそれがマスタということになります。

クライアント:kat-patroni-lb、クラスタ1-3:kat-patroni01-03

HAProxyを経由して接続している間、クラスタ1-3のサーバーに直接入ってPostgreSQLに繋ぎ、以下のSQLを実行してHAProxyのあるIPアドレスからのセッションが返ってきたらそこがマスタです。

SELECT pid, usename, client_addr, client_port, state, query
FROM pg_stat_activity
WHERE state != '' AND pid != pg_backend_pid();

今回の構築手順を行えば大抵はクラスタ1のPostgreSQLがマスタとなっていると思いますので、クラスタ1をサーバーごと停止し、自動フェイルオーバーが行われてクラスタ2か3のPostgreSQLがマスタに昇格され、HAProxyを経由して接続した際に新しいマスタに繋がるかを検証します。

  1. 上記のSQLでクラスタ1のPostgreSQL(マスタ)に接続できていることを確認
  2. GMOアプリクラウドのコンパネから、クラスタ1のサーバー(kat-patroni01)を停止
  3. HAProxy経由で再度PostgreSQLに接続し、クラスタ2か3のサーバーに接続できていることを上記SQLで確認

この手順で行った結果、クラスタ2のPostgreSQLがマスタに昇格されて、HAProxy経由で新しいマスタに接続できていることが確認できました!

マスタのPostgreSQLしかWALのアーカイブログは吐かないんですが、クラスタ2のサーバーの/var/lib/pgsql/12/archiveを見るとアーカイブログができていました。

最後に、この状態で新しいマスタに繋いでCREATE TABLE文を実行し、クラスタ3のPostgreSQLに同期されているかも確認してみます。

クライアント:kat-patroni-lb

HAProxy経由でPostgreSQL(新しいマスタ:クラスタ2)に接続し、以下のCREATE TABLE文を実行

CREATE TABLE films (
    code        char(5),
    title       varchar(40),
    did         integer,
    date_prod   date,
    kind        varchar(10),
    len         interval hour to minute,
    CONSTRAINT code_title PRIMARY KEY(code,title)
);

クラスタ2-3:kat-patroni02-03

クラスタ2-3のサーバーに直接入ってPostgreSQLに繋ぎ、作成したfilmsテーブルができていることを確認します。

\d films;

無事にfilmsテーブルをクラスタ3でも確認することができました。

これで最初マスタだったクラスタ1のPostgreSQLが倒れても、クラスタ2のPostgreSQLが自動フェイルオーバーでマスタに昇格し、クラスタ2とクラスタ3の間でレプリケーションが継続されていることも検証できました!

 

最後にもう一つ、停止したクラスタ1のサーバーを起動し直して、クラスタ1のPostgreSQLをPatroniのクラスタに戻すフェイルバックをやってみます。

実はとても簡単で、今回の構築手順を行っていればサーバーを起動するとPatroniも自動的に立ち上がってフェイルバックを自動的にやってくれました!

マスタはクラスタ2のPostgreSQLのままで、復帰したクラスタ1のPostgreSQLはスレーブとしてレプリケーションされていて、クラスタ1が停止しているときに作成したfilmsテーブルもクラスタ1のPostgreSQLで確認することができました。

クラスタ1:kat-patroni01

参考までにPatroniのクラスタ状態を確認すると以下のようになります。クラスタ2のPostgreSQLがマスタで、クラスタ1のPostgreSQLもクラスタに復帰できていることがわかります。

patronictl -c /etc/patroni.yml list
# 結果
+------------------+----------------------+---------------+--------------+---------+----+-----------+
|     Cluster      |        Member        |      Host     |     Role     |  State  | TL | Lag in MB |
+------------------+----------------------+---------------+--------------+---------+----+-----------+
| postgres-cluster | postgresql-patroni01 |  xx.xx.xx.99 |              | running |  2 |         0 |
| postgres-cluster | postgresql-patroni02 | xx.xx.xx.100 |    Leader    | running |  2 |           |
| postgres-cluster | postgresql-patroni03 | xx.xx.xx.101 | Sync Standby | running |  2 |         0 |
+------------------+----------------------+---------------+--------------+---------+----+-----------+

注意点としては、etcdは今回の構築手順ではサーバーを起動しても自動的には立ち上がりませんでした。etcdも3台でクラスタを構成しているため1台落ちていたも問題なかったです。 以下のコマンドで起動したらフェイルバックは完了です。

フェイルバックもとても簡単ですね。本番運用のときにはetcdも自動起動するようにしておくと良いと思いました。

# etcd起動
systemctl start etcd
# etcdのクラスタを確認
/usr/etcd-2.3.8/etcdctl member list
# 結果
b125cb142c522b4: name=etcd03 peerURLs=http://xx.xx.xx.101:2380 clientURLs=http://xx.xx.xx.101:2379 isLeader=true
91c55248feceb0b6: name=etcd02 peerURLs=http://xx.xx.xx.100:2380 clientURLs=http://xx.xx.xx.100:2379 isLeader=false
ab52793fbec2ca29: name=etcd01 peerURLs=http://xx.xx.xx.99:2380 clientURLs=http://xx.xx.xx.99:2379 isLeader=false

 

6. まとめ

1. Patroniの感想

正直に言ってPatroniは結構使える気がします。Patroniの設定を覚えるのはちょっと大変ですが、PatroniだけでPostgreSQL同士のレプリケーションや自動フェイルオーバーを制御できるのはかなり強力です。

今後新たにHadoopクラスタを組むことがあったら、Hiveが使うデータベースなどにPostgreSQL+Patroniのクラスターを利用してみようと思います。

2. 今後の課題

今回やれなかったこととして、

  • Patroniの設定項目をもっと深追いする
  • PostgreSQLが2台倒れたらどうなるか
  • 同期モードのレプリケーションについてもっと検証(特に性能面)

といった検証課題が残っています。

本番運用を見据えると、これらの課題はクリアしないといけないので、いつかどこかで試そうと思います。

最後に

次世代システム研究室では、データサイエンティスト/機械学習エンジニアを募集しています。ビッグデータの解析業務など次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ 募集職種一覧 からご応募をお願いします。

 

皆さんのご応募をお待ちしています。

  • Twitter
  • Facebook
  • はてなブックマークに追加

グループ研究開発本部の最新情報をTwitterで配信中です。ぜひフォローください。

 
  • AI研究開発室
  • 大阪研究開発グループ

関連記事