😎

Patroniを利用したDBクラスタを構築してみた

2022/11/09に公開約16,300字

はじめに

今回はPatroniを利用したDBクラスタについて書いていきたいと思います。
それぞれの機能について簡単に説明をしたいと思います。

Patroniとは

あまり聞き慣れている方はいないかもしれません。
Patroni(パトローニ)はetcd(etc distributed)とHAproxyと組み合わせて、
PostgreSQLの自動フェイルオーバー構成を実現することができます。

よくPostgreSQLで知られているのはPgpool-IIを利用したHA構成かなと思いますが、
バージョン4.2から動作モード設定が変わったり、
Watchdogが変わったり、マニュアルをよく見ると用語が今までと変わっていたりするので、
覚えなおしかぁと思ってしまいめんどくさいと思って(よくないんだけどね)、
もっと簡単にクラスタ組める方法ないかなと探していたら、Patroniと出会いました。

etcdとは

Go言語で記述された設定情報の共有とサービス検出ための分散KVS(分散Key-Valueストア)になります。
※kubernetesにも使われてます。
どんな機能か簡単に説明すると、サーバAで設定した値を入力したら、サーバーBにも更新され値を参照することができる機能になります。
必ずリーダーが存在し、heartbeatsを送り同期をとるような仕組みとなっており、リーダーがいなくなった場合には他のetcdサーバーがリーダーになります。
なお、リーダーになるためには過半数の満たさないとリーダーになることはできません。
そのため、etcd実際に本番運用する際には3ノードが最低基準で、クラスターを組む際には奇数で組みましょう。
2ノードでも構築はできますが、1台でも壊れるとクラスターが機能しなくなりますのでお勧めできません。

今回構築する際にはDB側にhaproxy/etcd/Patroni/PostgreSQLを同梱して構築します。
※本当ならhaproxyぐらいは別立てしたいのですがリソースに余裕がないので、同梱しています。
kubernetesの参考PAGEより

HAproxyとは

LBややReverse Proxyとして利用でき、事前に書いた定義にしたがって、HTTPリクエストの内容やTCPパケットの内容に応じた挙動をさせることができます。
今回はこのHAproxyを冗長化するにあたってKeepaliveを利用しております。

構築にあたり準備したもの

RHELが無料で15台(個人利用として)まで利用できるようになったので、Red Hat Developer Programでアカウントを作り、
RHEL8.3で構築を行なっております。

サーバーは合計3台でDB01/DB02/DB03を用意しています。
事前にPKGのUpdateやSELinux、Firewallの設定などは無効化して実施しています。
またいくつかソースインストールするにあたりDevelopment Toolsが必要になってくるので、
インストールを実施しています。

RHELは事前にsubscription-managerを利用してアカウントを登録しないとPKGのUpdateができないので、
必ず登録を行なった上で実施するようにして下さい。

DB01:192.168.13.45
DB02:192.168.13.46
DB03:192.168.13.47
DB-VIP:192.168.13.48

etcdのインストール

今回etcdは同時最新の3.4.15をソースからインストールしているので、serviceコマンドも作っています。
v2モードを有効にしている理由としては、Patroniがetcd v3をサポートしていないのでv2を有効にしています。
※構築した当初はv2モード無効のままで実施したら、Patroniが動きませんでした。

[DB01/DB02/DB03]

# ETCD_VER=v3.4.15
# 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 xvzf etcd-v3.4.15-linux-amd64.tar.gz
# cd etcd-v3.4.15-linux-amd64/
# cp etcd etcdctl /usr/local/bin/

[DB01]

# vi /etc/systemd/system/etcd.service
[Unit]
Description=etcd service
Documentation=https://github.com/coreos/etcd
 
 [Service]
Type=notify
ExecStart=/usr/local/bin/etcd --enable-v2=true --name=etcd0 --data-dir=/var/lib/etcd/default.etcd --initial-advertise-peer-urls=http://192.168.13.45:2380 --listen-peer-urls=http://192.168.13.45:2380 --listen-client-urls=http://192.168.13.45:2379,http://127.0.0.1:2379 --advertise-client-urls=http://192.168.13.45:2379 --initial-cluster-token=etcd-cluster-1 --initial-cluster=etcd0=http://192.168.13.45:2380,etcd1=http://192.168.13.46:2380,etcd2=http://192.168.13.47:2380 --initial-cluster-state=new
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target

[DB02]

# vi /etc/systemd/system/etcd.service
[Unit]
Description=etcd service
Documentation=https://github.com/coreos/etcd
 
