개요
2편까지 오면 클러스터는 스스로 failover하고, 옛 primary도 pg_rewind로 알아서 합류합니다. 여기서 “이제 HA는 끝났다"고 생각하기 쉽지만, Patroni 클러스터의 사고는 대부분 그다음에 일어납니다. 자동화가 무엇을 기준으로 움직이는지 모르고 운영하면, 멀쩡한 서버를 read-only로 만들거나, 커밋된 데이터를 잃거나, primary가 두 대가 됩니다.
이 글에서는 운영할 때 알아야 할 지점을 하나씩 재현합니다.
- 설정은 어디서 바꿔야 하는가 (
ALTER SYSTEM이 먹히지 않는 이유) - 재시작과 점검은 어떤 순서로 해야 하는가
- etcd가 멈추면 무슨 일이 생기고,
failsafe_mode는 무엇을 막아 주는가 - failover에서 데이터를 얼마나 잃을 수 있고, 동기 복제는 무엇을 바꾸는가
- Patroni 프로세스가 죽으면 무엇이 위험한가 (watchdog)
- 오래 멈춘 replica의 slot과
reinit - REST API 인증
기준 버전: Patroni 4.1.5, PostgreSQL 18.6, etcd 3.7.2, HAProxy 3.4.5, Rocky Linux 9.8. 2편에서 만든 클러스터(etcd 3대, pg1-pg3, HAProxy)에서 이어서 실행했습니다. 실습 중에 failover를 여러 번 일으켰기 때문에 절마다 leader와 timeline이 다릅니다.
writer.log는 2편의 0.5초 간격 쓰기 스크립트 출력입니다.psql결과 중 일부는 값만 출력하는-At옵션으로 받은 것을 읽기 쉽게 기본 표 형식으로 옮겼습니다.
설정은 DCS에서 바꾼다
Patroni 클러스터의 PostgreSQL 설정은 네 군데에서 옵니다. 우선순위가 높은 것부터 적습니다.
| 위치 | 바꾸는 방법 | 범위 |
|---|---|---|
postmaster 명령행 (--max_connections=...) | DCS 설정. 직접 바꿀 수 없음 | 클러스터 전체 |
postgresql.auto.conf | ALTER SYSTEM | 그 노드만 |
postgresql.conf (Patroni가 생성) | DCS 설정(patronictl edit-config), 노드의 patroni.yml | 클러스터 전체, 또는 그 노드 |
postgresql.base.conf | 직접 편집 | 그 노드만 |
1편에서 본 것처럼 Patroni는 max_connections, max_wal_senders, max_replication_slots, wal_level처럼 primary와 replica가 맞아야 하는 파라미터를 명령행으로 넘깁니다. 이 값들을 ALTER SYSTEM으로 바꾸면 어떻게 되는지 봅니다.
ALTER SYSTEM은 조용히 무시되거나, 한 노드에만 먹힌다
pg1=# ALTER SYSTEM SET max_connections = 200;
ALTER SYSTEM
pg1=# ALTER SYSTEM SET work_mem = '64MB';
ALTER SYSTEM
$ cat /var/lib/pgsql/18/data/postgresql.auto.conf
# Do not edit this file manually!
# It will be overwritten by the ALTER SYSTEM command.
max_connections = '200'
work_mem = '64MB'
$ patronictl -c /etc/patroni/patroni.yml restart pg-ha pg1 --force
pg1=# SELECT name, setting, source FROM pg_settings WHERE name IN ('max_connections', 'work_mem');
name | setting | source
-----------------+---------+--------------------
max_connections | 100 | command line
work_mem | 65536 | configuration file
(2 rows)
pg2=# SHOW work_mem;
work_mem
----------
4MB
(1 row)
- 재시작까지 했는데
max_connections는 100 그대로이고, 출처가command line입니다.ALTER SYSTEM은 성공했다고 답하지만 명령행 값을 이길 수 없습니다. work_mem은 64MB(65536kB)로 바뀌었지만 pg1에서만입니다. pg2는 4MB입니다.postgresql.auto.conf는 복제되는 파일이 아니라서, failover로 pg2가 primary가 되면 설정이 바뀐 것처럼 보입니다.
ALTER SYSTEM RESET ALL로 되돌리고, 클러스터 전체 설정은 DCS에서 바꿉니다.
patronictl edit-config와 pending restart
$ patronictl -c /etc/patroni/patroni.yml edit-config pg-ha --force -p max_connections=200 -p work_mem=64MB
---
+++
@@ -3,12 +3,14 @@
postgresql:
parameters:
hot_standby: 'on'
+ max_connections: 200
max_replication_slots: 10
max_wal_senders: 10
password_encryption: scram-sha-256
wal_keep_size: 128MB
wal_level: replica
wal_log_hints: 'on'
+ work_mem: 64MB
use_pg_rewind: true
use_slots: true
retry_timeout: 10
Configuration changed
$ patronictl -c /etc/patroni/patroni.yml list
+ Cluster: pg-ha (7689601790135361659) ------+----+-------------+-----+------------+-----+-----------------+---------------------------+
| Member | Host | Role | State | TL | Receive LSN | Lag | Replay LSN | Lag | Pending restart | Pending restart reason |
+--------+-------------+---------+-----------+----+-------------+-----+------------+-----+-----------------+---------------------------+
| pg1 | 172.30.0.21 | Leader | running | 5 | | | | | * | max_connections: 100->200 |
| pg2 | 172.30.0.22 | Replica | streaming | 5 | 0/54F9D50 | 0 | 0/54F9D50 | 0 | * | max_connections: 100->200 |
| pg3 | 172.30.0.23 | Replica | streaming | 5 | 0/54F9D50 | 0 | 0/54F9D50 | 0 | * | max_connections: 100->200 |
+--------+-------------+---------+-----------+----+-------------+-----+------------+-----+-----------------+---------------------------+
-p는 PostgreSQL 파라미터를,-s는 Patroni 설정(ttl,synchronous_mode등)을 바꿉니다. 옵션 없이 실행하면 편집기로 YAML을 엽니다. 바뀐 내용을 diff로 보여 주고 DCS의config키에 씁니다.- 각 노드의 Patroni가 다음 loop에서 이 값을 읽어 적용합니다.
work_mem은 reload로 세 노드에 모두 반영되었고, 재시작이 필요한max_connections는Pending restart로 표시됩니다.
$ patronictl -c /etc/patroni/patroni.yml restart pg-ha --pending --force
Success: restart on member pg1
Success: restart on member pg2
Success: restart on member pg3
$ awk '$1>="22:34:32" && $1<="22:34:37"' /root/writer.log
22:34:32.145 172.30.0.21 INSERT 0 1
22:34:32.671 172.30.0.21 INSERT 0 1
22:34:33.189 172.30.0.21 INSERT 0 1
22:34:34.731 172.30.0.21 INSERT 0 1
22:34:35.255 172.30.0.21 INSERT 0 1
22:34:35.772 172.30.0.21 INSERT 0 1
patronictl restart는 failover 없이 그 자리에서 재시작합니다. primary pg1이 재시작하는 동안(22:34:33.2-34.7) 쓰기가 1.5초 끊겼습니다. 데이터가 적은 실습이라 짧았을 뿐이고, shared_buffers가 크거나 종료 체크포인트가 길면 이 시간은 늘어납니다. 쓰기 중단을 줄이려면 replica를 먼저 재시작하고, switchover한 뒤, 옛 primary를 재시작합니다.
줄이는 변경은 Patroni가 순서를 지켜 준다
hot standby에는 “replica의 max_connections 같은 값이 primary보다 작으면 안 된다"는 규칙이 있습니다(Hot Standby Parameter Settings). 늘릴 때는 괜찮지만 줄일 때 replica를 먼저 재시작하면 규칙을 어기게 됩니다. max_connections를 50으로 줄이고 replica pg2만 먼저 재시작해 봅니다.
$ patronictl -c /etc/patroni/patroni.yml edit-config pg-ha --force -p max_connections=50
$ patronictl -c /etc/patroni/patroni.yml restart pg-ha pg2 --pending --force
Success: restart on member pg2
$ journalctl -u patroni -o cat --since "-10s" | grep max_connections # pg2
2026-09-25 22:35:16,387 INFO: max_connections value in pg_controldata: 200, in the global configuration: 50. pg_controldata value will be used. Setting 'Pending restart' flag
Patroni는 primary가 마지막으로 WAL에 기록한 값(pg_controldata의 200)을 보고, replica를 그 값으로 띄운 뒤 Pending restart를 남겨 둡니다. primary를 먼저 재시작해 값이 50으로 내려간 뒤 replica를 다시 재시작하면 됩니다.
서비스 재시작은 replica부터
patronictl restart와 systemctl restart patroni는 전혀 다릅니다. 앞의 것은 PostgreSQL만 재시작하고 leader key를 지킵니다. 뒤의 것은 Patroni를 멈추므로, leader의 Patroni는 PostgreSQL을 내리고 leader key를 내려놓습니다. 그러면 failover가 일어납니다.
REST API 인증을 켜려고(뒤에서 봅니다) 세 노드에서 차례로 systemctl restart patroni를 했더니 timeline이 두 번 올라갔습니다.
$ for n in pg1 pg2 pg3; do ssh $n systemctl restart patroni; done # 실습에서는 docker exec로 차례로 실행
$ journalctl -u patroni -o cat --since 22:56:25 --until 22:56:50 | grep -E "INFO|WARN" | grep -vE "no action|connection" # pg2
2026-09-25 22:56:43,329 INFO: Got response from pg3 http://172.30.0.23:8008/patroni: {"state": "running", ...
2026-09-25 22:56:43,364 WARNING: Request failed to pg1: GET http://172.30.0.21:8008/patroni ...
2026-09-25 22:56:43,489 INFO: promoted self to leader by acquiring session lock
2026-09-25 22:56:43,821 WARNING: Cancelling long running task promote
2026-09-25 22:56:45,881 WARNING: Postgresql is not running.
2026-09-25 22:56:45,881 INFO: Lock owner: pg1; I am pg2
...
2026-09-25 22:56:45,927 INFO: starting as a secondary
$ patronictl -c /etc/patroni/patroni.yml history | tail -3
| 9 | 122658128 | no recovery target specified | 2026-09-25T22:47:39.425190+00:00 | pg1 |
| 10 | 1644203792 | no recovery target specified | | |
| 11 | 1644204024 | no recovery target specified | 2026-09-25T22:56:45.821684+00:00 | pg1 |
- pg1(leader)의 Patroni를 재시작하자 pg1이 key를 내려놓았고, pg2가 곧바로 promote했습니다(timeline 10 → 11).
- 바로 이어서 pg2의 Patroni를 재시작하자 promote하던 pg2가 작업을 취소하고 내려갔고(
Cancelling long running task promote), 이번에는 다시 올라온 pg1이 promote했습니다(timeline 11 → 12).
2초 사이에 leader가 두 번 바뀌었고 그때마다 쓰기가 끊겼습니다. history의 timeline 10 줄에 시각과 leader가 비어 있는 것도 promote가 도중에 끊긴 흔적입니다. Patroni 자체를 재시작해야 할 때(버전 업그레이드, patroni.yml의 재시작이 필요한 항목 변경)는 replica를 하나씩 재시작하고, 멤버가 streaming으로 돌아온 것을 확인한 뒤, switchover로 leader를 옮기고, 마지막으로 옛 leader를 재시작합니다.
유지보수는 pause로
PostgreSQL을 손으로 내려야 하는 작업이 있습니다. 이때 Patroni는 PostgreSQL이 죽었다고 보고 다시 띄우거나 failover합니다. 이것을 막는 것이 pause(maintenance mode) 입니다.
$ patronictl -c /etc/patroni/patroni.yml pause pg-ha --wait
'pause' request sent, waiting until it is recognized by all nodes
Success: cluster management is paused
$ su - postgres -c "/usr/pgsql-18/bin/pg_ctl -D /var/lib/pgsql/18/data stop -m fast" # primary pg1
waiting for server to shut down.... done
server stopped
$ patronictl -c /etc/patroni/patroni.yml list # 25초 뒤
+ Cluster: pg-ha (7689601790135361659) ----+----+-------------+-----+------------+-----+
| Member | Host | Role | State | TL | Receive LSN | Lag | Replay LSN | Lag |
+--------+-------------+---------+---------+----+-------------+-----+------------+-----+
| pg1 | 172.30.0.21 | Replica | stopped | | unknown | | unknown | |
| pg2 | 172.30.0.22 | Replica | running | 5 | 0/5508380 | 0 | 0/5508380 | 0 |
| pg3 | 172.30.0.23 | Replica | running | 5 | 0/5508380 | 0 | 0/5508380 | 0 |
+--------+-------------+---------+---------+----+-------------+-----+------------+-----+
Maintenance mode: on
$ journalctl -u patroni -o cat --since "-25s" | grep -E "INFO|WARN" | tail -4 # pg1
2026-09-25 22:36:15,742 INFO: PAUSE: postgres is not running
2026-09-25 22:36:25,701 WARNING: Postgresql is not running.
2026-09-25 22:36:25,701 INFO: Lock owner: None; I am pg1
2026-09-25 22:36:25,744 INFO: PAUSE: postgres is not running
(pg2, pg3의 Pending restart 열은 줄였습니다.) primary를 내렸는데 아무도 재시작하지 않고, leader key가 비었는데도(Lock owner: None) 누구도 promote하지 않습니다. pg2의 로그는 PAUSE: no action. I am (pg2)만 반복합니다. 작업을 마치고 PostgreSQL을 직접 띄운 뒤 patronictl resume하면, Patroni가 다시 관리를 시작하고 pg1이 key를 되찾습니다.
pause 중에는 자동 failover가 없다는 점을 잊으면 안 됩니다. 작업이 끝나면 반드시 resume하고, 모니터링에서 Maintenance mode: on(REST API의 pause: true)이 오래 켜져 있으면 알리게 해 둡니다.
DCS가 멈추면 클러스터 전체가 read-only가 된다
1편에서 DCS와 끊긴 primary는 스스로 강등한다는 것을 봤습니다. split-brain을 막는 장치이지만 끊긴 것이 primary 한 대가 아니라 DCS 자체라면 어떻게 될까요? etcd 3대 중 2대를 멈춰 쿼럼을 깹니다. PostgreSQL 3대는 모두 멀쩡합니다.
$ systemctl stop etcd # etcd2, etcd3에서, 22:37:08.6
$ for n in pg1 pg2 pg3; do echo "$n in_recovery=$(ssh $n psql -h /run/postgresql -Atc 'select pg_is_in_recovery()')"; done # 30초 뒤
pg1 in_recovery=t
pg2 in_recovery=t
pg3 in_recovery=t
$ curl -s "http://localhost:7000/;csv" | cut -d, -f1,2,18 | grep -E "^(primary|replicas),pg"
primary,pg1,DOWN
primary,pg2,DOWN
primary,pg3,DOWN
replicas,pg1,UP
replicas,pg2,UP
replicas,pg3,UP
primary였던 pg1의 로그입니다.
2026-09-25 22:37:17,876 INFO: Lock owner: pg1; I am pg1
2026-09-25 22:37:27,875 INFO: Lock owner: pg1; I am pg1
2026-09-25 22:37:37,893 ERROR: Error communicating with DCS
2026-09-25 22:37:37,894 INFO: demoting self because DCS is not accessible and I was a leader
2026-09-25 22:37:39,072 INFO: demoted self because DCS is not accessible and I was a leader
세 노드가 모두 standby가 되었고, HAProxy의 쓰기 포트에는 서버가 하나도 없습니다. 클라이언트는 22:37:37.8까지 쓰다가 그 뒤로 read-only transaction 오류와 연결 오류만 받았습니다. 데이터베이스 서버는 하나도 죽지 않았는데 쓰기 서비스 전체가 멈췄습니다. Patroni 입장에서는 옳은 판단입니다. DCS에 닿지 못하는 동안에는 다른 노드가 promote했는지 알 수 없으니, 두 primary가 생길 위험보다 쓰기 중단을 택합니다.
etcd를 되살리자 pg1이 다시 leader가 되었는데 이때도 promote를 거치므로 timeline이 하나 올라갔습니다(history의 timeline 5 → 6, 22:38:12).
failsafe_mode: 모든 멤버와 연락되면 primary를 유지한다
failsafe_mode는 이 상황을 완화합니다. DCS에 닿지 못해도 leader가 다른 모든 멤버와 REST API로 연락되고 그들이 자신을 leader로 인정하면 primary를 유지합니다. 모든 멤버가 “나는 promote하지 않겠다"고 답했다면 두 primary가 생길 수 없다는 논리입니다.
$ patronictl -c /etc/patroni/patroni.yml edit-config pg-ha --force -s failsafe_mode=true
$ etcdctl get /service/pg-ha/failsafe --print-value-only
{"pg1":"http://172.30.0.21:8008/patroni","pg2":"http://172.30.0.22:8008/patroni","pg3":"http://172.30.0.23:8008/patroni"}
$ systemctl stop etcd # etcd2, etcd3에서, 22:38:50.3
DCS의 failsafe 키에는 leader가 연락해야 할 멤버 목록이 적힙니다. 같은 장애를 다시 일으킨 뒤의 pg1 로그입니다.
2026-09-25 22:39:01,800 INFO: Lock owner: pg1; I am pg1
2026-09-25 22:39:11,821 INFO: Got response from pg2 http://172.30.0.22:8008/failsafe: Accepted
2026-09-25 22:39:11,821 INFO: Got response from pg3 http://172.30.0.23:8008/failsafe: Accepted
2026-09-25 22:39:11,822 INFO: continue to run as a leader because failsafe mode is enabled and all members are accessible
2026-09-25 22:39:21,894 INFO: Got response from pg2 http://172.30.0.22:8008/failsafe: Accepted
2026-09-25 22:39:21,894 INFO: Got response from pg3 http://172.30.0.23:8008/failsafe: Accepted
2026-09-25 22:39:21,895 INFO: continue to run as a leader because failsafe mode is enabled and all members are accessible
45초가 지나도 pg1은 primary(in_recovery=f)였고, 클라이언트의 쓰기도 끊기지 않았습니다. 하지만 조건은 “모든 멤버"입니다. DCS가 멈춘 채로 replica pg3가 죽으면 어떻게 되는지 봅니다.
$ docker kill pg3 # 22:39:43.8
$ journalctl -u patroni -o cat --since "-27s" | grep -iE "failsafe|demot" | cut -c1-150 # pg1
2026-09-25 22:39:51,967 INFO: Got response from pg2 http://172.30.0.22:8008/failsafe: Accepted
2026-09-25 22:39:55,974 WARNING: Request failed to pg3: POST http://172.30.0.23:8008/failsafe (HTTPConnectionPool(host='172.30.0.23', port=8008): Max retries exceeded
2026-09-25 22:39:55,974 INFO: demoting self because DCS is not accessible and I was a leader
2026-09-25 22:39:55,974 INFO: Demoting self (offline)
2026-09-25 22:39:57,142 INFO: demoted self because DCS is not accessible and I was a leader
pg3가 응답하지 않자 pg1은 곧바로 강등했습니다. pg3가 정말 죽었는지, 아니면 pg1과만 끊겨서 promote했을지 모르기 때문입니다. failsafe_mode는 “DCS만 잠깐 멈춘” 경우를 살려 주는 장치이지 DCS 장애를 견디는 장치가 아닙니다. 가장 확실한 대책은 etcd를 PostgreSQL만큼 튼튼하게 운영하는 것입니다. 홀수 대, 서로 다른 장애 도메인, 빠른 디스크, 그리고 etcd 자체의 모니터링입니다.
비동기 복제는 커밋 응답을 받은 데이터도 잃을 수 있다
기본 설정의 스트리밍 복제는 비동기입니다. primary는 커밋을 자기 디스크에 기록하면 바로 클라이언트에 성공을 돌려주고, WAL은 그 뒤에 replica로 갑니다. 그 사이에 primary가 죽으면 클라이언트는 성공 응답을 받았는데 새 primary에는 없는 트랜잭션이 생깁니다.
얼마나 잃는지 정확히 세려고 클라이언트를 만들었습니다. 스레드 4개가 번호를 1씩 올리며 INSERT하고 COMMIT 성공 응답을 받은 번호만 파일에 적습니다.
# 4개 스레드가 각자 번호를 1씩 올리며 INSERT하고, COMMIT 응답을 받은 번호만 파일에 적는다
import sys, threading, time, psycopg2
DSN = "host=127.0.0.1 port=5000 user=app dbname=appdb connect_timeout=2"
def worker(w):
seq, conn = 0, None
with open(f"/root/ack.{w}", "w") as f:
while True:
try:
if conn is None:
conn = psycopg2.connect(DSN); conn.autocommit = True
seq += 1
conn.cursor().execute("INSERT INTO ack(worker, seq) VALUES (%s, %s)", (w, seq))
f.write(f"{seq} {time.time():.3f}\n"); f.flush()
except Exception as e:
conn = None; time.sleep(0.2)
for w in range(4):
threading.Thread(target=worker, args=(w,), daemon=True).start()
while True: time.sleep(1)
failover가 끝나면 파일에 적힌(응답을 받은) 번호 가운데 새 primary의 ack 테이블에 없는 것을 셉니다. 장애 시나리오는 “primary와 replica 사이의 네트워크가 잠깐 막히고, 그 사이에 primary가 죽는” 경우입니다. primary pg1에서 replica로 가는 복제 트래픽(출발 포트 5432)만 막으면, 클라이언트는 HAProxy로 계속 쓰지만 WAL은 replica에 가지 못합니다.
$ python3.12 /root/acker.py & # haproxy 서버, 5초 동안 쓰기
$ for r in 22 23; do iptables -A OUTPUT -p tcp --sport 5432 -d 172.30.0.$r -j DROP; done # pg1, 22:41:32.5
pg1=# SELECT pg_current_wal_lsn();
pg_current_wal_lsn
--------------------
0/6781D80
(1 row)
pg1=# SELECT application_name, sent_lsn, replay_lsn, pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) FROM pg_stat_replication;
application_name | sent_lsn | replay_lsn | pg_wal_lsn_diff
------------------+-----------+------------+-----------------
pg2 | 0/60B8D90 | 0/5ED77E0 | 9088504
pg3 | 0/60D6118 | 0/5ED7C00 | 9087448
(2 rows)
3초 만에 replica는 약 9MB(9,088,504바이트) 뒤처졌습니다. 이 상태에서 pg1을 죽입니다.
$ docker kill pg1 # 22:41:35.7
$ etcdctl get /service/pg-ha/leader --print-value-only # 22:42:00
pg3
$ /root/check_loss.sh
worker 0: acked=35267 missing=12906
worker 1: acked=35460 missing=13045
worker 2: acked=35389 missing=12948
worker 3: acked=35396 missing=13037
total acked=141512, lost=51936
클라이언트가 성공 응답을 받은 141,512건 중 51,936건이 새 primary에 없습니다. 나중에 pg1을 다시 붙이자 Patroni는 pg_rewind로 pg1을 두 서버가 갈라진 위치 0/5ED7C00까지 되돌렸고(pg1의 마지막 위치는 0/6781D80 이후), 그 9MB 남짓의 WAL과 함께 5만여 건은 영영 사라졌습니다.
maximum_lag_on_failover가 막지 못한 이유
이상한 점이 있습니다. 지연이 약 9MB였는데 maximum_lag_on_failover는 1MB입니다. 1MB 넘게 뒤처진 replica는 후보에서 빠져야 하는데 pg3가 승격했습니다.
$ journalctl -u patroni -o cat --since 22:41:30 --until 22:42:02 | grep -E "INFO|WARN" | cut -c1-110 # pg2
2026-09-25 22:41:38,630 INFO: no action. I am (pg2), a secondary, and following a leader (pg1)
2026-09-25 22:41:48,586 INFO: no action. I am (pg2), a secondary, and following a leader (pg1)
2026-09-25 22:41:58,319 INFO: Got response from pg3 http://172.30.0.23:8008/patroni: {"state": "running", "p
2026-09-25 22:42:00,307 WARNING: Request failed to pg1: GET http://172.30.0.21:8008/patroni (HTTPConnection
2026-09-25 22:42:00,308 INFO: Wal position of pg3 is ahead of my wal position
2026-09-25 22:42:00,363 INFO: following a different leader because i am not the healthiest node
pg2는 지연 때문이 아니라 “pg3가 나보다 앞서서” 양보했을 뿐입니다. 이유는 지연을 계산하는 기준에 있습니다(Ha.is_lagging()).
lag = self.cluster.status.last_lsn - wal_position
return lag > global_config.maximum_lag_on_failover
last_lsn은 leader가 loop마다 DCS status 키에 적어 둔 마지막 WAL 위치입니다. 실제 primary의 현재 위치가 아닙니다. leader는 이 값을 loop_wait(10초)마다 한 번 적습니다. pg3가 후보에서 빠지지 않았다는 것은 DCS의 last_lsn이 pg3의 위치(0/5ED7C00)에서 1MB 이내였고 실제 primary 위치(0/6781D80)보다 적어도 8MB 뒤의 값이었다는 뜻입니다. 마지막 기록 뒤에 쌓인 WAL은 replica들에게 보이지 않았습니다.
maximum_lag_on_failover는 오래 뒤처져 있던 replica를 걸러 내는 장치입니다. 최대 loop_wait 동안 쌓인 WAL은 보지 못하므로, 유실의 상한을 보장하지 않습니다.
synchronous_mode: 응답을 받은 커밋은 잃지 않는다
같은 실험을 동기 복제로 반복합니다. Patroni의 synchronous_mode를 켜면 Patroni가 synchronous_standby_names를 관리하고, 동기 standby만 failover 후보로 삼습니다.
$ patronictl -c /etc/patroni/patroni.yml edit-config pg-ha --force -s synchronous_mode=on
$ patronictl -c /etc/patroni/patroni.yml list
+ Cluster: pg-ha (7689601790135361659) -----------+----+-------------+-----+------------+-----+
| Member | Host | Role | State | TL | Receive LSN | Lag | Replay LSN | Lag |
+--------+-------------+--------------+-----------+----+-------------+-----+------------+-----+
| pg1 | 172.30.0.21 | Replica | streaming | 8 | 0/67307B0 | 0 | 0/67307B0 | 0 |
| pg2 | 172.30.0.22 | Sync Standby | streaming | 8 | 0/67307B0 | 0 | 0/67307B0 | 0 |
| pg3 | 172.30.0.23 | Leader | running | 8 | | | | |
+--------+-------------+--------------+-----------+----+-------------+-----+------------+-----+
$ etcdctl get /service/pg-ha/sync --print-value-only
{"leader":"pg3","quorum":0,"sync_standby":"pg2"}
leader는 pg3, 동기 standby는 pg2입니다(synchronous_standby_names = 'pg2'). 누가 동기 standby인지도 DCS의 sync 키에 기록됩니다. 이제 pg3에서 replica로 가는 복제를 막습니다.
$ for r in 21 22; do iptables -A OUTPUT -p tcp --sport 5432 -d 172.30.0.$r -j DROP; done # pg3, 22:43:20.0
pg3=# SELECT pid, wait_event_type, wait_event, left(query, 40) FROM pg_stat_activity WHERE usename = 'app';
pid | wait_event_type | wait_event | left
-----+-----------------+------------+------------------------------------------
195 | IPC | SyncRep | INSERT INTO ack(worker, seq) VALUES (2,
196 | IPC | SyncRep | INSERT INTO ack(worker, seq) VALUES (1,
197 | IPC | SyncRep | INSERT INTO ack(worker, seq) VALUES (0,
198 | IPC | SyncRep | INSERT INTO ack(worker, seq) VALUES (3,
(4 rows)
네 세션이 모두 SyncRep에서 기다립니다(인터널 9편에서 본 대기입니다). 커밋은 로컬에 기록됐지만 동기 standby의 확인이 오지 않으니 클라이언트에 응답하지 않습니다. 이 상태에서 pg3를 죽입니다.
$ docker kill pg3 # 22:43:23.2
$ etcdctl get /service/pg-ha/leader --print-value-only # 22:43:48
pg2
$ /root/check_loss.sh
worker 0: acked=17200 missing=0
worker 1: acked=17200 missing=0
worker 2: acked=17146 missing=0
worker 3: acked=17156 missing=0
total acked=68702, lost=0
응답을 받은 68,702건 중 잃은 것은 0건입니다. 옛 primary pg3에도 pg_rewind로 버려진 WAL이 있었지만, 그 안의 트랜잭션은 클라이언트가 성공 응답을 받지 못한 것들입니다.
동기 복제의 비용
동기 복제는 공짜가 아닙니다. 세 가지를 측정했습니다.
처리량: 같은 클러스터에서 pgbench -c 8 -j 4 -T 20 -N(scale 10)을 돌렸습니다.
| 모드 | tps | 평균 지연 |
|---|---|---|
| 비동기 | 7,950 | 1.006 ms |
synchronous_mode | 6,031 | 1.326 ms |
컨테이너가 모두 한 서버에 있어 네트워크 왕복이 거의 0인 조건입니다. 실제 네트워크에서는 커밋마다 standby까지의 왕복 시간이 더해지므로 차이가 더 커집니다.
동기 standby가 죽으면 커밋이 멈춘다: 동기 standby pg1을 죽이고 2편의 쓰기 스크립트를 봅니다.
$ docker kill pg1 # 22:44:33.3, 동기 standby
$ awk '$1>="22:44:32" && $1<="22:44:42"' /root/writer.log
22:44:32.388 172.30.0.22 INSERT 0 1
22:44:32.920 172.30.0.22 INSERT 0 1
22:44:40.959 172.30.0.22 INSERT 0 1
22:44:41.485 172.30.0.22 INSERT 0 1
$ journalctl -u patroni -o cat --since "-42s" | grep -iE "sync" # pg2(leader)
2026-09-25 22:44:40,903 INFO: Updating synchronous privilege temporarily from ['pg1'] to []
2026-09-25 22:44:40,950 INFO: Assigning synchronous standby status to ['pg3']
2026-09-25 22:44:43,109 INFO: Synchronous standby status assigned to ['pg3']
다음 HA loop에서 Patroni가 pg3를 새 동기 standby로 정하기까지 8초 동안 모든 커밋이 멈췄습니다.
replica가 모두 사라지면: 남은 replica pg3까지 죽이면 기본값(synchronous_mode_strict: false)에서는 Patroni가 동기 복제를 풀어 버립니다.
$ docker kill pg3 # 22:45:25.8
$ journalctl -u patroni -o cat --since "-32s" | grep -iE "sync" # pg2
2026-09-25 22:45:30,904 INFO: Updating synchronous privilege temporarily from ['pg3'] to []
2026-09-25 22:45:30,950 INFO: Assigning synchronous standby status to []
$ awk '$1>="22:45:24" && $1<="22:45:32"' /root/writer.log
22:45:24.777 172.30.0.22 INSERT 0 1
22:45:25.306 172.30.0.22 INSERT 0 1
22:45:25.819 172.30.0.22 INSERT 0 1
22:45:30.957 172.30.0.22 INSERT 0 1
22:45:31.489 172.30.0.22 INSERT 0 1
5초 멈춘 뒤 쓰기가 다시 됩니다. 가용성은 지켰지만 이제 데이터는 pg2 한 대에만 있습니다. synchronous_mode_strict: true로 바꾸면 반대로 동작합니다.
$ patronictl -c /etc/patroni/patroni.yml edit-config pg-ha --force -s synchronous_mode_strict=on
$ journalctl -u patroni -o cat --since "-17s" | grep -iE "sync" # pg2
2026-09-25 22:46:07,097 INFO: Changed synchronous_standby_names from '' to '*'
2026-09-25 22:46:17,075 INFO: Assigning synchronous standby status to ['*']
pg2=# SELECT wait_event, left(query, 30) FROM pg_stat_activity WHERE usename = 'app';
wait_event | left
------------+--------------------------------
SyncRep | INSERT INTO heartbeat DEFAULT
(1 row)
synchronous_standby_names가 '*'가 되어, 어떤 standby든 하나가 붙을 때까지 모든 커밋이 기다립니다. 데이터는 잃지 않지만 그동안은 쓰지 못합니다.
| 설정 | failover 유실 | standby 장애 때 |
|---|---|---|
| 비동기 (기본) | 응답을 받은 커밋도 잃을 수 있음 | 영향 없음 |
synchronous_mode: on | 동기 standby로만 failover, 응답을 받은 커밋은 잃지 않음 | 새 동기 standby를 정할 때까지 멈춤, 모두 사라지면 비동기로 전환 |
synchronous_mode_strict: on | 위와 같음 | standby가 하나도 없으면 쓰기 불가 |
어느 쪽이 맞는지는 서비스가 정합니다. 다만 “기본값에서 failover하면 잃을 수 있다"는 것을 알고 고르는 것과 모르고 쓰는 것은 다릅니다. 동기 복제를 쓴다면 replica는 최소 2대를 두어, 동기 standby 하나가 죽어도 다른 replica가 이어받게 합니다.
Patroni가 죽으면 PostgreSQL은 계속 primary다
1편의 자진 강등은 Patroni가 살아 있을 때만 일어납니다. primary 노드의 Patroni 프로세스만 kill -9로 죽여 봅니다. OOM killer, Python 예외, 운영자의 실수로 실제로 일어날 수 있는 일입니다. 2편에서 본 것처럼 systemd unit은 KillMode=process, Restart=no라서 PostgreSQL은 그대로 남고 Patroni는 되살아나지 않습니다.
$ kill -9 $(systemctl show -p MainPID --value patroni) # primary pg2, 22:47:16.3
$ patronictl -c /etc/patroni/patroni.yml list # 40초 뒤, pg1에서
+ Cluster: pg-ha (7689601790135361659) ------+----+-------------+-----+------------+-----+
| Member | Host | Role | State | TL | Receive LSN | Lag | Replay LSN | Lag |
+--------+-------------+---------+-----------+----+-------------+-----+------------+-----+
| pg1 | 172.30.0.21 | Leader | running | 10 | | | | |
| pg3 | 172.30.0.23 | Replica | streaming | 10 | 0/74FFED8 | 0 | 0/74FFED8 | 0 |
+--------+-------------+---------+-----------+----+-------------+-----+------------+-----+
$ systemctl is-active patroni # pg2
failed
$ ps -eo pid,args | grep "postgres -D" | grep -v grep | cut -c1-80 # pg2
2396 /usr/pgsql-18/bin/postgres -D /var/lib/pgsql/18/data --config-file=/var/li
pg2의 lease가 만료되자 pg1이 22:47:39.4에 promote했습니다(timeline 10). 그런데 pg2의 PostgreSQL도 그대로 살아 있습니다.
pg2=# SELECT pg_is_in_recovery();
pg_is_in_recovery
-------------------
f
(1 row)
pg2=# INSERT INTO heartbeat(server) VALUES ('172.30.0.22') RETURNING id, at::time(0), 'written on pg2 (old primary)';
id | at | ?column?
------+----------+------------------------------
1862 | 22:48:03 | written on pg2 (old primary)
(1 row)
primary가 두 대입니다. pg1은 timeline 10의 primary, pg2는 timeline 9의 primary로 각자 쓰기를 받습니다. HAProxy는 REST API가 죽은 pg2를 DOWN으로 보고 pg1으로만 보내므로, HAProxy를 거치는 클라이언트는 안전합니다. 하지만 pg2에 직접 붙어 있던 연결이나, 주소를 직접 적은 배치 작업은 pg2에 계속 씁니다.
pg2의 Patroni를 다시 띄우면 정리됩니다.
$ systemctl start patroni # pg2
$ journalctl -u patroni -o cat --since "-22s" | grep -E "INFO" | grep -E "Lock owner|emot|rewind|diverged" | cut -c1-120 # 일부 줄만 옮김
2026-09-25 22:48:11,714 INFO: Lock owner: pg1; I am pg2
2026-09-25 22:48:11,714 INFO: Demoting self (immediate-nolock)
2026-09-25 22:48:12,776 INFO: demoting self because I do not have the lock and I was a leader
2026-09-25 22:48:14,037 INFO: running pg_rewind from pg1
2026-09-25 22:48:15,594 INFO: stderr=pg_rewind: servers diverged at WAL location 0/74F9D50 on timeline 9
pg1=# SELECT count(*) FROM heartbeat WHERE id = 1862;
count
-------
0
(1 row)
Patroni는 자기가 leader key를 갖지 않은 primary라는 것을 알고 즉시 강등했고, pg_rewind로 pg1을 따르게 했습니다. 그 사이 pg2에 쓴 id 1862번 행은 사라졌습니다. 30초 남짓이었지만 그동안 pg2에 쓴 것은 모두 잃었습니다.
watchdog
이 틈을 막는 장치가 watchdog입니다. Patroni는 leader인 동안 리눅스 watchdog 장치(/dev/watchdog)를 주기적으로 건드리고, Patroni가 멈춰서 제때 건드리지 못하면 커널이 노드를 재부팅합니다. PostgreSQL도 함께 죽으므로 두 primary가 생기지 않습니다. Patroni는 lease가 만료되기 전에 재부팅이 일어나도록 타이머를 ttl보다 safety_margin(기본 5초)만큼 짧게 잡습니다.
watchdog:
mode: required # off, automatic(기본), required
device: /dev/watchdog
safety_margin: 5
- 기본값
automatic은 장치가 있으면 쓰고 없으면 그냥 넘어갑니다. 이 실습 환경(Docker Desktop의 리눅스 VM)에는/dev/watchdog이 없었고, Patroni 로그에는 watchdog에 관한 줄이 한 줄도 없었습니다. 운영에서 watchdog에 기대려면required로 두어, 장치가 없으면 leader가 되지 못하게 합니다. - 하드웨어 watchdog이 없는 서버는
softdog커널 모듈을 씁니다. Patroni RPM의 unit 파일에도modprobe softdog와/dev/watchdog소유권 변경이 주석으로 들어 있고, postgres 사용자가 장치를 열 수 있어야 합니다. - 이 실습 환경에서는 watchdog을 켤 수 없어서 재부팅 동작은 직접 확인하지 못했습니다.
동기 복제도 이 상황의 피해를 줄입니다. 옛 primary의 동기 standby가 새 primary를 따라가면 옛 primary의 커밋은 SyncRep에서 멈춰 응답을 돌려주지 못하기 때문입니다.
멈춘 멤버의 slot과 reinit
use_slots: true이면 primary에 멤버마다 replication slot이 있습니다. replica를 멈추면 slot은 어떻게 될까요? pg3의 Patroni를 멈추고 멤버 목록에서 pg3가 사라진 뒤 primary에서 WAL을 많이 만듭니다.
$ systemctl stop patroni # pg3, 22:48:40
pg1=# SELECT pg_size_pretty(sum(size)) AS pg_wal FROM pg_ls_waldir();
pg_wal
--------
208 MB
(1 row)
pg1=# CREATE TABLE big AS SELECT g, repeat(md5(g::text), 8) pad FROM generate_series(1, 1500000) g;
pg1=# CHECKPOINT;
pg1=# SELECT pg_size_pretty(sum(size)) AS pg_wal FROM pg_ls_waldir();
pg_wal
--------
672 MB
(1 row)
pg1=# SELECT slot_name, active, pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained FROM pg_replication_slots ORDER BY 1;
slot_name | active | retained
-----------+--------+----------
pg2 | t | 0 bytes
pg3 | f | 459 MB
(2 rows)
pg3는 이미 멤버 목록에 없는데, slot pg3는 남아서 459MB의 WAL을 붙잡고 있습니다. Patroni는 사라진 멤버의 slot을 member_slots_ttl(기본 30분) 동안 유지합니다(global_config.py). 잠깐 재시작하는 replica가 돌아왔을 때 WAL이 남아 있게 하려는 것입니다. 거꾸로 말하면, replica가 멈춘 뒤 30분 동안 primary의 pg_wal은 계속 커질 수 있습니다. 쓰기가 많은 시스템이라면 30분이면 디스크를 채우기에 충분합니다(인터널 9편).
실습에서는 기다리지 않도록 60초로 줄였습니다.
$ patronictl -c /etc/patroni/patroni.yml edit-config pg-ha --force -s member_slots_ttl=60s
$ journalctl -u patroni -o cat --since "-3min" | grep -i slot # pg1
2026-09-25 22:50:16,124 INFO: Replication slot 'pg3' for absent cluster member is expired after 60 sec.
2026-09-25 22:50:26,165 INFO: Trying to drop unknown replication slot 'pg3'
2026-09-25 22:50:26,167 INFO: Dropped replication slot 'pg3'
slot이 사라진 뒤 돌아온 replica
slot이 지워졌으니 pg3가 필요로 하던 WAL도 곧 지워집니다. 이제 pg3를 다시 띄웁니다.
$ systemctl start patroni # pg3
$ patronictl -c /etc/patroni/patroni.yml list | tail -5
+--------+-------------+---------+-----------+----+-------------+-----+------------+-----+
| pg1 | 172.30.0.21 | Leader | running | 10 | | | | |
| pg2 | 172.30.0.22 | Replica | streaming | 10 | 0/3F1BC838 | 0 | 0/3F1BC838 | 0 |
| pg3 | 172.30.0.23 | Replica | running | 10 | 0/7000000 | 898 | 0/75060A0 | 893 |
+--------+-------------+---------+-----------+----+-------------+-----+------------+-----+
$ grep -hE "FATAL" /var/lib/pgsql/18/data/log/*.log | tail -2 # pg3
2026-09-25 22:51:02.847 UTC [257] FATAL: could not receive data from WAL stream: ERROR: requested WAL segment 0000000A0000000000000007 has already been removed
2026-09-25 22:51:07.839 UTC [258] FATAL: could not receive data from WAL stream: ERROR: requested WAL segment 0000000A0000000000000007 has already been removed
$ journalctl -u patroni -o cat --since "-30s" | tail -2 # pg3
2026-09-25 22:50:56,223 INFO: no action. I am (pg3), a secondary, and following a leader (pg1)
2026-09-25 22:51:06,224 INFO: no action. I am (pg3), a secondary, and following a leader (pg1)
pg3의 상태는 streaming이 아니라 running이고, 지연은 898MB에서 줄지 않습니다. PostgreSQL 로그에는 필요한 WAL 파일이 이미 지워졌다는 오류가 5초마다 찍힙니다. 그런데 Patroni는 no action입니다. Patroni는 이 상태를 스스로 고치지 않습니다. WAL 아카이브가 있고 restore_command를 설정해 두었다면 아카이브에서 WAL을 가져와 따라잡을 수 있지만, 이 실습처럼 없으면 다시 복제하는 수밖에 없습니다. 모니터링은 State가 streaming인지와 Lag를 함께 봐야 합니다.
$ date +%T; patronictl -c /etc/patroni/patroni.yml reinit pg-ha pg3 --force --wait; date +%T
22:51:13
Success: reinitialize for member pg3
Waiting for reinitialize to complete on: pg3
Reinitialize is completed on: pg3
22:54:42
reinit은 pg3의 데이터 디렉터리를 지우고 leader에서 새로 pg_basebackup을 받습니다. 데이터베이스 전체가 469MB인데 3분 29초가 걸렸습니다. primary의 로그에서 원인을 확인합니다.
$ grep -hE "checkpoint (starting|complete)" /var/lib/pgsql/18/data/log/*.log | cut -c1-110 # pg1, 그 시각의 줄만 옮김
2026-09-25 22:51:14.684 UTC [74] LOG: checkpoint starting: force wait
2026-09-25 22:54:40.405 UTC [74] LOG: checkpoint complete: wrote 2002 buffers (12.2%), wrote 1 SLRU buffers
pg_basebackup은 시작할 때 체크포인트를 요청하는데, 기본값은 --checkpoint=spread입니다. 체크포인트가 checkpoint_completion_target(0.9) × checkpoint_timeout(5분)에 걸쳐 천천히 쓰도록 속도를 조절하므로, 실제 복사는 1초 남짓인데 체크포인트를 3분 26초 동안 기다렸습니다. Patroni가 pg_basebackup에 넘기는 옵션은 postgresql.basebackup에서 정할 수 있습니다. 모든 노드의 patroni.yml에 넣고 reload합니다.
postgresql:
basebackup:
- checkpoint: fast
dirty buffer를 다시 만든 뒤(UPDATE big SET pad = pad WHERE g % 10 = 0) 같은 reinit을 합니다.
$ date +%T; patronictl -c /etc/patroni/patroni.yml reinit pg-ha pg3 --force --wait >/dev/null; date +%T
22:56:20
22:56:25
$ grep -hE "checkpoint (starting|complete)" /var/lib/pgsql/18/data/log/*.log | tail -2 | cut -c1-110 # pg1
2026-09-25 22:56:22.151 UTC [74] LOG: checkpoint starting: immediate force wait
2026-09-25 22:56:22.286 UTC [74] LOG: checkpoint complete: wrote 15642 buffers (95.5%), wrote 2 SLRU buffers;
체크포인트가 immediate로 바뀌어 15,642개 버퍼를 0.14초에 썼고, reinit 전체가 5초에 끝났습니다. 새 노드를 붙이거나 장애 노드를 다시 만들 때마다 이 차이가 납니다. 다만 즉시 체크포인트는 순간적으로 primary의 I/O를 많이 쓰므로, 바쁜 시간대라면 max-rate 옵션으로 복사 속도를 제한하는 것도 함께 고려합니다.
REST API에는 인증을 건다
Patroni REST API는 상태 조회뿐 아니라 재시작, switchover, reinit 같은 명령도 받습니다. 기본 설정에서는 8008번 포트에 닿는 누구나 이 명령을 보낼 수 있습니다.
$ curl -s -w " (HTTP %{http_code})\n" -XPOST http://localhost:8008/restart # pg3
restarted successfully (HTTP 200)
인증 정보도 비밀번호와 같이 /etc/patroni_env.conf에 환경 변수로 넣고 Patroni를 재시작했습니다(restapi.authentication의 username, password에 해당합니다). 이때 순서를 지키지 않아 앞에서 본 두 번의 failover가 일어났습니다.
$ cat >> /etc/patroni_env.conf <<'EOF' # 모든 pg 노드, 값은 가렸습니다
> PATRONI_RESTAPI_USERNAME=patroni_api
> PATRONI_RESTAPI_PASSWORD=********
> EOF
$ curl -s -w " (HTTP %{http_code})\n" -XPOST http://localhost:8008/restart
no auth header received (HTTP 401)
$ curl -s -o /dev/null -w "GET /health (HTTP %{http_code})\n" http://localhost:8008/health
GET /health (HTTP 200)
$ patronictl -c /etc/patroni/patroni.yml restart pg-ha pg3 --force
...
Failed: restart for member pg3, status code=401, (no auth header received)
$ (set -a; . /etc/patroni_env.conf; set +a; patronictl -c /etc/patroni/patroni.yml restart pg-ha pg3 --force)
...
Success: restart on member pg3
- 인증을 켜면 명령(POST, PUT, PATCH, DELETE)에는 인증이 필요하고,
GET /health같은 조회는 그대로 열려 있어서 HAProxy의 health check는 바꿀 것이 없습니다. patronictl도 REST API로 명령을 보내므로 같은 인증 정보가 필요합니다. 여기서는 환경 변수를 읽혀서 실행했습니다. 운영자가 쓰는 계정에서 이 파일을 읽을 수 있게 할지,patronictl용 설정 파일을 따로 둘지 정해 두어야 합니다.- 인증은 평문 HTTP 위에서 Basic 인증이므로 운영에서는
restapi.certfile,restapi.keyfile로 TLS를 함께 켭니다.
운영 점검표
- 설정: PostgreSQL 파라미터는
patronictl edit-config로 바꾼다.ALTER SYSTEM은 명령행 파라미터에 무시되고, 한 노드에만 적용된다.Pending restart가 남아 있지 않은지 본다. - 재시작 순서:
patronictl restart는 제자리 재시작(primary는 잠깐 쓰기 중단),systemctl restart patroni는 leader에서 failover를 일으킨다. Patroni를 재시작할 때는 replica부터, leader는 switchover한 뒤에. - pause: 수동 작업 전에 pause, 끝나면 반드시 resume.
Maintenance mode: on을 모니터링한다. - DCS: etcd 쿼럼이 깨지면 클러스터 전체가 read-only가 된다. etcd는 홀수 대, 분리된 장애 도메인, 빠른 디스크로 운영하고 따로 모니터링한다.
failsafe_mode는 모든 멤버와 연락될 때만 primary를 지켜 준다. - 데이터 유실: 비동기 복제의 failover는 응답을 받은 커밋도 잃을 수 있고,
maximum_lag_on_failover는 그 상한을 보장하지 않는다. 잃으면 안 되는 데이터라면synchronous_mode를 쓰고 replica를 2대 이상 둔다. - watchdog: Patroni가 죽으면 옛 primary가 계속 쓰기를 받는다.
watchdog.mode: required와/dev/watchdog(softdog)을 준비한다. - slot: 멈춘 멤버의 slot은
member_slots_ttl(기본 30분) 동안 WAL을 붙잡는다.pg_wal크기와 비활성 slot을 모니터링한다. - reinit: WAL을 잃은 replica는 Patroni가 고치지 않는다(
running, 지연이 줄지 않음).postgresql.basebackup에checkpoint: fast를 넣어 두면 reinit이 빠르다. WAL 아카이브(restore_command)가 있으면 재복제 없이 따라잡을 수 있다. - REST API: 인증과 TLS를 켠다. 켜지 않으면 8008번에 닿는 누구나 재시작과 switchover를 할 수 있다.
정리
- Patroni 클러스터의 PostgreSQL 설정은 DCS가 기준입니다. 핵심 파라미터는 명령행으로 넘어가서
ALTER SYSTEM이 먹히지 않고, 재시작이 필요한 변경은Pending restart로 표시됩니다. - leader의 Patroni를 멈추면 failover가 일어납니다. 점검은 pause, 교대는 switchover, 재시작은 replica부터 합니다.
- DCS에 닿지 못하면 primary는 강등합니다. etcd 쿼럼이 깨지면 멀쩡한 클러스터 전체가 read-only가 되고,
failsafe_mode는 모든 멤버와 연락될 때만 이를 막아 줍니다. - 비동기 복제의 failover에서 응답을 받은 커밋 51,936건(141,512건 중)을 잃었고,
maximum_lag_on_failover는 DCS에 기록된 마지막 위치만 보기 때문에 막지 못했습니다.synchronous_mode에서는 0건이었지만 처리량이 줄고, standby 장애 때 커밋이 멈춥니다. - Patroni 프로세스가 죽으면 PostgreSQL은 primary로 남아 split-brain이 됩니다. watchdog이 이 틈을 막습니다.
- 멈춘 멤버의 slot은 30분 동안 WAL을 붙잡고, 그 뒤 WAL을 잃은 replica는
reinit해야 합니다.basebackup에checkpoint: fast를 주면 3분 29초가 5초가 됩니다.
참고 자료
Patroni 소스 코드 (v4.1.5 태그 기준)
- patroni/ha.py:
is_lagging(L1280),_handle_dcs_error(L2352),check_failsafe_topology(L1228) - patroni/global_config.py:
member_slots_ttl기본값 - patroni/dcs/__init__.py: 사라진 멤버의 slot 보존과 만료
Patroni 공식 문서
- Dynamic Configuration Settings:
failsafe_mode,synchronous_mode,member_slots_ttl - Replication modes
- DCS Failsafe Mode
- Pause/Resume mode for the cluster
- Watchdog support
- REST API, YAML Configuration: REST API
PostgreSQL 18 공식 문서
- Hot Standby: Administrator’s Overview
- Synchronous Replication
- pg_basebackup:
--checkpoint,--max-rate