詳見repo: https://github.com/cupkappu/pgsql-ha-exp
本文使用以下位址:
| 節點 | IP | 服務 |
|---|---|---|
db1 | 192.168.50.11 | PostgreSQL、Patroni、etcd1、HAProxy |
db2 | 192.168.50.12 | PostgreSQL、Patroni、etcd2、HAProxy |
witness | 192.168.50.13 | etcd3 |
拓撲如下:
application
│
┌────────────┴────────────┐
│ │
db1:5000 / 5001 db2:5000 / 5001
HAProxy HAProxy
│ │
└────── PostgreSQL ───────┘
streaming replication
db1 db2 witness
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Docker Engine │ │ Docker Engine │ │ Docker Engine │
│ │ │ │ │ │
│ etcd1 │◀─────▶│ etcd2 │◀─────▶│ etcd3 │
│ Patroni + PG16 │ │ Patroni + PG16 │ │ │
│ HAProxy │ │ HAProxy │ │ │
└─────────────────┘ └─────────────────┘ └─────────────────┘etcd 三成員叢集的多數票是 2。這讓它可以容忍任意一個 etcd 成員失效:
db1整台失效時,etcd2 + etcd3仍有兩票;db2整台失效時,etcd1 + etcd3仍有兩票;- witness 失效時,
etcd1 + etcd2仍有兩票。
2. 每個元件負責什麼
PostgreSQL
負責真正的資料儲存與串流複寫。平常是一台 primary、一台 replica。
Patroni
Patroni 管理 PostgreSQL 的生命週期與角色。
- 在 etcd 中維護 leader lock;
- 初始化第一台 PostgreSQL;
- 讓第二台透過
pg_basebackup成為 replica; - 在 primary 失效時判斷 replica 是否可提升;
- 在舊 primary 回來後,透過
pg_rewind或重新初始化讓它回到正確 timeline。
etcd
etcd 是 Patroni 的分散式協調儲存,不保存資料表內容,只保存主要配置。
HAProxy
每台資料庫主機都執行一個 HAProxy:
5000/TCP只轉送到 Patroni 判定的 primary;5001/TCP只轉送到 Patroni 判定的 replica。
HAProxy 不是靠猜測 PostgreSQL 狀態,而是查詢 Patroni REST API:
/primary只有真正持有 leader lock 的 primary 才回傳 HTTP 200;/replica只有健康且可負載均衡的 replica 才回傳 HTTP 200。
3. 版本與前提
本文示範以下版本組合:
Ubuntu 24.04 LTS
PostgreSQL 16
Patroni 4.1.4
etcd 3.5.21
HAProxy 3.0
Docker Engine + Docker Compose plugin先檢查主機狀態:
hostnamectl
ip -brief address
timedatectl status安裝時間同步:
sudo apt-get update
sudo apt-get install -y chrony curl ca-certificates
sudo systemctl enable --now chrony
chronyc tracking4. 安裝 Docker Engine 與 Compose plugin
在 db1、db2 與 witness 上執行:
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
-o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
printf '%s\n' \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" \
| sudo tee /etc/apt/sources.list.d/docker.list >/dev/null
sudo apt-get update
sudo apt-get install -y \
docker-ce docker-ce-cli containerd.io \
docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now docker
sudo docker version
sudo docker compose version5. Port
| Port | 來源 | 用途 |
|---|---|---|
| 2379/TCP | Patroni 節點、管理主機 | etcd client API |
| 2380/TCP | 三個 etcd 成員 | etcd peer traffic |
| 5432/TCP | db1、db2、管理主機 | PostgreSQL 複寫與管理 |
| 8008/TCP | db1、db2、受控管理主機 | Patroni REST API |
| 5000/TCP | 應用程式網段 | 可寫 PostgreSQL 入口 |
| 5001/TCP | 可接受 replica 延遲的應用程式 | 唯讀入口 |
6. 建立目錄
兩台資料庫主機都建立相同的設定目錄:
sudo install -d -m 0755 /opt/pgsql-ha
sudo install -d -m 0700 /srv/pgsql-ha/postgresql在 db1 建立 etcd1 資料目錄:
sudo install -d -m 0700 /srv/pgsql-ha/etcd1在 db2 建立 etcd2 資料目錄:
sudo install -d -m 0700 /srv/pgsql-ha/etcd2witness
sudo install -d -m 0755 /opt/pgsql-ha
sudo install -d -m 0700 /srv/pgsql-ha/etcd37. 在 db1 與 db2 建立 Patroni 映像
以下三個檔案在 db1 與 db2 完全相同。
/opt/pgsql-ha/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"]/opt/pgsql-ha/patroni-entrypoint.sh
#!/usr/bin/env bash
set -euo pipefail
install -d -o postgres -g postgres -m 0700 \
/var/lib/postgresql/data/pgdata
install -d -o postgres -g postgres /var/run/postgresql
chown -R postgres:postgres \
/var/lib/postgresql/data \
/var/run/postgresql
chmod 0700 /var/lib/postgresql/data/pgdata
umask 077
exec gosu postgres patroni "$@"設定權限:
sudo chmod 0755 /opt/pgsql-ha/patroni-entrypoint.sh8. 兩台資料庫主機共用的 Compose 設定
db1 與 db2 使用相同的 Compose 檔案,節點差異由 .env 與 patroni.yml 提供。
/opt/pgsql-ha/compose.yml
name: pgsql-ha
services:
etcd:
container_name: pgsql-ha-etcd
image: gcr.io/etcd-development/etcd:v3.5.21
restart: unless-stopped
network_mode: host
volumes:
- ${ETCD_DATA_DIR:?set ETCD_DATA_DIR}:/etcd-data
command:
- /usr/local/bin/etcd
- --name=${ETCD_NAME:?set ETCD_NAME}
- --data-dir=/etcd-data
- --listen-client-urls=http://0.0.0.0:2379
- --advertise-client-urls=http://${NODE_IP:?set NODE_IP}:2379
- --listen-peer-urls=http://0.0.0.0:2380
- --initial-advertise-peer-urls=http://${NODE_IP}:2380
- --initial-cluster=${ETCD_INITIAL_CLUSTER:?set ETCD_INITIAL_CLUSTER}
- --initial-cluster-state=new
- --initial-cluster-token=${ETCD_CLUSTER_TOKEN:?set ETCD_CLUSTER_TOKEN}
- --auto-compaction-retention=1
- --auto-compaction-mode=periodic
patroni:
container_name: pgsql-ha-patroni
build:
context: .
dockerfile: Dockerfile.patroni
image: local/patroni-postgres:16-4.1.4
restart: unless-stopped
network_mode: host
environment:
PATRONI_SUPERUSER_PASSWORD: ${POSTGRES_SUPERUSER_PASSWORD:?set POSTGRES_SUPERUSER_PASSWORD}
PATRONI_REPLICATION_PASSWORD: ${POSTGRES_REPLICATION_PASSWORD:?set POSTGRES_REPLICATION_PASSWORD}
volumes:
- ${PGDATA_DIR:?set PGDATA_DIR}:/var/lib/postgresql/data
- ./patroni.yml:/etc/patroni.yml:ro
command: ["/etc/patroni.yml"]
haproxy:
container_name: pgsql-ha-haproxy
image: haproxy:3.0-alpine
restart: unless-stopped
network_mode: host
volumes:
- ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro先不要啟動服務。
9. HAProxy 設定
在 db1 與 db2 完全相同。
/opt/pgsql-ha/haproxy.cfg
global
log stdout format raw local0
maxconn 1024
defaults
log global
mode tcp
option tcplog
timeout connect 3s
timeout client 30s
timeout server 30s
timeout check 2s
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 pg1 192.168.50.11:5432 check port 8008
server pg2 192.168.50.12:5432 check port 8008
listen postgres-read
bind 0.0.0.0:5001
balance roundrobin
option httpchk GET /replica
http-check expect status 200
default-server inter 1s fall 2 rise 2
server pg1 192.168.50.11:5432 check port 8008
server pg2 192.168.50.12:5432 check port 8008postgres-write 的健康檢查指向 Patroni /primary;所以兩台 HAProxy 雖然都列出 pg1 與 pg2,實際上同一時間只會把新連線送到目前 primary。
on-marked-down shutdown-sessions 會在後端失去 primary 身分時關閉既有連線。這通常比讓舊連線繼續黏在失效節點安全,但應用程式必須具備重新連線與交易重試能力。
10. db1 設定
/opt/pgsql-ha/.env
NODE_IP=192.168.50.11
ETCD_NAME=etcd1
ETCD_DATA_DIR=/srv/pgsql-ha/etcd1
PGDATA_DIR=/srv/pgsql-ha/postgresql
ETCD_CLUSTER_TOKEN=CHANGE_ME_ETCD_CLUSTER_TOKEN
ETCD_INITIAL_CLUSTER=etcd1=http://192.168.50.11:2380,etcd2=http://192.168.50.12:2380,etcd3=http://192.168.50.13:2380
POSTGRES_SUPERUSER_PASSWORD=CHANGE_ME_POSTGRES_SUPERUSER_PASSWORD
POSTGRES_REPLICATION_PASSWORD=CHANGE_ME_POSTGRES_REPLICATION_PASSWORD
APP_PASSWORD=CHANGE_ME_APPLICATION_PASSWORD/opt/pgsql-ha/patroni.yml
scope: pgsql-ha
namespace: /service/
name: pg1
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
hot_standby: "on"
wal_log_hints: "on"
max_wal_senders: 10
max_replication_slots: 10
wal_keep_size: 256MB
initdb:
- encoding: UTF8
- data-checksums
pg_hba:
- host replication replicator 192.168.50.0/24 scram-sha-256
- host all all 192.168.50.0/24 scram-sha-256
postgresql:
listen: 0.0.0.0:5432
connect_address: 192.168.50.11:5432
data_dir: /var/lib/postgresql/data/pgdata
bin_dir: /usr/lib/postgresql/16/bin
authentication:
superuser:
username: postgres
replication:
username: replicator
parameters:
password_encryption: scram-sha-256
unix_socket_directories: /var/run/postgresql
watchdog:
mode: off
tags:
clonefrom: true
noloadbalance: false
nofailover: false
nosync: false11. db2 設定
/opt/pgsql-ha/.env
NODE_IP=192.168.50.12
ETCD_NAME=etcd2
ETCD_DATA_DIR=/srv/pgsql-ha/etcd2
PGDATA_DIR=/srv/pgsql-ha/postgresql
ETCD_CLUSTER_TOKEN=CHANGE_ME_ETCD_CLUSTER_TOKEN
ETCD_INITIAL_CLUSTER=etcd1=http://192.168.50.11:2380,etcd2=http://192.168.50.12:2380,etcd3=http://192.168.50.13:2380
POSTGRES_SUPERUSER_PASSWORD=CHANGE_ME_POSTGRES_SUPERUSER_PASSWORD
POSTGRES_REPLICATION_PASSWORD=CHANGE_ME_POSTGRES_REPLICATION_PASSWORD
APP_PASSWORD=CHANGE_ME_APPLICATION_PASSWORD/opt/pgsql-ha/patroni.yml
scope: pgsql-ha
namespace: /service/
name: pg2
restapi:
listen: 0.0.0.0:8008
connect_address: 192.168.50.12: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
hot_standby: "on"
wal_log_hints: "on"
max_wal_senders: 10
max_replication_slots: 10
wal_keep_size: 256MB
initdb:
- encoding: UTF8
- data-checksums
pg_hba:
- host replication replicator 192.168.50.0/24 scram-sha-256
- host all all 192.168.50.0/24 scram-sha-256
postgresql:
listen: 0.0.0.0:5432
connect_address: 192.168.50.12:5432
data_dir: /var/lib/postgresql/data/pgdata
bin_dir: /usr/lib/postgresql/16/bin
authentication:
superuser:
username: postgres
replication:
username: replicator
parameters:
password_encryption: scram-sha-256
unix_socket_directories: /var/run/postgresql
watchdog:
mode: off
tags:
clonefrom: true
noloadbalance: false
nofailover: false
nosync: falsedb1 與 db2 的以下值必須一致:
scope與namespace;- 三個 etcd endpoint;
ETCD_CLUSTER_TOKEN;ETCD_INITIAL_CLUSTER;- PostgreSQL superuser 密碼;
- replication 密碼。
以下值必須不同:
- Patroni
name; NODE_IP;ETCD_NAME;- Patroni REST
connect_address; - PostgreSQL
connect_address。
12. witness 設定
/opt/pgsql-ha/.env
NODE_IP=192.168.50.13
ETCD_DATA_DIR=/srv/pgsql-ha/etcd3
ETCD_CLUSTER_TOKEN=CHANGE_ME_ETCD_CLUSTER_TOKEN
ETCD_INITIAL_CLUSTER=etcd1=http://192.168.50.11:2380,etcd2=http://192.168.50.12:2380,etcd3=http://192.168.50.13:2380/opt/pgsql-ha/compose.yml
name: pgsql-ha-witness
services:
etcd:
container_name: pgsql-ha-etcd-witness
image: gcr.io/etcd-development/etcd:v3.5.21
restart: unless-stopped
network_mode: host
volumes:
- ${ETCD_DATA_DIR:?set ETCD_DATA_DIR}:/etcd-data
command:
- /usr/local/bin/etcd
- --name=etcd3
- --data-dir=/etcd-data
- --listen-client-urls=http://0.0.0.0:2379
- --advertise-client-urls=http://${NODE_IP:?set NODE_IP}:2379
- --listen-peer-urls=http://0.0.0.0:2380
- --initial-advertise-peer-urls=http://${NODE_IP}:2380
- --initial-cluster=${ETCD_INITIAL_CLUSTER:?set ETCD_INITIAL_CLUSTER}
- --initial-cluster-state=new
- --initial-cluster-token=${ETCD_CLUSTER_TOKEN:?set ETCD_CLUSTER_TOKEN}
- --auto-compaction-retention=1
- --auto-compaction-mode=periodic13. 設定密碼
三台主機的 ETCD_CLUSTER_TOKEN 必須相同。db1 與 db2 的 PostgreSQL superuser、replication 與 application 密碼也必須相同。
可使用 hex 字串,避免 .env 特殊字元解析問題:
openssl rand -hex 32 # 生成四個變數的Hex,然後放入.env文檔中。
# ETCD_CLUSTER_TOKEN
# POSTGRES_SUPERUSER_PASSWORD
# POSTGRES_REPLICATION_PASSWORD
# APP_PASSWORD14. 啟動三成員 etcd
先啟動 DCS,再啟動 Patroni。
db1
cd /opt/pgsql-ha
sudo docker compose --env-file .env -f compose.yml up -d etcddb2
cd /opt/pgsql-ha
sudo docker compose --env-file .env -f compose.yml up -d etcdwitness
cd /opt/pgsql-ha
sudo docker compose --env-file .env -f compose.yml up -d etcd三個成員全部啟動後再檢查。
從 db1 執行:
sudo docker exec pgsql-ha-etcd \
etcdctl \
--endpoints=http://192.168.50.11:2379,http://192.168.50.12:2379,http://192.168.50.13:2379 \
endpoint health --cluster再看狀態與成員表:
sudo docker exec pgsql-ha-etcd \
etcdctl \
--endpoints=http://192.168.50.11:2379,http://192.168.50.12:2379,http://192.168.50.13:2379 \
endpoint status --cluster -w table
sudo docker exec pgsql-ha-etcd \
etcdctl \
--endpoints=http://192.168.50.11:2379 \
member list -w table應確認:
- 三個 endpoint 都是 healthy;
- 三個 member 都是 started;
- 只有一個 etcd leader;
- 各成員 Raft index 接近。
如果 etcd 還沒有多數票,不要啟動 Patroni。
15. 啟動 Patroni 與 PostgreSQL
先在 db1 建立映像並啟動 Patroni:
cd /opt/pgsql-ha
sudo docker compose --env-file .env -f compose.yml build patroni
sudo docker compose --env-file .env -f compose.yml up -d patroni檢查 db1:
curl -fsS http://192.168.50.11:8008/patroni | jq
curl -fsS http://192.168.50.11:8008/primary全新叢集通常會由 db1 初始化為 primary。確認它穩定後,在 db2 執行:
cd /opt/pgsql-ha
sudo docker compose --env-file .env -f compose.yml build patroni
sudo docker compose --env-file .env -f compose.yml up -d patronidb2 會向目前 primary 取得 base backup,所需時間取決於資料量與網路速度。查看日誌:
sudo docker logs -f pgsql-ha-patroni確認角色:
curl -fsS http://192.168.50.11:8008/patroni | jq '{name,role,state,timeline}'
curl -fsS http://192.168.50.12:8008/patroni | jq '{name,role,state,timeline}'也可以直接測試健康端點:
curl -fsS http://192.168.50.11:8008/primary || true
curl -fsS http://192.168.50.11:8008/replica || true
curl -fsS http://192.168.50.12:8008/primary || true
curl -fsS http://192.168.50.12:8008/replica || true最終應該剛好有一個 /primary 回傳成功,另一個 /replica 回傳成功。
16. 啟動兩個 HAProxy
在 db1:
cd /opt/pgsql-ha
sudo docker compose --env-file .env -f compose.yml up -d haproxy在 db2:
cd /opt/pgsql-ha
sudo docker compose --env-file .env -f compose.yml up -d haproxy查看 HAProxy 日誌:
sudo docker logs pgsql-ha-haproxy正常情況下,postgres-write 只會有一個 UP backend,postgres-read 也只會有一個 UP backend。
17. 建立應用程式帳號與資料庫
可以直接用 PostgreSQL client container,不必在宿主機安裝 psql。
先從 db1 的 .env 取出實際密碼並手動填入下列變數。正式環境不要把密碼留在 shell history:
export POSTGRES_ADMIN_PASSWORD='替換成實際 superuser 密碼'
export APPLICATION_PASSWORD='替換成實際 app 密碼'透過 db1 的 HAProxy 寫入入口執行:
sudo docker run --rm -i --network host \
-e PGPASSWORD="$POSTGRES_ADMIN_PASSWORD" \
postgres:16-bookworm \
psql -v ON_ERROR_STOP=1 \
-h 127.0.0.1 -p 5000 -U postgres -d postgres <<SQL
DO \$\$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'app') THEN
CREATE ROLE app LOGIN PASSWORD '${APPLICATION_PASSWORD}';
ELSE
ALTER ROLE app LOGIN PASSWORD '${APPLICATION_PASSWORD}';
END IF;
END
\$\$;
SELECT 'CREATE DATABASE appdb OWNER app'
WHERE NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = 'appdb')
\gexec
SQL完成後清除 shell 變數:
unset POSTGRES_ADMIN_PASSWORD APPLICATION_PASSWORD18. 驗證兩個寫入入口
兩台 HAProxy 的 5000 都應把連線送到同一個 primary。
export PGPASSWORD='替換成實際 app 密碼'
sudo docker run --rm --network host \
-e PGPASSWORD="$PGPASSWORD" \
postgres:16-bookworm \
psql -At \
-h 192.168.50.11 -p 5000 -U app -d appdb \
-c 'SELECT inet_server_addr(), NOT pg_is_in_recovery();'
sudo docker run --rm --network host \
-e PGPASSWORD="$PGPASSWORD" \
postgres:16-bookworm \
psql -At \
-h 192.168.50.12 -p 5000 -U app -d appdb \
-c 'SELECT inet_server_addr(), NOT pg_is_in_recovery();'兩次都應在第二欄回傳 t,而且通常會顯示同一個 PostgreSQL server address。
建立測試資料:
sudo docker run --rm -i --network host \
-e PGPASSWORD="$PGPASSWORD" \
postgres:16-bookworm \
psql -v ON_ERROR_STOP=1 \
-h 192.168.50.11 -p 5000 -U app -d appdb <<'SQL'
CREATE TABLE IF NOT EXISTS ha_probe (
id bigserial PRIMARY KEY,
marker text UNIQUE NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
INSERT INTO ha_probe(marker)
VALUES ('before-failover');
SQL19. 驗證唯讀入口與複寫
5001 只連到 replica:
sudo docker run --rm --network host \
-e PGPASSWORD="$PGPASSWORD" \
postgres:16-bookworm \
psql -At \
-h 192.168.50.12 -p 5001 -U app -d appdb \
-c "SELECT marker, pg_is_in_recovery() FROM ha_probe WHERE marker='before-failover';"應看到:
before-failover|t這表示資料已到達 replica,而且連線確實不是 primary。
5001 不是強一致讀取入口。需要 read-your-writes、交易後立即讀取或不能接受複寫延遲的請求,應使用 5000。
完成後:
unset PGPASSWORD20. 應用程式應如何連線
這套架構沒有浮動 VIP。兩台 HAProxy 是兩個獨立入口,所以應用程式必須知道兩個位址。
支援 libpq 多主機語法的驅動可使用:
host=192.168.50.11,192.168.50.12 port=5000,5000 dbname=appdb user=app target_session_attrs=read-write connect_timeout=3URI 形式:
postgresql://[email protected]:5000,192.168.50.12:5000/appdb?target_session_attrs=read-write&connect_timeout=3不支援多主機連線的驅動,需要另外部署高可用 VIP、外部負載平衡器、服務探索或受監控 DNS。
21. 測試 PostgreSQL 程序故障轉移
先找出目前 primary:
curl -fsS http://192.168.50.11:8008/primary >/dev/null && echo db1-primary
curl -fsS http://192.168.50.12:8008/primary >/dev/null && echo db2-primary假設 db1 是 primary,在 db1 停止 Patroni 容器:
cd /opt/pgsql-ha
sudo docker compose --env-file .env -f compose.yml stop patroni觀察 db2:
watch -n 1 'curl -s -o /dev/null -w "%{http_code}\n" http://192.168.50.12:8008/primary'當 db2 /primary 回傳 200 後,從 db2 的寫入入口測試:
export PGPASSWORD='替換成實際 app 密碼'
sudo docker run --rm --network host \
-e PGPASSWORD="$PGPASSWORD" \
postgres:16-bookworm \
psql -v ON_ERROR_STOP=1 \
-h 192.168.50.12 -p 5000 -U app -d appdb \
-c "INSERT INTO ha_probe(marker) VALUES ('after-process-failover');"恢復 db1:
cd /opt/pgsql-ha
sudo docker compose --env-file .env -f compose.yml up -d patroni等待 db1 成為 replica:
curl -fsS http://192.168.50.11:8008/replica最後確認兩筆資料:
sudo docker run --rm --network host \
-e PGPASSWORD="$PGPASSWORD" \
postgres:16-bookworm \
psql -At \
-h 192.168.50.11 -p 5000 -U app -d appdb \
-c "SELECT marker FROM ha_probe ORDER BY id;"
unset PGPASSWORD22. 測試整台資料庫主機失效
假設 db1 是 primary:
- 先寫入唯一探針;
- 關閉 db1 或停止其 Docker daemon;
- 確認 etcd2 與 etcd3 仍有多數票;
- 等待 db2 提升;
- 從 db2:5000 寫入新資料;
- 恢復 db1;
- 確認 db1 以 replica 身分重新加入。
在 db2 可以檢查 etcd quorum:
sudo docker exec pgsql-ha-etcd \
etcdctl \
--endpoints=http://192.168.50.12:2379,http://192.168.50.13:2379 \
endpoint health然後檢查 Patroni:
curl -fsS http://192.168.50.12:8008/primary23. 測試 witness 故障
停止 witness 上的 etcd:
cd /opt/pgsql-ha
sudo docker compose --env-file .env -f compose.yml stop etcd因為 etcd1 與 etcd2 還有兩票,叢集應繼續運作。從任一資料庫主機檢查:
sudo docker exec pgsql-ha-etcd \
etcdctl \
--endpoints=http://192.168.50.11:2379,http://192.168.50.12:2379 \
endpoint health並透過 5000 執行一次寫入。
恢復 witness:
cd /opt/pgsql-ha
sudo docker compose --env-file .env -f compose.yml up -d etcd再次檢查三個 endpoint 與 Raft index。
24. 同步複寫設定代表什麼
本文啟用了:
synchronous_mode: true
synchronous_mode_strict: false
synchronous_node_count: 1正常狀態下,Patroni 會選一台同步 standby,並限制自動提升候選,避免把缺少已確認提交交易的副本提升為 primary。
但 synchronous_mode_strict: false 代表:當同步副本消失時,Patroni 可以在短暫等待後解除同步要求,讓 primary 繼續接受寫入。
若改成:
synchronous_mode_strict: true沒有同步副本時,primary 的寫入會阻塞。這可以提高雙節點持久性保證,但任何 replica 維護、網路故障或磁碟延遲都可能直接影響寫入可用性。
25. 日常操作
查看狀態
cd /opt/pgsql-ha
sudo docker compose --env-file .env -f compose.yml ps
sudo docker logs --tail 100 pgsql-ha-patroni
sudo docker logs --tail 100 pgsql-ha-haproxy
sudo docker logs --tail 100 pgsql-ha-etcd一般重啟
sudo docker compose --env-file .env -f compose.yml restart haproxy
sudo docker compose --env-file .env -f compose.yml restart patroni停止容器但保留資料
sudo docker compose --env-file .env -f compose.yml stop移除容器但保留 bind-mounted 資料
sudo docker compose --env-file .env -f compose.yml down重新啟動
sudo docker compose --env-file .env -f compose.yml up -d etcd
sudo docker compose --env-file .env -f compose.yml up -d patroni
sudo docker compose --env-file .env -f compose.yml up -d haproxy參考資料
- Docker Engine on Ubuntu:https://docs.docker.com/engine/install/ubuntu/
- Patroni YAML configuration:https://patroni.readthedocs.io/en/latest/yaml_configuration.html
- Patroni environment configuration:https://patroni.readthedocs.io/en/latest/ENVIRONMENT.html
- Patroni replication modes:https://patroni.readthedocs.io/en/latest/replication_modes.html
- Patroni REST API:https://patroni.readthedocs.io/en/latest/rest_api.html
- etcd clustering guide:https://etcd.io/docs/v3.5/op-guide/clustering/
- etcd FAQ:https://etcd.io/docs/v3.5/faq/
- PostgreSQL libpq connection parameters:https://www.postgresql.org/docs/current/libpq-connect.html