[Service]
Type=notify
ExecStart=/usr/local/bin/etcd --enable-v2=true --name=etcd1 --data-dir=/var/lib/etcd/default.etcd --initial-advertise-peer-urls=http://192.168.13.46:2380 --listen-peer-urls=http://192.168.13.46:2380 --listen-client-urls=http://192.168.13.46:2379,http://127.0.0.1:2379 --advertise-client-urls=http://192.168.13.46:2379 --initial-cluster-token=etcd-cluster-1 --initial-cluster=etcd0=http://192.168.13.45:2380,etcd1=http://192.168.13.46:2380,etcd2=http://192.168.13.47:2380 --initial-cluster-state=new
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target

[DB03]

# vi /etc/systemd/system/etcd.service
[Unit]
Description=etcd service
Documentation=https://github.com/coreos/etcd
 
[Service]
Type=notify
ExecStart=/usr/local/bin/etcd --enable-v2=true  --name=etcd2 --data-dir=/var/lib/etcd/default.etcd --initial-advertise-peer-urls=http://192.168.13.47:2380 --listen-peer-urls=http://192.168.13.47:2380 --listen-client-urls=http://192.168.13.47:2379,http://127.0.0.1:2379 --advertise-client-urls=http://192.168.13.47:2379 --initial-cluster-token=etcd-cluster-1 --initial-cluster=etcd0=http://192.168.13.45:2380,etcd1=http://192.168.13.46:2380,etcd2=http://192.168.13.47:2380 --initial-cluster-state=new
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target

インストールと起動スクリプト作成したらetcdを起動し、問題ないことを確認します。

[DB01/DB02/DB03]

# systemctl restart etcd 
# etcdctl endpoint status --cluster -w table
+---------------------------+------------------+---------+---------+-----------+------------+-----------+------------+--------------------+--------+
|         ENDPOINT          |        ID        | VERSION | DB SIZE | IS LEADER | IS LEARNER | RAFT TERM | RAFT INDEX | RAFT APPLIED INDEX | ERRORS |
+---------------------------+------------------+---------+---------+-----------+------------+-----------+------------+--------------------+--------+
| http://192.168.13.46:2379 | 1b42b7885cd4db54 |  3.4.15 |   16 kB |      true |      false |       489 |         12 |                 12 |        |
| http://192.168.13.47:2379 | 1bf5525ed52f1ade |  3.4.15 |   20 kB |     false |      false |       489 |         12 |                 12 |        |
| http://192.168.13.45:2379 | 8aa82c2cfc4db9db |  3.4.15 |   20 kB |     false |      false |       489 |         12 |                 12 |        |
+---------------------------+------------------+---------+---------+-----------+------------+-----------+------------+--------------------+--------+
# systemctl enable etcd

PostgreSQLとPatroniのインストール

PostgreSQLのリポジトリファイルをインストールして、PostgreSQL13をインストールします。
ポイントタイムリカバリをする際に必要なWALアーカイブフォルダも作成します。

[DB01/DB02/DB03]

# dnf -y install https://download.postgresql.org/pub/repos/yum/reporpms/EL-8-x86_64/pgdg-redhat-repo-latest.noarch.rpm
# dnf -qy module disable postgresql
# dnf -y install postgresql13 postgresql13-server

# mkdir /var/lib/pgsql/13/archive
# chown postgres:postgres /var/lib/pgsql/13/archive
# chmod 700 /var/lib/pgsql/13/archive

Patroniのインストールをし、serviceコマンドも作っています。

[DB01/DB02/DB03]

# dnf -y install python36-devel
# pip3 install --upgrade setuptools
# pip3 install python-etcd
# dnf -y install python3-psycopg2
# pip3 install patroni[etcd]

# vi /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

# systemctl enable patroni

Patroniの設定ファイルを作ります。PostgreSQLの設定もPatroni側で全て記載します。
ここが一番苦労し、サンプルファイルとドキュメントをみながら作成しました。

[DB01]

# cat /etc/patroni.yml
scope: postgres-cluster
namespace: /db/
name: postgresql-patroni01
restapi:
  listen: 0.0.0.0:8008
  connect_address: 192.168.13.45:8008
etcd:
        hosts: '192.168.13.45:2379,192.168.13.46:2379,192.168.13.47: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/13/archive/%f && cp %p /var/lib/pgsql/13/archive/%f' 
                archive_timeout: 300 
    initdb: #PostgreSQLの初期化
    - encoding: UTF8
    - data-checksums
    pg_hba: 
    - host replication replicator 127.0.0.1/32 md5
    - host replication replicator 192.168.13.45/32 md5
    - host replication replicator 192.168.13.46/32 md5
    - host replication replicator 192.168.13.47/32 md5
    - local all all md5
    - host all all 192.168.13.0/24 md5
    users: 
        admin:
            password: adm-postgres
            options:
            - createrole
            - createdb
        replicator:
            password: rep-postgres
            options:
            - replication
