멀티 리전에서 Patroni H/A 운용하기 — Part 6: 모니터링, 운영 자동화 및 Best Practices
Prometheus·Grafana·Alertmanager로 제어 평면과 데이터 평면을 함께 감시하고, pgBackRest 멀티 리전 백업으로 HA의 마지막 안전망을 완성한다. Ansible 자동화, patronictl 치트 시트, 시리즈 전체 Best Practices까지 — 멀티 리전 Patroni 운용 완결편.
이 시리즈의 마지막 파트다. Part 1에서 아키텍처를 설계하고, Part 2-3에서 동기·비동기 복제를 구성했으며, Part 4에서 Split-Brain 방어층을 쌓고, Part 5에서 Runbook과 DR 훈련으로 대응 체계를 갖췄다. 이제 남은 질문은 하나다 — 이 클러스터와 매일 함께 살아가는 방법은 무엇인가?
HA는 한 번 구성하면 끝나는 것이 아니다. 매일 관찰하고, 백업으로 검증하고, 자동화로 반복 가능하게 만들어야 완성된다. 이 파트는 그 운영 체계를 다룬다.
1. Patroni 메트릭 수집 구조 이해
Patroni는 v2.1.0부터 /metrics 엔드포인트를 통해 Prometheus 형식의 메트릭을 네이티브로 제공한다. 별도 exporter 없이 Patroni REST API만으로 클러스터 상태를 수집할 수 있다는 것이 큰 장점이다.
모니터링 스택의 핵심은 Patroni(제어 평면)와 PostgreSQL(데이터 평면), etcd(DCS)를 분리하지 않고 함께 보는 것이다. HA 장애는 데이터 평면이 아니라 제어 평면 신호(DCS heartbeat 지연, 페일오버 pause, etcd leader election 급증)에서 먼저 나타나는 경우가 많다.
Patroni /metrics 주요 메트릭 목록
| 메트릭 이름 | 타입 | 설명 |
|---|---|---|
patroni_primary | Gauge | 1 = 현재 Primary (Leader Lock 보유) |
patroni_replica | Gauge | 1 = 현재 Replica |
patroni_standby_leader | Gauge | 1 = Standby Cluster의 Leader |
patroni_xlog_location | Gauge | 현재 WAL 위치 (LSN, bytes) |
patroni_xlog_received_location | Gauge | 수신된 WAL 위치 (Replica) |
patroni_xlog_replayed_location | Gauge | 재생된 WAL 위치 (Replica) |
patroni_postgres_running | Gauge | PostgreSQL 프로세스 실행 여부 |
patroni_dcs_last_seen | Gauge | DCS(etcd) 마지막 통신 시각 (Unix timestamp) |
patroni_failsafe_mode_is_active | Gauge | DCS Failsafe Mode 활성화 여부 |
patroni_is_paused | Gauge | 자동 페일오버 일시 정지 여부 |
patroni_heartbeat_failed_at | Gauge | 마지막 DCS 하트비트 실패 시각 |
# Patroni /metrics 응답 샘플 확인
curl -s http://10.1.0.10:8008/metrics | grep -E "^patroni_"
# 예상 출력 (Primary 노드):
# patroni_primary 1
# patroni_replica 0
# patroni_postgres_running 1
# patroni_xlog_location 5.36870912e+08
# patroni_dcs_last_seen 1.745780000e+09
# patroni_failsafe_mode_is_active 0
# patroni_is_paused 0
2. Prometheus 스크레이핑 구성
Prometheus 설치 (모니터링 전용 노드, Docker Compose)
# /opt/monitoring/docker-compose.yml
version: '3.8'
services:
prometheus:
image: prom/prometheus:v3.3.1
container_name: prometheus
ports: ["9090:9090"]
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- ./prometheus/rules:/etc/prometheus/rules:ro
- prometheus_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.retention.time=30d'
- '--web.enable-lifecycle'
restart: unless-stopped
grafana:
image: grafana/grafana:11.6.0
container_name: grafana
ports: ["3000:3000"]
volumes:
- grafana_data:/var/lib/grafana
environment:
- GF_SECURITY_ADMIN_PASSWORD=SecureGrafanaPass!
- GF_USERS_ALLOW_SIGN_UP=false
restart: unless-stopped
alertmanager:
image: prom/alertmanager:v0.28.1
container_name: alertmanager
ports: ["9093:9093"]
volumes:
- ./alertmanager/alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro
restart: unless-stopped
volumes:
prometheus_data:
grafana_data:
prometheus.yml — Patroni + PostgreSQL + etcd 스크레이핑
# /opt/monitoring/prometheus/prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
external_labels:
cluster: 'pg-multiregion'
env: 'production'
rule_files:
- "rules/patroni_alerts.yml"
- "rules/postgresql_alerts.yml"
alerting:
alertmanagers:
- static_configs:
- targets: ['alertmanager:9093']
scrape_configs:
# -- Patroni REST API /metrics --
- job_name: 'patroni'
scheme: https
tls_config:
ca_file: /etc/prometheus/ssl/ca.pem
cert_file: /etc/prometheus/ssl/prometheus.pem
key_file: /etc/prometheus/ssl/prometheus-key.pem
static_configs:
- targets:
- '10.1.0.10:8008' # pg-seoul-1
- '10.1.0.11:8008' # pg-seoul-2
- '10.1.0.12:8008' # pg-seoul-3
- '10.2.0.10:8008' # pg-busan-1
- '10.2.0.11:8008' # pg-busan-2
- '10.2.0.12:8008' # pg-busan-3
relabel_configs:
# DC 레이블 자동 부여
- source_labels: [__address__]
regex: '10\.1\..*'
target_label: dc
replacement: 'seoul'
- source_labels: [__address__]
regex: '10\.2\..*'
target_label: dc
replacement: 'busan'
# -- postgres_exporter (PostgreSQL 내부 메트릭) --
- job_name: 'postgres'
static_configs:
- targets:
- '10.1.0.10:9187'
- '10.1.0.11:9187'
- '10.1.0.12:9187'
- '10.2.0.10:9187'
- '10.2.0.11:9187'
- '10.2.0.12:9187'
# -- etcd 메트릭 --
- job_name: 'etcd'
scheme: https
tls_config:
ca_file: /etc/prometheus/ssl/ca.pem
cert_file: /etc/prometheus/ssl/prometheus.pem
key_file: /etc/prometheus/ssl/prometheus-key.pem
static_configs:
- targets:
- '10.1.0.10:2381'
- '10.2.0.10:2381'
- '10.3.0.10:2381'
# -- Node Exporter (OS 레벨) --
- job_name: 'node'
static_configs:
- targets:
- '10.1.0.10:9100'
- '10.1.0.11:9100'
- '10.1.0.12:9100'
- '10.2.0.10:9100'
- '10.2.0.11:9100'
- '10.2.0.12:9100'
postgres_exporter 설치 및 구성
# 모든 PostgreSQL 노드에서 실행
wget https://github.com/prometheus-community/postgres_exporter/releases/download/v0.17.1/postgres_exporter-0.17.1.linux-amd64.tar.gz
tar xzf postgres_exporter-0.17.1.linux-amd64.tar.gz
cp postgres_exporter-0.17.1.linux-amd64/postgres_exporter /usr/local/bin/
# 전용 모니터링 유저 생성 (PostgreSQL)
psql -U postgres -c "
CREATE ROLE prometheus_scraper WITH LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE
PASSWORD 'MonitoringPass123!';
GRANT pg_monitor TO prometheus_scraper;
GRANT CONNECT ON DATABASE postgres TO prometheus_scraper;
"
# systemd 서비스 등록
cat > /etc/systemd/system/postgres_exporter.service <<'EOF'
[Unit]
Description=PostgreSQL Exporter for Prometheus
After=postgresql.service
[Service]
Type=simple
User=postgres
Environment="DATA_SOURCE_NAME=postgresql://prometheus_scraper:MonitoringPass123!@localhost:5432/postgres?sslmode=disable"
Environment="PG_EXPORTER_AUTO_DISCOVER_DATABASES=true"
ExecStart=/usr/local/bin/postgres_exporter \
--web.listen-address=:9187 \
--collector.stat_bgwriter \
--collector.replication \
--collector.replication_slot
Restart=always
RestartSec=5s
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now postgres_exporter
3. 핵심 알림 규칙 (Alert Rules)
알림 규칙은 운영 불변식(operational invariant)을 코드로 표현한 것이다. "Primary가 정확히 1개 있어야 한다", "DCS heartbeat가 TTL의 절반 이내여야 한다" 같은 조건이 깨지면 즉시 알린다.
# /opt/monitoring/prometheus/rules/patroni_alerts.yml
groups:
- name: patroni_critical
rules:
# -- Primary 없음: 클러스터 전체 쓰기 불가 --
- alert: PatroniNoPrimary
expr: sum(patroni_primary) by (cluster) == 0
for: 30s
labels:
severity: critical
annotations:
summary: "Patroni 클러스터에 Primary가 없습니다"
description: >
클러스터 {{ $labels.cluster }}에서 Primary 노드가 감지되지 않습니다.
자동 페일오버 진행 중이거나 모든 노드가 다운된 상태입니다.
즉각 Runbook A 또는 B를 실행하십시오.
# -- Primary 2개 이상: Split-Brain 위험 --
- alert: PatroniSplitBrain
expr: sum(patroni_primary) by (cluster) > 1
for: 0s # 즉시 발동 (지연 없음)
labels:
severity: critical
annotations:
summary: "Split-Brain 감지: Primary가 {{ $value }}개"
description: >
클러스터 {{ $labels.cluster }}에서 Primary가 2개 이상 감지되었습니다.
즉각 Runbook E를 실행하고 모든 쓰기를 차단하십시오.
# -- PostgreSQL 프로세스 다운 --
- alert: PatroniPostgresDown
expr: patroni_postgres_running == 0
for: 30s
labels:
severity: critical
annotations:
summary: "{{ $labels.instance }} PostgreSQL 프로세스 중단"
description: >
{{ $labels.instance }}의 PostgreSQL이 실행되지 않고 있습니다.
Patroni가 자동 복구를 시도 중입니다. 30초 이상 지속 시 수동 확인이 필요합니다.
- name: patroni_warning
rules:
# -- DCS 통신 단절 경고 --
- alert: PatroniDCSUnreachable
expr: time() - patroni_dcs_last_seen > 20
for: 10s
labels:
severity: warning
annotations:
summary: "{{ $labels.instance }} DCS 통신 단절"
description: >
{{ $labels.instance }}이 {{ $value | humanizeDuration }} 동안
etcd와 통신하지 못했습니다. TTL 만료 시 자동 강등됩니다.
# -- 자동 페일오버 비활성화 경고 --
- alert: PatroniAutofailoverPaused
expr: patroni_is_paused == 1
for: 5m
labels:
severity: warning
annotations:
summary: "{{ $labels.instance }} 자동 페일오버 일시 정지"
description: >
patronictl pause가 5분 이상 적용 중입니다.
유지보수 완료 후 patronictl resume을 실행하십시오.
# -- 복제 지연 경고 (500MB 이상) --
- alert: PatroniReplicationLagHigh
expr: >
(patroni_xlog_location - patroni_xlog_replayed_location) > 524288000
for: 2m
labels:
severity: warning
annotations:
summary: "{{ $labels.instance }} 복제 지연 500MB 초과"
description: >
{{ $labels.instance }}의 복제 지연이 {{ $value | humanize1024 }}B입니다.
네트워크 상태 및 디스크 I/O를 확인하십시오.
# -- Replica 수 부족 경고 --
- alert: PatroniInsufficientReplicas
expr: sum(patroni_replica) by (cluster) < 1
for: 1m
labels:
severity: warning
annotations:
summary: "클러스터 {{ $labels.cluster }} 활성 Replica 없음"
description: >
현재 클러스터에 활성 Replica가 없습니다.
Primary 장애 시 자동 페일오버가 불가능한 상태입니다.
alertmanager.yml — Slack + PagerDuty 연동
# /opt/monitoring/alertmanager/alertmanager.yml
global:
resolve_timeout: 5m
route:
receiver: 'default'
group_by: ['alertname', 'cluster', 'dc']
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
routes:
- match:
severity: critical
receiver: 'pagerduty-critical'
repeat_interval: 1h
- match:
severity: warning
receiver: 'slack-warning'
receivers:
- name: 'default'
slack_configs:
- api_url: 'https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK'
channel: '#db-alerts'
- name: 'pagerduty-critical'
pagerduty_configs:
- service_key: 'YOUR_PAGERDUTY_SERVICE_KEY'
description: '{{ .GroupLabels.alertname }}: {{ .CommonAnnotations.summary }}'
- name: 'slack-warning'
slack_configs:
- api_url: 'https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK'
channel: '#db-warnings'
title: '{{ .GroupLabels.alertname }}'
text: '{{ .CommonAnnotations.description }}'
4. Grafana 대시보드 구성
Grafana 대시보드는 장식용 차트가 아니라 장애 판단 화면이다. 어떤 노드가 Primary인지, 복제 지연이 임계값을 넘었는지, DCS 통신이 살아있는지를 3초 안에 파악할 수 있어야 한다.
즉시 임포트 가능한 공식 대시보드 ID
| 대시보드 | Grafana ID | 설명 |
|---|---|---|
| PostgreSQL Patroni (Percona PMM) | 18870 | Patroni /metrics 기반 클러스터 상태 |
| PostgreSQL Overview | 9628 | postgres_exporter 기반 DB 내부 메트릭 |
| pgBackRest Exporter | 17709 | 백업 상태 및 WAL 아카이브 현황 |
| etcd | 3070 | etcd 클러스터 상태 및 Raft 메트릭 |
| Node Exporter Full | 1860 | OS 레벨 CPU/메모리/디스크/네트워크 |
# Grafana API로 대시보드 자동 임포트
# (Grafana.com에서 대시보드 JSON을 먼저 받아온 후 임포트하는 방식)
GRAFANA_URL="http://localhost:3000"
GRAFANA_AUTH="admin:SecureGrafanaPass!"
for ID in 18870 9628 17709 3070 1860; do
echo "Importing dashboard ID: $ID"
curl -s -X POST \
-H "Content-Type: application/json" \
-u "$GRAFANA_AUTH" \
-d "{\"dashboard\": {\"id\": null}, \"folderId\": 0,
\"inputs\": [{\"name\": \"DS_PROMETHEUS\",
\"type\": \"datasource\",
\"pluginId\": \"prometheus\",
\"value\": \"Prometheus\"}],
\"overwrite\": true}" \
"${GRAFANA_URL}/api/dashboards/import/${ID}" \
| python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('status',''))"
done
Grafana 운영 중 반드시 확인할 핵심 패널
매일 확인 (Daily Health Check):
patroni_primary == 1인 노드가 정확히 1개인가?
모든 Replica의 replay_lag이 허용 임계값 이내인가?
patroni_dcs_last_seen이 현재 시각과 가까운가? (20초 이내)
pgBackRest 마지막 백업이 24시간 이내인가?
주간 확인 (Weekly Health Check):
etcd 리더 선출 빈도가 급증하지 않았는가?
노드별 디스크 I/O 및 WAL 생성 속도가 정상인가?
Replication Slot의 WAL 보유량이 과도하게 증가하지 않았는가?
5. pgBackRest 멀티 리전 백업 전략
Patroni와 pgBackRest는 함께 사용할 때 시너지가 크다. 단순한 백업 도구를 넘어, Replica 초기화(create_replica_methods)나 재초기화(reinit) 시에도 pgBackRest를 활용하면 네트워크 부하를 줄이고 대용량 클러스터의 복제 설정 속도를 대폭 높일 수 있다.
고가용성(HA)과 재해 복구(DR)는 다른 개념이다. Patroni 페일오버는 HA를 제공하지만, 데이터를 삭제하거나 스토리지가 손상되면 페일오버로는 복구할 수 없다. pgBackRest가 그 마지막 안전망이다.
멀티 리전 백업 아키텍처
DC1 PostgreSQL 노드들
- archive_command: WAL을 S3(서울) + S3(싱가포르)로 Push
- backup-standby: prefer -> Replica에서 백업 (Primary 부하 분산)
pgBackRest Repository:
repo1: S3 ap-northeast-2 (서울) <- 메인 리포, 7일 보관
repo2: S3 ap-southeast-1 (싱가포르) <- DR 리포, 3일 보관
pgbackrest.conf — 멀티 리포지토리 + S3 설정
# /etc/pgbackrest/pgbackrest.conf (모든 PG 노드 동일 적용)
[global]
process-max=4 # 병렬 처리
start-fast=y
delta=y # 증분 백업 시 변경 블록만 처리
archive-async=y # 비동기 WAL 아카이빙 (쓰기 지연 최소화)
compress-type=lz4
compress-level=3
backup-standby=prefer # Replica에서 백업 우선
# -- 리포지토리 1: S3 서울 (메인) --
repo1-type=s3
repo1-path=/pg-multiregion
repo1-s3-bucket=my-pgbackrest-seoul
repo1-s3-region=ap-northeast-2
repo1-s3-endpoint=s3.ap-northeast-2.amazonaws.com
repo1-s3-uri-style=host
repo1-retention-full=7
repo1-retention-diff=14
repo1-retention-full-type=count
# -- 리포지토리 2: S3 싱가포르 (DR 크로스 리전) --
repo2-type=s3
repo2-path=/pg-multiregion
repo2-s3-bucket=my-pgbackrest-singapore
repo2-s3-region=ap-southeast-1
repo2-s3-endpoint=s3.ap-southeast-1.amazonaws.com
repo2-s3-uri-style=host
repo2-retention-full=3
repo2-retention-full-type=count
log-level-console=info
log-level-file=detail
log-path=/var/log/pgbackrest
# -- 스탠자: 모든 노드 등록 (페일오버 후에도 자동 아카이브 유지) --
[pg-multiregion]
pg1-path=/var/lib/postgresql/17/main
pg1-host=10.1.0.10
pg1-host-user=postgres
pg1-port=5432
pg2-path=/var/lib/postgresql/17/main
pg2-host=10.1.0.11
pg2-host-user=postgres
pg2-port=5432
pg3-path=/var/lib/postgresql/17/main
pg3-host=10.1.0.12
pg3-host-user=postgres
pg3-port=5432
Patroni에서 pgBackRest 통합
# patroni.yml - pgBackRest 연동
bootstrap:
dcs:
postgresql:
parameters:
archive_mode: "on"
archive_command: >
pgbackrest --stanza=pg-multiregion
--config=/etc/pgbackrest/pgbackrest.conf
archive-push %p
restore_command: >
pgbackrest --stanza=pg-multiregion
--config=/etc/pgbackrest/pgbackrest.conf
archive-get %f "%p"
# Replica 초기화를 pgBackRest로 처리 (pg_basebackup 대신)
# 대용량 클러스터에서 basebackup보다 훨씬 빠르고 효율적
method:
pgbackrest:
command: >
pgbackrest --stanza=pg-multiregion
--config=/etc/pgbackrest/pgbackrest.conf
--delta restore
keep_data: True
no_params: True
basebackup:
command: pg_basebackup -R -P --wal-method=stream
백업 스케줄 자동화 (crontab)
# postgres 유저의 crontab에 추가
crontab -u postgres -e
# 전체 백업: 일요일 02:00
0 2 * * 0 pgbackrest --stanza=pg-multiregion --type=full backup --repo=1
# 차등 백업: 월~토 02:00
0 2 * * 1-6 pgbackrest --stanza=pg-multiregion --type=diff backup --repo=1
# DR 리포 전체 백업: 매일 04:00
0 4 * * * pgbackrest --stanza=pg-multiregion --type=full backup --repo=2
# 백업 무결성 검증: 매주 토요일 03:00
0 3 * * 6 pgbackrest --stanza=pg-multiregion verify
# 일일 상태 리포트: 매일 06:00
0 6 * * * pgbackrest info --stanza=pg-multiregion \
| mail -s "[pgBackRest] Daily Backup Status" dba@example.com
PITR 복원 테스트 (월 1회 권장)
실제 PITR 복원을 실행해보지 않은 백업 체계는 검증되지 않은 체계다. 월 1회 이상 별도 테스트 인스턴스에서 복원을 실행해 복원 시간과 데이터 정합성을 직접 확인해야 한다.
# 테스트 인스턴스에서 특정 시점 복원
pgbackrest --stanza=pg-multiregion \
--type=time \
--target="2026-04-26 14:30:00+09" \
--target-action=promote \
--log-level-console=detail \
restore
# 데이터 정합성 확인
psql -U postgres -c "
SELECT COUNT(*), MAX(created_at) FROM orders
WHERE created_at < '2026-04-26 14:30:00+09';
"
6. Ansible로 클러스터 구성 자동화
Ansible을 통해 모든 노드에 일관된 설정을 적용하면 수동 오류를 제거하고, 클러스터가 수십 개로 확장되더라도 동일한 방식으로 관리할 수 있다. 설정 drift(노드 간 설정 불일치)는 조용한 장애의 원인이 된다.
디렉토리 구조
ansible/
inventory/
production.ini
staging.ini
group_vars/
all.yml # 공통 변수 (버전, TTL 등)
dc1.yml # DC1 전용 변수
dc2.yml # DC2 전용 변수
roles/
common/ # OS 기본 설정
etcd/ # etcd 설치/설정
postgresql/ # PostgreSQL 설치
patroni/ # Patroni 설치/설정
haproxy/ # HAProxy 설정
pgbackrest/ # pgBackRest 설치/설정
site.yml # 마스터 플레이북
inventory/production.ini
[dc1_nodes]
pg-seoul-1 ansible_host=10.1.0.10 dc=seoul
pg-seoul-2 ansible_host=10.1.0.11 dc=seoul
pg-seoul-3 ansible_host=10.1.0.12 dc=seoul
[dc2_nodes]
pg-busan-1 ansible_host=10.2.0.10 dc=busan
pg-busan-2 ansible_host=10.2.0.11 dc=busan
pg-busan-3 ansible_host=10.2.0.12 dc=busan
[pg_all:children]
dc1_nodes
dc2_nodes
[haproxy]
haproxy-seoul ansible_host=10.1.0.20
group_vars/all.yml
postgresql_version: "17"
patroni_version: "4.1.2"
etcd_version: "v3.5.17"
patroni_scope: "pg-multiregion"
patroni_namespace: "/db/"
patroni_dcs:
ttl: 30
loop_wait: 10
retry_timeout: 10
maximum_lag_on_failover: 1048576
synchronous_mode: true
patroni_watchdog:
mode: required
device: /dev/watchdog
safety_margin: 5
tls_ca_cert: /etc/etcd/ssl/ca.pem
roles/patroni/tasks/main.yml (핵심 발췌)
---
- name: Patroni 가상환경 및 설치
pip:
name: "patroni[etcd3]"
version: "{{ patroni_version }}"
virtualenv: /opt/patroni
virtualenv_python: python3
become: yes
- name: patroni.yml 배포 (Jinja2 템플릿)
template:
src: patroni.yml.j2
dest: /etc/patroni/patroni.yml
owner: postgres
group: postgres
mode: '0640'
notify: restart patroni
- name: Softdog 커널 모듈 로드
modprobe:
name: softdog
state: present
- name: /dev/watchdog 권한 설정
file:
path: /dev/watchdog
owner: postgres
group: postgres
mode: '0600'
- name: Patroni systemd 서비스 등록 및 활성화
template:
src: patroni.service.j2
dest: /etc/systemd/system/patroni.service
notify:
- systemd daemon-reload
- restart patroni
- name: PostgreSQL systemd 서비스 비활성화 (Patroni가 관리)
systemd:
name: "postgresql@{{ postgresql_version }}-main"
state: stopped
enabled: no
실행 예시
# 전체 클러스터 최초 배포
ansible-playbook -i inventory/production.ini site.yml --ask-vault-pass
# Patroni 설정 변경 후 Rolling Update (노드 1개씩 순차 적용)
ansible-playbook -i inventory/production.ini site.yml \
--tags patroni-config \
--serial 1 \
--ask-vault-pass
# DC1만 업데이트
ansible-playbook -i inventory/production.ini site.yml \
--limit dc1_nodes --tags patroni
# 드라이 런 (변경 사항 미리 확인)
ansible-playbook -i inventory/production.ini site.yml \
--check --diff
7. patronictl 치트 시트
실무에서 매일 쓰는 명령어들을 한 곳에 정리했다. export PATRONI_CONF=/etc/patroni/patroni.yml을 셸 프로파일에 등록해두면 -c $PATRONI_CONF 옵션을 생략할 수 있다.
상태 확인
# 클러스터 토폴로지 (역할, 상태, 복제 지연)
patronictl topology
# 간결한 목록 뷰
patronictl list
# DCS에 저장된 동적 설정 확인
patronictl show-config
# 특정 노드 REST API 상세 정보
curl -s http://10.1.0.10:8008/patroni | python3 -m json.tool
# 헬스 체크 엔드포인트 전체 확인
for EP in /primary /replica /synchronous /standby-leader /health; do
echo -n "$EP: $(curl -s -o /dev/null -w '%{http_code}' http://10.1.0.10:8008$EP)"
echo ""
done
# 클러스터 타임라인 이력 (페일오버 발생 이력)
patronictl history
페일오버 / 역할 전환
# 계획된 Switchover (다운타임 없음)
# 반드시 Replica가 동기화 상태임을 먼저 확인한다
patronictl switchover \
--master pg-seoul-1 --candidate pg-seoul-2 \
--scheduled now --force
# 강제 Failover (Primary 없을 때 수동 트리거)
# 전제 조건: patronictl list에서 Primary가 0개인 상태임을 확인
patronictl failover pg-multiregion \
--candidate pg-tokyo-1 --force
# Standby Cluster 승격 (Patroni 4.1+, Runbook B 참조)
patronictl promote-cluster pg-busan-standby
# Primary -> Standby Cluster 전환
patronictl demote-cluster pg-seoul-cluster
노드 관리
# 특정 노드 재초기화 (데이터 재동기화)
# 주의: 해당 노드의 데이터 디렉토리를 삭제하고 재동기화한다
patronictl reinit pg-multiregion pg-seoul-1 --force
# 특정 노드 재시작
patronictl restart pg-multiregion pg-seoul-1
# 자동 페일오버 일시 정지 (유지보수 시작 전)
patronictl pause pg-multiregion --wait
# 자동 페일오버 재개 (유지보수 완료 후)
patronictl resume pg-multiregion --wait
설정 관리
# DCS 동적 설정 수정 (전체 클러스터에 실시간 적용)
patronictl edit-config
# 단일 파라미터 즉시 변경
patronictl edit-config --set synchronous_mode=true --force
# PostgreSQL 설정 reload (재시작 불필요)
patronictl reload pg-multiregion
# PostgreSQL 재시작 (재시작 필요한 파라미터 변경 후)
patronictl restart pg-multiregion --scheduled now --force
8. 운영 Best Practices 총정리
이 시리즈 전체를 통해 얻은 핵심 교훈을 한 곳에 정리한다.
설계
etcd 노드는 반드시 홀수(3, 5, 7개)로 3개 이상의 DC에 분산 배치한다
3DC가 불가능하다면 2DC + Standby Cluster로 설계한다
RPO=0이 필요하다면 synchronous_mode: true를 사용한다
TTL, loop_wait, retry_timeout은 리전 간 RTT를 반드시 고려해 조정한다
etcd와 PostgreSQL이 동일 노드라면 디스크 I/O 경합을 주의한다
보안
리전 간 모든 통신(etcd peer/client, Patroni REST API)에 mTLS를 적용한다
replicator, rewind_user 계정은 최소 권한 원칙을 따른다
patroni.yml의 비밀번호는 Vault 또는 Secret Manager로 관리한다
Patroni REST API에 TLS 클라이언트 인증을 설정한다
안정성
모든 Primary 후보 노드에 Watchdog(mode: required)을 활성화한다
PostgreSQL systemd 서비스를 disabled 상태로 유지한다 (Patroni가 단독 관리)
postgresql.service가 enabled 상태이면 Split-Brain의 조용한 원인이 된다
Permanent Replication Slot으로 WAL이 조기 삭제되지 않도록 한다
페일오버 전후로 patronictl pause/resume을 반드시 사용한다
백업
pgBackRest를 사용하고 backup-standby: prefer로 Primary 부하를 줄인다
백업 리포지토리를 2개 이상의 다른 리전에 유지한다 (3-2-1 규칙)
월 1회 이상 실제 PITR 복원 테스트를 수행한다
archive_command 실패를 즉각 알림으로 탐지한다
모든 Patroni 노드를 pgbackrest.conf에 등록해 페일오버 후에도 아카이브를 유지한다
모니터링
patroni_primary == 1인 노드가 항상 정확히 1개인지 알림을 설정한다
patroni_dcs_last_seen이 TTL의 절반을 초과하면 경고를 발생시킨다
복제 지연(xlog_location 차이)에 임계값 알림을 설정한다
patroni_is_paused가 5분 이상 지속되면 경고를 발생시킨다
etcd 클러스터 리더 선출 빈도가 급증하면 즉각 조사한다
운영 문화
patronictl topology를 매일 확인하는 루틴을 팀에 정착시킨다
Runbook은 분기별 DR 훈련을 통해 검증하고 즉시 업데이트한다
장애 후에는 반드시 Post-Mortem을 작성하고 액션 아이템을 추적한다
Patroni 버전 업그레이드는 Replica -> Primary 순으로 Rolling Update한다
연습하지 않은 Runbook은 실제 장애 때 작동하지 않는다
9. 시리즈 마무리
6개 파트에 걸쳐 멀티 리전 Patroni HA 운영의 처음부터 끝까지를 함께 살펴봤다.
Part 1: 왜 멀티 리전인가? 어떤 아키텍처를 선택해야 하는가?
Part 2: 3DC 동기 복제로 RPO=0과 자동 페일오버를 구현한다
Part 3: 2DC 비동기 + Standby Cluster로 비용과 유연성을 확보한다
Part 4: Watchdog, STONITH, etcd Quorum으로 Split-Brain을 원천 차단한다
Part 5: 장애 유형별 Runbook과 DR 훈련으로 팀의 실전 대응력을 키운다
Part 6: 모니터링, 자동화, Best Practices로 클러스터를 건강하게 유지한다
Patroni는 PostgreSQL HA의 사실상 표준(de facto standard)이지만, 그 힘은 도구 자체가 아니라 도구를 이해하고 올바르게 운영하는 팀에서 나온다.
오늘 소개한 내용이 여러분의 데이터베이스가 밤새 조용히, 그리고 안전하게 서비스를 유지하는 데 실질적인 도움이 되길 바란다.
참고 자료
- Patroni 공식 문서
- Patroni GitHub — patroni/patroni
- Grafana Dashboard 18870 — PostgreSQL Patroni (Percona)
- Percona - Monitoring a PostgreSQL Patroni Cluster
- pgstef's blog — Patroni and pgBackRest Combined
- PGConf.EU 2025 — Patroni and pgBackRest: Better Together (Stefan Fercot)
- DEV Community — PostgreSQL HA: Patroni, Replication and Failover Patterns