詳見repo: https://github.com/cupkappu/pgsql-ha-exp
本文使用以下節點:
db1 192.168.50.11 現有 PostgreSQL 資料
db2 192.168.50.12 新增 PostgreSQL replica
witness 192.168.50.13 第三個 etcd 成員轉換後服務:
db1 PostgreSQL + Patroni + etcd1 + HAProxy
db2 PostgreSQL + Patroni + etcd2 + HAProxy
witness etcd3PostgreSQL 大版本維持不變。以下命令以 PostgreSQL 16 為例。
舊 PostgreSQL 與新 Patroni 分別位於不同容器。切換時先停止舊容器,再由 Patroni 容器掛載同一個 PGDATA。
1. 記錄現有 PostgreSQL 狀態
在現有 PostgreSQL 執行:
SELECT version();
SHOW data_directory;
SHOW config_file;
SHOW hba_file;
SHOW port;
SHOW listen_addresses;
SHOW wal_level;
SHOW wal_log_hints;
SHOW max_wal_senders;
SHOW max_replication_slots;
SELECT pg_current_wal_lsn();本文使用以下 PGDATA:
/srv/postgres/data保存容器設定與日誌:
sudo docker inspect standalone-postgres \
> standalone-postgres.inspect.json
sudo docker logs --tail 200 standalone-postgres \
> standalone-postgres.log
sudo du -sh /srv/postgres/dataconfig_file 或 hba_file 位於 PGDATA 外部時,在 Patroni Compose 掛載相同路徑。
2. 建立 Patroni 帳號
在現有 PostgreSQL 執行:
CREATE ROLE patroni_admin
WITH LOGIN SUPERUSER
PASSWORD 'CHANGE_ME_PATRONI_SUPERUSER_PASSWORD';
CREATE ROLE replicator
WITH LOGIN REPLICATION
PASSWORD 'CHANGE_ME_REPLICATION_PASSWORD';
CREATE ROLE rewind_user
WITH LOGIN
PASSWORD 'CHANGE_ME_REWIND_PASSWORD';
GRANT EXECUTE ON FUNCTION pg_catalog.pg_ls_dir(text, boolean, boolean)
TO rewind_user;
GRANT EXECUTE ON FUNCTION pg_catalog.pg_stat_file(text, boolean)
TO rewind_user;
GRANT EXECUTE ON FUNCTION pg_catalog.pg_read_binary_file(text)
TO rewind_user;
GRANT EXECUTE ON FUNCTION pg_catalog.pg_read_binary_file(
text,
bigint,
bigint,
boolean
) TO rewind_user;在 pg_hba.conf 加入:
host replication replicator 192.168.50.11/32 scram-sha-256
host replication replicator 192.168.50.12/32 scram-sha-256
host all patroni_admin 192.168.50.0/24 scram-sha-256
host all rewind_user 192.168.50.0/24 scram-sha-256
host all all 192.168.50.0/24 scram-sha-256重新載入設定:
SELECT pg_reload_conf();3. 設定 PostgreSQL 複寫參數
在現有 PostgreSQL 設定中加入:
wal_level = replica
max_wal_senders = 10
max_replication_slots = 10
wal_log_hints = on
hot_standby = on
wal_keep_size = '256MB'重新啟動 PostgreSQL。
檢查參數:
SHOW wal_level;
SHOW wal_log_hints;
SHOW max_wal_senders;
SHOW max_replication_slots;4. 建立三成員 etcd
etcd peer 位址:
etcd1=http://192.168.50.11:2380
etcd2=http://192.168.50.12:2380
etcd3=http://192.168.50.13:2380etcd client 位址:
http://192.168.50.11:2379
http://192.168.50.12:2379
http://192.168.50.13:2379三個成員使用相同值:
ETCD_INITIAL_CLUSTER=etcd1=http://192.168.50.11:2380,etcd2=http://192.168.50.12:2380,etcd3=http://192.168.50.13:2380
ETCD_CLUSTER_TOKEN=CHANGE_ME_ETCD_CLUSTER_TOKEN啟動三個 etcd 成員後執行:
etcdctl \
--endpoints=http://192.168.50.11:2379,http://192.168.50.12:2379,http://192.168.50.13:2379 \
endpoint health --cluster
etcdctl \
--endpoints=http://192.168.50.11:2379 \
member list -w table三個 endpoint 顯示 healthy 後執行下一步。
5. 建立 Patroni 映像
在 db1 與 db2 建立相同檔案。
Dockerfile.patroni
FROM postgres:16-bookworm
ARG PATRONI_VERSION=4.1.4
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
python3-pip curl jq \
&& pip3 install --break-system-packages --no-cache-dir \
"patroni[etcd3,psycopg2-binary]==${PATRONI_VERSION}" \
&& rm -rf /var/lib/apt/lists/*
COPY patroni-entrypoint.sh /usr/local/bin/patroni-entrypoint
RUN chmod 0755 /usr/local/bin/patroni-entrypoint
ENTRYPOINT ["/usr/local/bin/patroni-entrypoint"]
CMD ["/etc/patroni.yml"]patroni-entrypoint.sh
#!/usr/bin/env bash
set -euo pipefail
install -d -o postgres -g postgres /var/run/postgresql
umask 077
exec gosu postgres patroni "$@"檢查原 PGDATA 所有者:
sudo stat -c '%u:%g %a %n' /srv/postgres/data
sudo docker run --rm postgres:16-bookworm id postgres兩個 UID/GID 一致後掛載 PGDATA。
6. 建立 db1 Patroni 設定
db1/patroni.yml:
scope: production-pg
namespace: /service/
name: db1
restapi:
listen: 0.0.0.0:8008
connect_address: 192.168.50.11:8008
etcd3:
hosts:
- 192.168.50.11:2379
- 192.168.50.12:2379
- 192.168.50.13:2379
bootstrap:
dcs:
ttl: 12
loop_wait: 2
retry_timeout: 4
maximum_lag_on_failover: 1048576
synchronous_mode: true
synchronous_mode_strict: false
synchronous_node_count: 1
postgresql:
use_pg_rewind: true
use_slots: true
parameters:
wal_level: replica
wal_log_hints: "on"
hot_standby: "on"
max_wal_senders: 10
max_replication_slots: 10
wal_keep_size: 256MB
postgresql:
listen: 0.0.0.0:5432
connect_address: 192.168.50.11:5432
data_dir: /var/lib/postgresql/data
bin_dir: /usr/lib/postgresql/16/bin
authentication:
superuser:
username: patroni_admin
replication:
username: replicator
rewind:
username: rewind_userdb1/compose.yml:
services:
patroni:
build:
context: .
dockerfile: Dockerfile.patroni
image: local/patroni-postgres:16-4.1.4
container_name: pgsql-ha-patroni
restart: unless-stopped
network_mode: host
environment:
PATRONI_SUPERUSER_PASSWORD: CHANGE_ME_PATRONI_SUPERUSER_PASSWORD
PATRONI_REPLICATION_PASSWORD: CHANGE_ME_REPLICATION_PASSWORD
PATRONI_REWIND_PASSWORD: CHANGE_ME_REWIND_PASSWORD
volumes:
- /srv/postgres/data:/var/lib/postgresql/data
- ./patroni.yml:/etc/patroni.yml:ro
command: ["/etc/patroni.yml"]7. 建立 db2 Patroni 設定
複製 db1 設定,修改:
name: db2
restapi:
connect_address: 192.168.50.12:8008
postgresql:
connect_address: 192.168.50.12:5432建立空資料目錄:
sudo install -d -m 0700 -o 999 -g 999 \
/srv/pgsql-ha/postgresqldb2/compose.yml 的資料掛載:
volumes:
- /srv/pgsql-ha/postgresql:/var/lib/postgresql/data
- ./patroni.yml:/etc/patroni.yml:ro8. 停止現有 PostgreSQL
停止應用寫入。
在 PostgreSQL 執行:
CHECKPOINT;
SELECT pg_switch_wal();停止舊容器:
sudo docker update --restart=no standalone-postgres
sudo docker stop standalone-postgres檢查程序與 PID 檔案:
sudo ss -lntp | grep ':5432 ' || true
sudo test ! -f /srv/postgres/data/postmaster.pid建立停止狀態副本:
sudo rsync -aHAX --numeric-ids \
/srv/postgres/data/ \
/srv/postgres/pre-ha-cutover/重新命名舊容器:
sudo docker rename \
standalone-postgres \
standalone-postgres-pre-ha9. 啟動 db1 Patroni
cd /opt/pgsql-ha/db1
sudo docker compose build patroni
sudo docker compose up -d patroni
sudo docker logs -f pgsql-ha-patroni檢查 Patroni:
curl -fsS http://192.168.50.11:8008/patroni | jq
curl -fsS http://192.168.50.11:8008/primary檢查 PostgreSQL:
SELECT pg_is_in_recovery();
SELECT pg_current_wal_lsn();
SELECT datname FROM pg_database ORDER BY datname;pg_is_in_recovery() 回傳 false。
10. 啟動 db2 Patroni
cd /opt/pgsql-ha/db2
sudo docker compose build patroni
sudo docker compose up -d patroni
sudo docker logs -f pgsql-ha-patroni檢查角色:
curl -fsS http://192.168.50.11:8008/primary
curl -fsS http://192.168.50.12:8008/replica在 db1 查詢複寫狀態:
SELECT application_name,
client_addr,
state,
sync_state,
write_lsn,
flush_lsn,
replay_lsn
FROM pg_stat_replication;11. 啟動 HAProxy
兩台 HAProxy 使用相同設定:
listen postgres-write
bind 0.0.0.0:5000
option httpchk GET /primary
http-check expect status 200
default-server inter 1s fall 2 rise 2 on-marked-down shutdown-sessions
server db1 192.168.50.11:5432 check port 8008
server db2 192.168.50.12:5432 check port 8008檢查兩個入口:
psql -h 192.168.50.11 -p 5000 -U app -d appdb \
-Atc 'SELECT inet_server_addr(), NOT pg_is_in_recovery();'
psql -h 192.168.50.12 -p 5000 -U app -d appdb \
-Atc 'SELECT inet_server_addr(), NOT pg_is_in_recovery();'兩個查詢的第二欄回傳 t。
應用程式 DSN:
host=192.168.50.11,192.168.50.12 port=5000,5000 dbname=appdb user=app target_session_attrs=read-write connect_timeout=3恢復應用流量。
12. 驗證資料複寫
在 primary 執行:
CREATE TABLE IF NOT EXISTS ha_migration_probe (
id bigserial PRIMARY KEY,
marker text UNIQUE NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
INSERT INTO ha_migration_probe(marker)
VALUES ('after-ha-cutover');在 replica 執行:
SELECT marker, pg_is_in_recovery()
FROM ha_migration_probe
WHERE marker = 'after-ha-cutover';回傳:
after-ha-cutover|true13. 執行故障切換
停止目前 primary 的 Patroni:
sudo docker stop pgsql-ha-patroni檢查另一台:
curl -fsS http://192.168.50.12:8008/primary寫入第二筆資料:
INSERT INTO ha_migration_probe(marker)
VALUES ('after-first-failover');恢復舊節點:
sudo docker start pgsql-ha-patroni檢查舊節點:
curl -fsS http://192.168.50.11:8008/replica查詢兩筆資料:
SELECT marker
FROM ha_migration_probe
ORDER BY id;14. 回退
應用流量尚未恢復時執行:
sudo docker stop pgsql-ha-patroni
sudo rsync -aHAX --delete --numeric-ids \
/srv/postgres/pre-ha-cutover/ \
/srv/postgres/data/
sudo docker rename \
standalone-postgres-pre-ha \
standalone-postgres
sudo docker update \
--restart=unless-stopped \
standalone-postgres
sudo docker start standalone-postgresHA 已接收新寫入後,使用目前 Patroni primary 重建故障節點。