postgresql: 
    listen: 192.168.13.45:5432
    connect_address: 192.168.13.45:5432
    data_dir: /var/lib/pgsql/13/data
    config_dir: /var/lib/pgsql/13/data
    bin_dir: /usr/pgsql-13/bin
    pgpass: /var/lib/pgsql/pgpass
    authentication: 
        replication:
            username: replicator
            password: rep-postgres
        superuser:
            username: postgres
            password: pos-postgres
    parameters:
        unix_socket_directories: /var/run/postgresql
        shared_buffers: 2GB
        work_mem: 8MB
        effective_cache_size: 4GB
        autovacuum: on
        track_counts: on
tags: 
    nofailover: false
    noloadbalance: false
    clonefrom: false
    nosync: false
log:
    level: INFO

[DB02] ※長くなるので差分だけ記載しています。

name: postgresql-patroni02

restapi:
  connect_address: 192.168.13.46:8008

etcd:
        hosts: '192.168.13.46:2379,192.168.13.47:2379,192.168.13.45:2379'

postgresql:
    listen: 192.168.13.46:5432
    connect_address: 192.168.13.46:5432

[DB03] ※長くなるので差分だけ記載しています。

name: postgresql-patroni03

restapi:
  connect_address: 192.168.13.47:8008

etcd:
        hosts: '192.168.13.47:2379,192.168.13.45:2379,192.168.13.46:2379'

postgresql:
    listen: 192.168.13.47:5432
    connect_address: 192.168.13.47:5432

Patroniを起動し、PostgreSQLのCluster状況を確認します。
今回3台構成なので、Leader/Replica/Sync Standbyになっていることを確認します。

[DB01/DB02/DB03]

# systemctl start patroni
# patronictl -c /etc/patroni.yml list
+ Cluster: postgres-cluster (6946573433626602279) ----+---------+----+-----------+
| Member               | Host          | Role         | State   | TL | Lag in MB |
+----------------------+---------------+--------------+---------+----+-----------+
| postgresql-patroni01 | 192.168.13.45 | Leader       | running |  5 |           |
| postgresql-patroni02 | 192.168.13.46 | Replica      | running |  4 |         0 |
| postgresql-patroni03 | 192.168.13.47 | Sync Standby | running |  5 |         0 |
+----------------------+---------------+--------------+---------+----+-----------+

keepalivedとHAproxyのインストール

keepalivedのインストールを実施し、serviceコマンドも作っています。

[DB01/DB02/DB03]

# dnf -y install openssl-devel
# wget https://www.keepalived.org/software/keepalived-2.2.2.tar.gz
# tar xvzf keepalived-2.2.2.tar.gz 
# cd keepalived-2.2.2/
# ./configure 
# make
# vi /etc/systemd/system/keepalived.service
[Unit]
Description=LVS and VRRP High Availability Monitor
After=network-online.target syslog.target
Wants=network-online.target
[Service]
Type=forking
PIDFile=/run/keepalived.pid
KillMode=process
EnvironmentFile=-/usr/local/etc/sysconfig/keepalived
ExecStart=/usr/local/sbin/keepalived $KEEPALIVED_OPTIONS
ExecReload=/bin/kill -HUP $MAINPID
[Install]
WantedBy=multi-user.target

keepalivedのconfigを設定していきます。

[DB01/DB02/DB03]

