杯子茶室

关注有趣的事物

用兩台獨立 Docker 資料庫主機部署 PostgreSQL HA:Patroni、etcd witness 與 HAProxy 實作

网络 0 评
詳見repo: https://github.com/cupkappu/pgsql-ha-exp

本文使用以下位址:

節點IP服務
db1192.168.50.11PostgreSQL、Patroni、etcd1、HAProxy
db2192.168.50.12PostgreSQL、Patroni、etcd2、HAProxy
witness192.168.50.13etcd3

拓撲如下:

                         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 tracking

4. 安裝 Docker Engine 與 Compose plugin

db1db2 與 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 version

5. Port

Port來源用途
2379/TCPPatroni 節點、管理主機etcd client API
2380/TCP三個 etcd 成員etcd peer traffic
5432/TCPdb1、db2、管理主機PostgreSQL 複寫與管理
8008/TCPdb1、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/etcd2

witness

sudo install -d -m 0755 /opt/pgsql-ha
sudo install -d -m 0700 /srv/pgsql-ha/etcd3

7. 在 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.sh

8. 兩台資料庫主機共用的 Compose 設定

db1 與 db2 使用相同的 Compose 檔案,節點差異由 .envpatroni.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 8008

postgres-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: false

11. 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: false

db1 與 db2 的以下值必須一致:

  • scopenamespace
  • 三個 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=periodic

13. 設定密碼

三台主機的 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_PASSWORD

14. 啟動三成員 etcd

先啟動 DCS,再啟動 Patroni。

db1

cd /opt/pgsql-ha
sudo docker compose --env-file .env -f compose.yml up -d etcd

db2

cd /opt/pgsql-ha
sudo docker compose --env-file .env -f compose.yml up -d etcd

witness

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 patroni

db2 會向目前 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_PASSWORD

18. 驗證兩個寫入入口

兩台 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');
SQL

19. 驗證唯讀入口與複寫

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 PGPASSWORD

20. 應用程式應如何連線

這套架構沒有浮動 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=3

URI 形式:

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 PGPASSWORD

22. 測試整台資料庫主機失效

假設 db1 是 primary:

  1. 先寫入唯一探針;
  2. 關閉 db1 或停止其 Docker daemon;
  3. 確認 etcd2 與 etcd3 仍有多數票;
  4. 等待 db2 提升;
  5. 從 db2:5000 寫入新資料;
  6. 恢復 db1;
  7. 確認 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/primary

23. 測試 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

參考資料

將現有單實例 PostgreSQL 轉換為 Patroni HA 叢集
发表评论
撰写评论