멀티 리전에서 Patroni H/A 운용하기 — Part 5: 장애 대응 Runbook 및 DR 훈련 시나리오
단일 노드 장애부터 DC 전체 장애, Split-Brain까지 — 실제 장애 상황에서 바로 꺼내 쓸 수 있는 Patroni 멀티 리전 HA 대응 Runbook 5종과 DR 훈련 시나리오, Post-Mortem 체크리스트를 정리한다.
Part 4에서는 Split-Brain 방지를 위한 세 가지 방어층(DCS Leader Lock, Watchdog, STONITH)을 구성했다. 방어 구조가 제자리를 잡았더라도, 실제 장애가 발생했을 때 어떤 순서로 판단하고 어떤 명령을 실행할지 사전에 정의되어 있지 않으면 그 구조는 작동하지 않는다.
이 글은 그 간극을 채우는 Runbook 모음이다. 멀티 리전 HA 환경에서 발생할 수 있는 다섯 가지 주요 장애 시나리오에 대해 — 각각 전제 조건, 판단 기준, 단계별 명령, 복구 후 검증 체크리스트를 포함했다. Runbook은 문서가 아니라 안전 시스템이다. 훈련하지 않은 Runbook은 실제 장애 때 작동하지 않는다.
1. Runbook 사용 전 공통 전제 조건
모든 Runbook 실행 전에 아래 항목을 반드시 확인한다.
공통 환경 변수 설정
터미널 세션을 열 때마다 아래 변수를 먼저 설정한다. 각 Runbook의 명령은 이 변수들을 참조한다.
# -- 공통 환경 변수 --
export PATRONI_CONF="/etc/patroni/patroni.yml"
export DC1_NODES="10.1.0.10 10.1.0.11 10.1.0.12" # 서울 PG 노드
export DC2_NODES="10.2.0.10 10.2.0.11 10.2.0.12" # 부산 PG 노드
export DC1_CLUSTER="pg-seoul-cluster"
export DC2_CLUSTER="pg-busan-standby"
export HAPROXY_HOST="10.1.0.20"
export ETCD_CERTS="--cacert=/etc/etcd/ssl/ca.pem \
--cert=/etc/etcd/ssl/etcd-seoul.pem \
--key=/etc/etcd/ssl/etcd-seoul-key.pem"
# -- 공통 헬스 체크 함수 --
check_cluster() {
local CLUSTER=$1
local CONF=$2
echo "=== 클러스터 토폴로지 확인: $CLUSTER ==="
patronictl -c $CONF topology
echo ""
echo "=== 복제 지연 확인 ==="
for NODE in $DC1_NODES; do
echo -n " $NODE: "
psql -h $NODE -U postgres -t -c \
"SELECT CASE WHEN pg_is_in_recovery()
THEN pg_last_wal_replay_lsn()::text
ELSE pg_current_wal_lsn()::text END;" 2>/dev/null || echo "연결 실패"
done
}
장애 대응 시작 전 공통 알림 체크리스트
장애 대응 시작 전:
□ 슬랙/PagerDuty 등 알림 채널에 장애 인지 공지
□ DBA/인프라 담당자 온콜 연락 완료
□ 현재 서비스 트래픽 수준 및 비즈니스 임팩트 파악
□ 읽기 전용 모드 전환 가능 여부 확인 (애플리케이션 레벨)
□ Runbook 최신 버전 확인 (버전 및 최종 수정일 기록 필수)
2. Runbook A — 단일 노드 장애 (자동 페일오버 확인)
트리거 조건: Primary 노드 1개가 응답 없음 / HAProxy 헬스 체크 실패 알림 수신
예상 RTO: 30초 ~ 2분 (자동 페일오버)
예상 RPO: 0 (Synchronous 모드) / 수 초 (Asynchronous 모드)
Step 1: 장애 노드 상태 파악 (목표 2분 이내)
# 클러스터 전체 상태 즉시 확인
patronictl -c $PATRONI_CONF list
# 장애 노드 직접 접근 시도
ping -c 3 10.1.0.10
ssh 10.1.0.10 "systemctl status patroni postgresql"
# Patroni REST API 응답 확인
curl -s --max-time 5 http://10.1.0.10:8008/health | python3 -m json.tool
# etcd에서 현재 Leader 키 확인
etcdctl $ETCD_CERTS \
--endpoints=https://10.1.0.10:2379,https://10.2.0.10:2379,https://10.3.0.10:2379 \
get /db/pg-seoul-cluster/leader
Step 2: 자동 페일오버 진행 모니터링
Patroni는 TTL 만료 후 자동으로 새 Leader를 선출한다. 개입 없이 상태를 관찰한다.
# 실시간 클러스터 상태 모니터링 (5초마다 갱신)
watch -n 5 "patronictl -c $PATRONI_CONF list"
# 페일오버 완료 확인 포인트:
# 1. 새 Leader 노드가 'running' 상태로 표시됨
# 2. 장애 노드가 목록에서 사라지거나 'stopped' 상태
# 3. 나머지 Replica가 새 Primary에 streaming 상태로 연결됨
Step 3: 페일오버 후 검증
# 새 Primary 확인
NEW_PRIMARY=$(patronictl -c $PATRONI_CONF list | grep Leader | awk '{print $4}')
echo "새 Primary: $NEW_PRIMARY"
# 새 Primary에서 쓰기 가능 여부 테스트
psql -h $NEW_PRIMARY -U postgres -c "
CREATE TABLE IF NOT EXISTS failover_test (
id SERIAL PRIMARY KEY,
created_at TIMESTAMPTZ DEFAULT now(),
note TEXT
);
INSERT INTO failover_test (note) VALUES ('failover_$(date +%s)');
SELECT * FROM failover_test ORDER BY created_at DESC LIMIT 1;
"
# HAProxy가 새 Primary로 라우팅하는지 확인
psql -h $HAPROXY_HOST -p 5000 -U postgres -c \
"SELECT inet_server_addr(), pg_is_in_recovery();"
# 결과: 새 Primary IP | f (false)
# 복제 상태 확인
psql -h $NEW_PRIMARY -U postgres -c "
SELECT application_name, state, sync_state, write_lag
FROM pg_stat_replication;
"
Step 4: 장애 노드 복구 및 재합류
# 장애 원인 해결 후 (서비스 재시작, HW 교체 등)
# 장애 노드에서:
systemctl start patroni
# Patroni가 자동으로 pg_basebackup 또는 pg_rewind로 동기화 시작
# 진행 상황 모니터링:
journalctl -fu patroni
# 재합류 성공 확인
patronictl -c $PATRONI_CONF list
# 장애 노드가 Replica로 streaming 상태여야 함
# 자동 재합류 실패 시 수동 reinitialize:
patronictl -c $PATRONI_CONF reinit $DC1_CLUSTER pg-seoul-1 --force
3. Runbook B — DC 전체 장애 (2DC Standby Cluster 수동 승격)
트리거 조건: DC1(서울) 전체 연결 불가 / DC1의 모든 노드 응답 없음
아키텍처: Part 3 기준 (비동기 복제 + Standby Cluster)
⚠️ 이 Runbook은 수동 페일오버이므로 절차를 절대 서두르지 않는다. 각 단계는 순서대로 완료 후 다음으로 진행한다.
예상 RTO: 5분 ~ 20분 (운영자 숙련도에 따라 다름)
예상 RPO: 마지막 복제 지연 값 (수 초 ~ 수 분)
사전 체크리스트 (승격 전 반드시 수행)
□ 1. DC1의 모든 PostgreSQL 인스턴스가 완전히 정지되었는가?
-> DC1 노드에 SSH 접근 불가 + Patroni REST API 타임아웃 확인
□ 2. DC1 etcd 클러스터가 완전히 중단되었는가?
-> etcdctl endpoint health 응답 없음 확인
□ 3. DC2 Standby Leader의 현재 LSN과 복제 지연을 기록했는가?
□ 4. DC1으로 향하는 모든 애플리케이션 트래픽이 차단되었는가?
□ 5. STONITH가 필요한 경우 수행했는가? (Part 4 참조)
□ 6. 승격 결정권자(DBA 리더 / 인프라 책임자)의 승인을 받았는가?
Step 1: DC1 완전 중단 확인
승격 전 DC1이 살아있을 가능성을 배제해야 한다. DC1이 부분적으로 살아있는 상태에서 승격하면 Split-Brain이 발생한다.
# DC1 모든 노드 연결 불가 확인
for NODE in $DC1_NODES; do
echo -n "DC1 $NODE: "
timeout 5 bash -c "echo >/dev/tcp/$NODE/5432" 2>/dev/null \
&& echo "연결됨 - 아직 살아있음!" \
|| echo "연결 불가 OK"
done
# DC2에서 바라보는 DC1 Primary 연결 상태
psql "host=10.1.0.10,10.1.0.11,10.1.0.12 port=5432 \
user=replicator password=SecureRepPass123! \
connect_timeout=5 \
target_session_attrs=read-write sslmode=require" \
-c "SELECT 1;" 2>&1 | grep -E "error|FATAL|could not"
# "could not connect to the server" -> DC1 완전 중단 확인 OK
Step 2: DC2 현재 상태 및 데이터 지연 확인
# DC2 Standby Cluster 상태
patronictl -c /etc/patroni/patroni.yml topology # DC2 노드에서 실행
# DC2 Standby Leader의 수신된 마지막 WAL 위치 확인
psql -h 10.2.0.10 -U postgres -c "
SELECT
pg_last_wal_receive_lsn() AS receive_lsn,
pg_last_wal_replay_lsn() AS replay_lsn,
now() - pg_last_xact_replay_timestamp() AS replay_delay,
pg_last_wal_receive_lsn() = pg_last_wal_replay_lsn() AS fully_replayed;
"
# 복제 지연이 있다면 반드시 기록 (Post-Mortem 용)
# fully_replayed = t -> 데이터 손실 없음
# fully_replayed = f -> replay_delay 만큼의 데이터 손실 가능
Step 3: STONITH 수행 (DC1이 부분적으로 살아있을 가능성이 있는 경우)
# AWS 환경 예시: DC1 인스턴스 강제 중지
python3 /usr/local/bin/stonith-aws.py i-0123456789abcdef0 stop # pg-seoul-1
python3 /usr/local/bin/stonith-aws.py i-0123456789abcdef1 stop # pg-seoul-2
python3 /usr/local/bin/stonith-aws.py i-0123456789abcdef2 stop # pg-seoul-3
# STONITH 완료 후 재확인
for INSTANCE in i-0123456789abcdef0 i-0123456789abcdef1 i-0123456789abcdef2; do
aws ec2 describe-instances --instance-ids $INSTANCE \
--query 'Reservations[].Instances[].State.Name' --output text
# 모두 "stopped" 또는 "terminated" 이어야 함
done
Step 4: DC2 Standby Cluster 승격
# Patroni 4.1+ 권장 방법
patronictl -c /etc/patroni/patroni.yml promote-cluster $DC2_CLUSTER
# 구버전 또는 수동 방법
# patronictl -c /etc/patroni/patroni.yml edit-config \
# --set standby_cluster=null --force
# 승격 후 상태 확인
patronictl -c /etc/patroni/patroni.yml topology
# 예상 결과:
# pg-busan-1 Leader running TL+1
# pg-busan-2 Replica running TL+1
# pg-busan-3 Replica running TL+1
# PostgreSQL에서 직접 확인
psql -h 10.2.0.10 -U postgres -c "SELECT pg_is_in_recovery();"
# f (false) -> Primary로 승격 완료 OK
Step 5: 애플리케이션 엔드포인트 전환
# HAProxy를 DC2 노드들로 업데이트 (또는 DNS 전환)
# 방법 1: HAProxy 설정 교체 및 reload
cp /etc/haproxy/haproxy-dc2.cfg /etc/haproxy/haproxy.cfg
haproxy -c -f /etc/haproxy/haproxy.cfg # 설정 검증
systemctl reload haproxy
# 방법 2: DNS TTL을 미리 낮춰뒀다면 DNS 레코드 업데이트
# (AWS Route53 예시)
aws route53 change-resource-record-sets \
--hosted-zone-id Z1234567890ABC \
--change-batch '{
"Changes": [{
"Action": "UPSERT",
"ResourceRecordSet": {
"Name": "db.example.com",
"Type": "A",
"TTL": 60,
"ResourceRecords": [{"Value": "10.2.0.10"}]
}
}]
}'
# 전환 후 실제 트래픽 라우팅 확인
psql -h db.example.com -p 5000 -U appuser -c \
"SELECT inet_server_addr(), pg_is_in_recovery();"
Step 6: 승격 후 즉시 체크리스트
□ DC2 Primary에서 쓰기 테스트 성공
□ 애플리케이션 연결 정상 (에러 로그 없음)
□ DC2 내부 Replica 복제 재개 확인
□ 알림 채널에 "DC2 승격 완료" 공지
□ DC1 복구 계획 수립 시작 (Part 3의 demote-cluster 절차 참조)
4. Runbook C — 네트워크 파티션 (부분 연결 장애)
트리거 조건: 일부 노드만 통신 불가 / 클러스터 일부가 고립됨
핵심 판단 기준: etcd Quorum이 유지되는가?
# etcd 클러스터 상태 즉시 확인
etcdctl $ETCD_CERTS \
--endpoints=https://10.1.0.10:2379,https://10.2.0.10:2379,https://10.3.0.10:2379 \
endpoint health --write-out=table
# 출력 예시:
# +------------------------+--------+---------------------------+
# | ENDPOINT | HEALTH | ERROR |
# +------------------------+--------+---------------------------+
# | https://10.1.0.10:2379 | true | |
# | https://10.2.0.10:2379 | true | |
# | https://10.3.0.10:2379 | false | context deadline exceeded |
# +------------------------+--------+---------------------------+
# -> 2/3 노드 정상 = Quorum 유지 -> 클러스터 정상 운영 가능
etcdctl $ETCD_CERTS \
--endpoints=https://10.1.0.10:2379,https://10.2.0.10:2379,https://10.3.0.10:2379 \
member list -w table
케이스별 대응
케이스 1: etcd Quorum 유지 (2/3 이상 정상)
-> 자동으로 처리됨. 모니터링만 지속.
-> 고립된 노드가 Primary였다면 자동 페일오버 진행 확인
-> 네트워크 복구 후 고립 노드 자동 재합류 확인
케이스 2: etcd Quorum 손실 (과반 미만 정상)
-> 클러스터 전체가 읽기 전용 모드로 전환됨
-> 네트워크 복구 최우선
-> 복구 불가 시 Runbook D (etcd 장애) 참조
-> DCS Failsafe Mode가 활성화된 경우: 모든 멤버에게 직접 연결 시도
# 네트워크 파티션 중 Patroni 로그 모니터링
journalctl -fu patroni | grep -E "DCS|leader|promote|demote|failover"
# 파티션 해소 후 각 노드 상태 확인
for NODE in $DC1_NODES; do
echo "=== $NODE ==="
curl -s --max-time 3 http://$NODE:8008/patroni 2>/dev/null \
| python3 -c "import sys,json; d=json.load(sys.stdin); \
print(f'role={d[\"role\"]}, timeline={d[\"timeline\"]}')" \
|| echo "응답 없음"
done
5. Runbook D — etcd 클러스터 장애
트리거 조건: etcd 전체 또는 과반 노드 다운 / Patroni 로그에 "DCS not accessible" 빈번 출력
즉각 결과: 모든 Patroni 클러스터 읽기 전용 전환 (DCS Failsafe Mode가 비활성화된 경우)
Step 1: etcd 상태 파악
# etcd 클러스터 상태 확인
etcdctl $ETCD_CERTS \
--endpoints=https://10.1.0.10:2379,https://10.2.0.10:2379,https://10.3.0.10:2379 \
endpoint status --write-out=table
# 정상 etcd 노드 수 확인
HEALTHY=$(etcdctl $ETCD_CERTS \
--endpoints=https://10.1.0.10:2379,https://10.2.0.10:2379,https://10.3.0.10:2379 \
endpoint health 2>/dev/null | grep -c "is healthy")
echo "정상 etcd 노드: $HEALTHY / 3"
if [ "$HEALTHY" -ge 2 ]; then
echo "Quorum 유지 중 -> Patroni 자동 복구 대기"
else
echo "Quorum 손실! 즉각 복구 필요"
fi
Step 2: etcd 노드 재시작 시도
# 장애 etcd 노드에서 (예: 싱가포르 노드)
ssh pg-singapore-1
# 디스크 공간 및 메모리 확인 (etcd 다운의 흔한 원인)
df -h /var/lib/etcd
free -h
journalctl -u etcd --since "10 minutes ago" | tail -50
# etcd 재시작
systemctl restart etcd
# 재시작 후 클러스터 합류 확인
etcdctl $ETCD_CERTS \
--endpoints=https://10.1.0.10:2379 \
member list
Step 3: etcd 완전 재구성 (최후 수단)
etcd 데이터가 손상되어 재시작으로 복구 불가능한 경우에만 사용한다. Patroni는 patroni.dynamic.json에서 DCS 설정을 복원하므로, 데이터 디렉토리만 초기화해도 클러스터 설정은 자동 재등록된다.
# 이 절차는 etcd 데이터를 완전히 초기화한다.
# 1. Patroni 일시 정지 (자동 페일오버 방지)
patronictl -c $PATRONI_CONF pause $DC1_CLUSTER --wait
# 2. 모든 etcd 노드에서 서비스 중지 및 데이터 삭제
for NODE in $DC1_NODES; do
ssh $NODE "systemctl stop etcd && rm -rf /var/lib/etcd/data"
done
# 3. 서울 노드(첫 번째)에서 새 클러스터 초기화
# etcd.conf.yml: initial-cluster-state: new 확인 후
ssh 10.1.0.10 "systemctl start etcd"
# 4. 도쿄, 싱가포르 노드 추가
# etcd.conf.yml에서 initial-cluster-state를 "existing"으로 변경 후 시작
ssh 10.2.0.10 "systemctl start etcd"
ssh 10.3.0.10 "systemctl start etcd"
# 5. Patroni 재개
patronictl -c $PATRONI_CONF resume $DC1_CLUSTER --wait
# 6. Patroni가 patroni.dynamic.json에서 설정 복원 확인
patronictl -c $PATRONI_CONF show-config
patronictl -c $PATRONI_CONF list
6. Runbook E — Split-Brain 감지 및 긴급 복구
트리거 조건:
- PMM/모니터링 시스템에서 Primary 노드 2개 이상 감지
- HAProxy stats에서 두 노드 모두
/primary200 응답 - 애플리케이션 레벨에서 데이터 불일치 감지
⚠️ Split-Brain은 최고 등급의 긴급 상황이다. 모든 쓰기를 즉시 차단하고 복구를 시작한다. 낮은 LSN 노드를 살리면 더 많은 데이터가 손실된다.
Step 1: Split-Brain 확정 확인
# 모든 노드의 역할과 LSN 즉시 확인
echo "=== Split-Brain 진단 ==="
for NODE in $DC1_NODES $DC2_NODES; do
echo -n "$NODE: "
psql -h $NODE -U postgres -t --no-align -c \
"SELECT inet_server_addr()::text || ' | is_recovery=' || pg_is_in_recovery()::text
|| ' | lsn=' || CASE WHEN pg_is_in_recovery()
THEN pg_last_wal_replay_lsn()::text
ELSE pg_current_wal_lsn()::text END;" 2>/dev/null \
|| echo "연결 실패"
done
# 두 노드에서 is_recovery=false가 나오면 Split-Brain 확정
Step 2: 즉각 쓰기 차단
판단하는 동안 추가 쓰기를 방지하기 위해 두 Primary 모두 차단한다.
# iptables로 두 Primary 모두의 5432 포트 즉시 차단
for NODE in $DC1_NODES $DC2_NODES; do
ssh $NODE "iptables -I INPUT -p tcp --dport 5432 -j DROP" &
done
wait
echo "모든 노드 쓰기 차단 완료"
Step 3: 신규 Primary 결정 (LSN 기준)
LSN이 높은 노드가 더 많은 데이터를 가진 노드다. 반드시 이 노드를 살린다.
# LSN 비교
echo "=== LSN 비교 ==="
for NODE in $DC1_NODES $DC2_NODES; do
LSN=$(psql -h $NODE -U postgres -t --no-align -c \
"SELECT pg_current_wal_lsn();" 2>/dev/null)
echo "$NODE: LSN = $LSN"
done
# LSN이 낮은 노드(데이터가 적은 쪽)를 중지
# 예시: pg-seoul-1이 구 Primary로 판명된 경우:
ssh 10.1.0.10 "systemctl stop patroni; systemctl stop postgresql"
Step 4: 데이터 손실 범위 파악
# 분기 발생 시점 확인 (pg_waldump 활용)
# 구 Primary에서 (중지된 노드):
pg_waldump -n 100 --path=/var/lib/postgresql/17/main/pg_wal \
--start=<분기_LSN> 2>/dev/null | head -50
# 손실된 트랜잭션 목록 작성 (비즈니스 복구용)
# PostgreSQL 로그에서 분기 이후 커밋된 트랜잭션 확인
grep "COMMIT" /var/log/postgresql/postgresql-*.log \
| tail -100 > /tmp/lost_transactions.txt
Step 5: 살아남은 Primary로 클러스터 정상화
# 살아남은 Primary (LSN이 높은 쪽)에서 iptables 차단 해제
ssh $SURVIVING_PRIMARY "iptables -D INPUT -p tcp --dport 5432 -j DROP"
# 구 Primary를 Replica로 재초기화
# (data 디렉토리 삭제 후 basebackup으로 재동기화)
ssh $OLD_PRIMARY "rm -rf /var/lib/postgresql/17/main/*"
ssh $OLD_PRIMARY "systemctl start patroni"
# 재합류 확인
patronictl -c $PATRONI_CONF list
# iptables 규칙 전체 정리
for NODE in $DC1_NODES $DC2_NODES; do
ssh $NODE "iptables -D INPUT -p tcp --dport 5432 -j DROP" 2>/dev/null
done
7. DR 훈련 시나리오 (Chaos Engineering)
DR 훈련은 최소 분기별 1회 수행하는 것이 권장된다. 훈련을 하지 않은 Runbook은 실제 장애 때 작동하지 않는다는 것이 현업에서 반복적으로 증명되어 왔다. 훈련에는 반드시 정해진 역할과 시간 제한이 있어야 하며, 각 훈련은 측정 가능한 결과를 남겨야 한다.
훈련 준비
# 훈련 전 현재 데이터 상태 스냅샷
psql -U postgres -h $CURRENT_PRIMARY -c "
CREATE TABLE IF NOT EXISTS dr_drill_log (
drill_id TEXT,
event TEXT,
ts TIMESTAMPTZ DEFAULT now(),
lsn PG_LSN DEFAULT pg_current_wal_lsn()
);
INSERT INTO dr_drill_log (drill_id, event)
VALUES ('drill_$(date +%Y%m%d_%H%M)', 'drill_start');
"
# 훈련 중 지속 쓰기 부하 생성 (별도 터미널, 선택 사항)
# pgbench -h $CURRENT_PRIMARY -U postgres -d postgres \
# -c 5 -j 2 -T 300 --no-vacuum &
시나리오 1: Primary 노드 프로세스 강제 종료
목표: 자동 페일오버 완료 시간 측정 / Watchdog 동작 확인
echo "=== 훈련 시나리오 1 시작: $(date) ==="
# Primary 노드의 Patroni 프로세스 강제 종료 (kill -9)
PRIMARY_NODE=$(patronictl -c $PATRONI_CONF list \
| grep Leader | awk '{print $2}' | cut -d: -f1)
echo "현재 Primary: $PRIMARY_NODE"
ssh $PRIMARY_NODE "kill -9 \$(pgrep -f 'patroni')"
FAILOVER_START=$(date +%s)
# 페일오버 완료까지 대기 및 시간 측정
while true; do
NEW_LEADER=$(patronictl -c $PATRONI_CONF list 2>/dev/null \
| grep Leader | awk '{print $2}')
if [ -n "$NEW_LEADER" ] && [ "$NEW_LEADER" != "$PRIMARY_NODE:5432" ]; then
FAILOVER_END=$(date +%s)
echo "페일오버 완료: $NEW_LEADER"
echo "소요 시간: $((FAILOVER_END - FAILOVER_START))초"
break
fi
sleep 1
done
# 검증
patronictl -c $PATRONI_CONF topology
psql -h $HAPROXY_HOST -p 5000 -U postgres -c \
"SELECT inet_server_addr(), pg_is_in_recovery();"
시나리오 2: 네트워크 파티션 시뮬레이션 (iptables)
목표: 리전 간 네트워크 절단 시 etcd Quorum 동작 확인
# 이 시나리오는 Primary 노드에서 실행하면 실제 페일오버가 발생한다.
# 반드시 Replica 노드에서 실행하거나, 사전에 팀에 공지한다.
echo "=== 훈련 시나리오 2: 리전 간 파티션 시뮬레이션 ==="
REPLICA_NODE="10.2.0.10"
ssh $REPLICA_NODE "iptables -I INPUT -s 10.1.0.0/24 -j DROP
iptables -I OUTPUT -d 10.1.0.0/24 -j DROP"
PARTITION_START=$(date +%s)
echo "파티션 시작. 30초 후 복구..."
sleep 30
# 파티션 해소
ssh $REPLICA_NODE "iptables -D INPUT -s 10.1.0.0/24 -j DROP
iptables -D OUTPUT -d 10.1.0.0/24 -j DROP"
PARTITION_END=$(date +%s)
echo "파티션 지속 시간: $((PARTITION_END - PARTITION_START))초"
# 복구 후 클러스터 상태 확인
sleep 15 # 재연결 안정화 대기
patronictl -c $PATRONI_CONF topology
시나리오 3: etcd 노드 1개 장애
목표: etcd Quorum 유지 하에 클러스터 안정성 확인
echo "=== 훈련 시나리오 3: etcd 노드 1개 장애 ==="
# 싱가포르 etcd 중단
ssh 10.3.0.10 "systemctl stop etcd"
ETCD_STOP=$(date)
echo "etcd 중단 시각: $ETCD_STOP"
# 클러스터 정상 운영 확인 (Quorum 2/3 유지)
etcdctl $ETCD_CERTS \
--endpoints=https://10.1.0.10:2379,https://10.2.0.10:2379 \
endpoint health
# Patroni 클러스터 정상 여부
patronictl -c $PATRONI_CONF list
# 쓰기 테스트 (정상이어야 함)
psql -h $HAPROXY_HOST -p 5000 -U postgres -c \
"INSERT INTO dr_drill_log (drill_id, event)
VALUES ('drill_$(date +%Y%m%d)', 'etcd_node_down_write_test');"
# 10분 후 etcd 복구
sleep 600
ssh 10.3.0.10 "systemctl start etcd"
# etcd 재합류 확인
etcdctl $ETCD_CERTS \
--endpoints=https://10.1.0.10:2379,https://10.2.0.10:2379,https://10.3.0.10:2379 \
member list
시나리오 4: Standby Cluster 승격 훈련 (계획된 전환)
목표: 수동 승격 절차 숙달 / RTO 측정
echo "=== 훈련 시나리오 4: Standby Cluster 계획된 승격 훈련 ==="
DRILL_START=$(date +%s)
# DC1을 계획적으로 중지 (훈련이므로 정상 절차)
patronictl -c $PATRONI_CONF pause $DC1_CLUSTER --wait
for NODE in $DC1_NODES; do
ssh $NODE "systemctl stop patroni" &
done
wait
# DC2 Standby 승격 (Runbook B Step 4 참조)
patronictl -c /etc/patroni/patroni.yml promote-cluster $DC2_CLUSTER
PROMOTE_TIME=$(date +%s)
echo "승격 소요 시간: $((PROMOTE_TIME - DRILL_START))초"
# 쓰기 테스트
psql -h 10.2.0.10 -U postgres -c \
"INSERT INTO dr_drill_log (drill_id, event)
VALUES ('drill_$(date +%Y%m%d)', 'standby_promoted');"
# -- 훈련 완료 후 원복 --
# DC1 재기동 (Standby 모드로 재전환)
# DC2에 Permanent Slot 등록 후 DC1 Patroni 재시작
# (Part 3 Step 6 참조)
DR 훈련 결과 기록 양식
매 훈련마다 아래 양식을 작성해 팀 위키에 보관한다.
## DR 훈련 기록
- 훈련 일시: YYYY-MM-DD HH:MM ~ HH:MM
- 훈련 참여자: DBA, 인프라 담당자 이름
- 시나리오: [ ] 1 (Primary kill) [ ] 2 (파티션) [ ] 3 (etcd) [ ] 4 (Standby 승격)
### 측정 결과
| 항목 | 목표값 | 실측값 | 달성 여부 |
|---------------------------|------------------|--------|-----------|
| RTO (서비스 복구 시간) | 5분 이내 | __ 분 | OK/NG |
| RPO (데이터 손실) | 0 (sync)/30초 | __ 초 | OK/NG |
| 페일오버 감지 시간 | 30초 이내 | __ 초 | OK/NG |
| 애플리케이션 재연결 시간 | 60초 이내 | __ 초 | OK/NG |
### 발견된 문제점 및 개선 사항
- [ ] Runbook 단계 X의 명령어 오류 발견 -> 수정 필요
- [ ] 특정 노드 SSH 키 만료 발견
- [ ] HAProxy DNS 전환 시간이 목표보다 긺
### 다음 훈련 일정
- 예정일: YYYY-MM-DD
- 시나리오: (이번 훈련에서 발견된 취약점 위주로 선정)
8. 장애 대응 의사결정 트리
실제 장애 상황에서 어떤 Runbook을 실행할지 빠르게 판단하기 위한 플로우차트다. 첫 번째 질문은 항상 patronictl list를 실행할 수 있는가이다.
9. 복구 후 Post-Mortem 체크리스트
장애 복구 완료 후 24시간 이내에 Post-Mortem을 작성한다. 단순한 사후 보고가 아니라, 재발 방지 액션 아이템을 만드는 것이 목적이다. Post-Mortem은 복구가 완전히 끝났을 때 비로소 닫힌다.
즉각 확인 (복구 직후 30분 이내)
# 클러스터 최종 상태 스냅샷
patronictl -c $PATRONI_CONF topology > /tmp/post_recovery_topology.txt
patronictl -c $PATRONI_CONF show-config > /tmp/post_recovery_config.txt
# 복제 지연이 0으로 수렴했는지 확인
psql -h $NEW_PRIMARY -U postgres -c "
SELECT application_name, state, sync_state,
write_lag, flush_lag, replay_lag
FROM pg_stat_replication;
"
# 데이터 일관성 기본 확인 (주요 테이블 행 수 비교)
for NODE in $DC1_NODES; do
echo -n "$NODE: "
psql -h $NODE -U postgres -d mydb -t --no-align -c \
"SELECT COUNT(*) FROM orders;" 2>/dev/null || echo "접근 불가"
done
# etcd 상태 최종 확인
etcdctl $ETCD_CERTS \
--endpoints=https://10.1.0.10:2379,https://10.2.0.10:2379,https://10.3.0.10:2379 \
endpoint health --write-out=table
# Watchdog 상태 확인
curl -s http://localhost:8008/patroni | python3 -m json.tool \
| grep -E "role|watchdog|timeline"
Post-Mortem 문서 작성 양식
## Post-Mortem 보고서
- 장애 발생 시각: YYYY-MM-DD HH:MM:SS (KST)
- 장애 감지 시각: (모니터링 알림 시각)
- 복구 완료 시각: YYYY-MM-DD HH:MM:SS (KST)
- 총 다운타임: __ 분 __ 초
- 영향 범위: 쓰기 불가 / 읽기 불가 / 완전 중단
- 담당자: DBA 이름
### 타임라인
| 시각 | 이벤트 |
|---------|---------------------------|
| T+00:00 | 장애 발생 (추정) |
| T+00:XX | 모니터링 알림 수신 |
| T+00:XX | 담당자 온콜 응답 |
| T+00:XX | 장애 원인 파악 완료 |
| T+00:XX | 복구 작업 시작 |
| T+00:XX | 서비스 복구 완료 |
### 근본 원인 (Root Cause)
(5-Why 분석 결과)
### 즉각 조치 사항
- (완료) 기존 조치 내용
- (진행 중) 조치 중인 내용
- (예정) 향후 조치 예정 내용
### 재발 방지 액션 아이템
| 항목 | 담당자 | 기한 | 상태 |
|---------------------------------|----------|------------|------|
| Watchdog mode를 required로 변경 | DBA | YYYY-MM-DD | [] |
| etcd 디스크 I/O 알림 추가 | 인프라 | YYYY-MM-DD | [] |
| DR 훈련 월 1회로 증가 | DBA 리더 | YYYY-MM-DD | [] |
### Runbook 개선 사항
(이번 장애에서 Runbook의 어떤 부분이 부정확했는가?)
이 시리즈의 다음 글인 Part 6에서는 멀티 리전 Patroni 클러스터의 일상 운영을 지탱하는 모니터링·자동화·Best Practices를 다룬다. Prometheus + Grafana 기반 Patroni 대시보드 구성, patroni_exporter 핵심 알림 규칙, pgBackRest 멀티 리전 백업 전략, 자주 쓰는 patronictl 명령어 치트 시트가 포함된다.
참고 자료
- Patroni 공식 문서 - FAQ
- Percona - PostgreSQL HA with Patroni: Your Turn to Test Failure Scenarios
- Percona - How to Perform a Disaster Recovery Switchover with Patroni (2025)
- Stormatics - Understanding Patroni Failovers
- DEV Community - PostgreSQL High Availability: Patroni, Replication and Failover Patterns