[root@zbx5-db01 ~]# mkdir /etc/keepalived
# vi /etc/keepalived/keepalived.conf
global_defs {
}
vrrp_script chk_haproxy { 
    script "killall -0 haproxy" 
    interval 2 
    weight 2 

vrrp_instance VI_1 {
    interface ens192 #Interfaceは適切に修正してください
    state MASTER 
    priority 101 #Priorityは全て同じするとエラーが起きます。値が大きいものが優先されてVIPが割り当てられます。
    virtual_router_id 51
    authentication {
        auth_type PASS
        auth_pass 1234
    }
    virtual_ipaddress {
        192.168.13.48 
    }
    track_script {
        chk_haproxy
    }

keepalivedを起動し、一番Priorityが高いノードでVIPが割り当てられているか確認します。

# systemctl start keepalived
# systemctl enable keepalived
# ip a
2: ens192: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 00:0c:29:8e:ba:15 brd ff:ff:ff:ff:ff:ff
    inet 192.168.13.45/24 brd 192.168.13.255 scope global noprefixroute ens192
       valid_lft forever preferred_lft forever
    inet 192.168.13.48/32 scope global ens192
       valid_lft forever preferred_lft forever

HAproxyのインストール

HAproxyのインストールを実施してきます。

[DB01/DB02/DB03]

# wget https://www.haproxy.org/download/2.2/src/haproxy-2.2.12.tar.gz
# tar zxvf haproxy-2.2.12.tar.gz 
# cd haproxy-2.2.12/
# make TARGET=linux-glibc #利用するOSによって変わってくるので、適切なもので実行して下さい。

# mkdir /etc/haproxy
# mkdir /var/lib/haproxy
# touch /var/lib/haproxy/stats

# vi /etc/systemd/system/haproxy.service
[Unit]
Description=HAProxy Load Balancer
Documentation=man:haproxy(1)
Documentation=file:/usr/share/doc/haproxy/configuration.txt.gz
After=network.target syslog.service
Wants=syslog.service
 
[Service]
Environment="CONFIG=/etc/haproxy/haproxy.cfg" "PIDFILE=/run/haproxy.pid"
ExecStartPre=/usr/local/sbin/haproxy -f $CONFIG -c -q
ExecStart=/usr/local/sbin/haproxy -W -f $CONFIG -p $PIDFILE $EXTRAOPTS
ExecReload=/usr/local/sbin/haproxy -f $CONFIG -c -q $EXTRAOPTS $RELOADOPTS
ExecReload=/bin/kill -USR2 $MAINPID
KillMode=mixed
#Restart=always
#Type=forking
Restart=on-failure
RestartSec=5
 
[Install]
WantedBy=multi-user.target

# vi /etc/haproxy/haproxy.cfg
[Unit]
Description=HAProxy Load Balancer
Documentation=man:haproxy(1)
Documentation=file:/usr/share/doc/haproxy/configuration.txt.gz
After=network.target syslog.service
Wants=syslog.service
 
[Service]
Environment="CONFIG=/etc/haproxy/haproxy.cfg" "PIDFILE=/run/haproxy.pid"
ExecStartPre=/usr/local/sbin/haproxy -f $CONFIG -c -q
ExecStart=/usr/local/sbin/haproxy -W -f $CONFIG -p $PIDFILE $EXTRAOPTS
ExecReload=/usr/local/sbin/haproxy -f $CONFIG -c -q $EXTRAOPTS $RELOADOPTS
ExecReload=/bin/kill -USR2 $MAINPID
KillMode=mixed
#Restart=always
#Type=forking
Restart=on-failure
RestartSec=5
 
[Install]
WantedBy=multi-user.target

keepalivedのconfigを設定していきます。
PatronのAPIにステータスチェックをする際にマスター/レプリカになっているPostgreSQLについては、
応答コード200を返答するので以下のような振り分け方を実施します。

[DB01/DB02/DB03]

# cat /etc/haproxy/haproxy.cfg
global
    maxconn 100
    stats socket /var/lib/haproxy/stats level admin
 
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 production
    bind *:5000
    option httpchk OPTIONS/master
    http-check expect status 200
    default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
    server postgresql_192.168.13.45_5432 192.168.13.45:5432 maxconn 100 check port 8008
    server postgresql_192.168.13.46_5432 192.168.13.46:5432 maxconn 100 check port 8008
    server postgresql_192.168.13.47_5432 192.168.13.47:5432 maxconn 100 check port 8008 

listen standby
    bind *:5001
    option httpchk OPTIONS/replica
    http-check expect status 200
    default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
    server postgresql_192.168.13.45_5432 192.168.13.45:5432 maxconn 100 check port 8008
    server postgresql_192.168.13.46_5432 192.168.13.46:5432 maxconn 100 check port 8008
    server postgresql_192.168.13.47_5432 192.168.13.47:5432 maxconn 100 check port 8008

HAproxyを起動して、5000/5001ポートにアクセスできることを確認します。

# systemctl start haproxy
# systemctl enable haproxy
# su - postgres

5001に接続してreplicaに接続できていることを確認します。

# psql -h 192.168.13.48 -p 5001 -U postgres 
ユーザ postgres のパスワード: 
psql (13.2)
"help"でヘルプを表示します。

postgres=# select pg_is_in_recovery();
 pg_is_in_recovery 
-------------------
 t
(1 行)
# \q

5000に接続してmasterに接続できていることを確認します。

# psql -h 192.168.13.48 -p 5000 -U postgres
ユーザ postgres のパスワード: 
psql (13.2)
"help"でヘルプを表示します。

postgres=# select pg_is_in_recovery();
 pg_is_in_recovery 
-------------------
 f
(1 行)
# \q

ここまで構築完了です。
あとは片側のHAproxyを落としても接続できることを確認したり、
Patroniをわざと停止させてFailOverが実施されているかを確認します。

最後に

途中でも書いている通り、Patroniの設定ファイルを作成するのが一番苦労しました。
また本当にこれであっている?と聞かれると不安な部分もあるので、
もう少し突き詰めないといけないかと思ってますが、比較的に容易にクラスタを組むことができました。

全体を作ってみた感想

Patroniのconfigの理解するのに相当時間かかりました。
一旦動作はしているものの本当にあっているかどうかについては正直自信ないので、
ちゃんと突き詰めていきたいと思っています。

GitHubで編集を提案

Discussion

ログインするとコメントできます