CNPG HA

Hermaeus Mora · · DB

operator 1.28.1 (kustomizations/cnpg_operator/kustomization.yaml:5). HA slot 기본 on.

slot: 어디까지 추적했는지 기록 + 그 이후 WAL 못 지우게 강제". 기록만 하는 게 아니라 primary 리소스 보존을 강제하는 게 핵심.

physical slot (CNPG HA slot 이 쓰는 것)

  • restart_lsn 보관 → primary가 그 LSN 이후 WAL segment 삭제 금지
  • slot 없으면 wal_keep_size 추측 게임. replica 좀 느리면 WAL 잘림 → replica 깨짐. slot 있으면 확정 보장

logical slot

  • confirmed_flush_lsn + catalog_xmin 보관
  • WAL 뿐 아니라 vacuum이 catalog dead tuple 못 지우게 막음 (decoding에 필요해서)

함정 — slot은 양날

  • 소비자 죽었는데 slot 남으면 WAL 무한 누적 → pg_wal 디스크 full → primary 다운. slot이 replica 아니고 primary 를 죽임
  • 안전장치 max_slot_wal_keep_size (기본 -1 = 무제한). 차트에 세팅 없음 → prod 도 무제한
  • slot은 primary 로컬 상태 (pg_replslot/), standby로 복제 안 됨. 그래서 CNPG가 synchronizeReplicas 로 직접 standby 쪽 slot 을 맞춰줌 — failover 후 남은 replica들이 새 primary 에 재연결할 때 필요한 WAL 남아있게

완충재: barman WAL archive 도 돌고 있음 (plugins: barman-cloud, isWALArchiver) → slot 놓쳐도 replica가 object store 에서 WAL fetch 가능. 그래서 slot 사고 치명도 낮음.

볼 만한 것

max_slot_wal_keep_size 안 걸려있는 게 prod 유일 실질 리스크. 2-instance 라 standby 하나 장기 down 시 pg_wal 무한 증가. archive 있으니 값 걸어도 복구 가능 — 예: prod values 에 max_slot_wal_keep_size: "20GB" (storage 기본 20Gi 기준이면 더 낮게).

실제 slot 상태(pg_replication_slots, wal_status, lag)는 psql exec 가 auto mode classifier 에 막혔음. 확인 원하면 직접:

wal을 왜 지워 ?

디스크 무한 아님. WAL은 계속 쌓이는 append-only 스트림 → 안 지우면 무조건 full.

WAL 역할 = 임시 보험

WAL 의 존재 이유: 데이터 페이지를 디스크에 fsync 하기 전에 변경을 먼저 순차 기록. 크래시 나면 WAL 재생(redo)해서 복구.

즉 WAL 은 "아직 데이터 파일에 반영 안 된 변경" 백업. 반영되면 → 그 WAL 쓸모 없음.

checkpoint = 지워도 되는 시점 만들기

  1. checkpoint 발생 → shared_buffers 의 dirty page 전부 데이터 파일로 flush + fsync
  2. flush 끝나면 그 시점 이전 WAL은 redo 에 불필요 (데이터 파일이 이미 최신)
  3. → 삭제/재활용 대상

이게 없으면 크래시 복구 때 DB 생성 시점부터 전체 WAL 재생해야 함. 복구 시간 무한.

실제로는 "지운다" 보다 "재활용"

WAL은 16MB 고정 세그먼트 파일. 불필요해진 세그먼트는 unlink 하지 않고 이름만 미래 번호로 rename 해서 재사용 (recycle). 새 파일 생성/fsync 비용 회피 + ext4 같은 데서 미리 할당된 블록 재사용.

min_wal_size(기본 80MB) 만큼은 항상 재활용 풀로 유지. max_wal_size(기본 1GB) 초과하면 checkpoint 강제 유발 + 초과분은 진짜 삭제.

정리: 삭제 보류시키는 것들

WAL 세그먼트는 모든 소비자가 끝났을 때만 지워짐:

┌───────────────────────────────────┬──────────────────────────────────┐ │ 보류 이유 │ 제어 │ ├───────────────────────────────────┼──────────────────────────────────┤ │ checkpoint 아직 안 됨 (redo 필요) │ max_wal_size, checkpoint_timeout │ ├───────────────────────────────────┼──────────────────────────────────┤ │ archive 아직 안 올라감 │ archive_command / barman plugin │ ├───────────────────────────────────┼──────────────────────────────────┤ │ replication slot 이 잡고 있음 │ max_slot_wal_keep_size │ ├───────────────────────────────────┼──────────────────────────────────┤ │ 보수적 여유분 │ wal_keep_size │ └───────────────────────────────────┴──────────────────────────────────┘

앞 글의 사고 시나리오가 여기서 나옴: checkpoint 끝났고 barman archive 도 성공했는데, 죽은 slot 하나가 restart_lsn 을 잡고 있어서 재활용 금지 → 세그먼트 계속 신규 생성 → pg_wal full → primary 정지.

그래서 max_slot_wal_keep_size 는 "그 한도 넘으면 slot 을 버려라(wal_status=lost)" 라는 뜻. replica 하나 포기 ↔ primary 살리기 트레이드. prod-db 는 barman archive 있으니 포기해도 replica 재구축 가능.

barman이 있어서 복구는 되는거지 ?

→ slot 이 lost 되어 replica 가 깨져도 objectstore 에서 WAL fetch 해서 따라잡음. 최악엔 base backup + WAL replay 로 재구축. 데이터 손실 없음.

근데 barman 이 막아주는 건 데이터, 가용성 아님

앞 시나리오 다시: 죽은 slot 이 WAL 잡음 → pg_wal full → primary 정지. 이건 barman 이 안 막아준다. archive 는 이미 성공했는데도 slot 때문에 로컬 세그먼트 재활용이 금지되는 상황이라서.

즉 max_slot_wal_keep_size 의 가치 = 데이터 보호 아니라 primary 다운 회피. barman 있으니 slot 버리는 비용이 싸다 → 오히려 값을 걸어야 하는 이유. storage 50Gi 니 max_slot_wal_keep_size: "10GB" 정도.

지금 상황에서 split brain이 일어날 수 있나 ?

영구 split brain: 안 일어남. 구조적으로 막힘. 근데 짧은 dual-writer 창은 존재. 그리고 그보다 async 데이터 손실이 더 현실적 위험.

왜 막히나 — 3중

  1. 승격 결정이 etcd 단일 필드로 직렬화

CNPG 는 Patroni 처럼 노드끼리 leader 경쟁 안 함. operator 가 유일 심판, 상태는 Cluster CR 의 targetPrimary 한 필드:

currentPrimary: ncp-cnpg-2 targetPrimary: ncp-cnpg-2 timeline: 2

각 pod 의 instance manager 는 targetPrimary == 자기 이름 일 때만 승격. 단일 필드 + optimistic concurrency(resourceVersion) → 두 pod 가 동시에 자기를 target 으로 볼 수 없음. operator 자체도 leader election 걸려있어 중복 심판 없음.

  1. 쓰기 라우팅이 단일 타겟

ncp-cnpg-rw selector: cnpg.io/instanceRole=primary ncp-cnpg-ro selector: cnpg.io/instanceRole=replica ncp-cnpg-r selector: cnpg.io/podRole=instance ← 둘 다 포함

instanceRole=primary 라벨은 operator 만 붙임. 현재 ncp-cnpg-2 만 보유 → -rw endpoint 도 198.18.4.227 하나. 구 primary 프로세스가 read-write 로 살아있어도 라벨 잃으면 -rw 로 트래픽 안 옴.

  1. 앱이 실제로 -rw 만 씀 — 확인함

10 ncp-pgproxy-rw.prod-db.svc.cluster.local 2 ncp-cnpg-rw.prod-db.svc.cluster.local

-r 이나 pod DNS(ncp-cnpg-1.ncp-cnpg-r...)로 쓰는 앱 0개. 이게 제일 중요. -r 로 쓰기 하는 앱이 있었으면 파티션 때 구 primary 로 계속 write 들어갔음.

남아있는 창

apiserver 파티션 시나리오. ncp-cnpg-2 가 있는 노드가 kube-apiserver 만 잃고 pod network 은 유지:

  1. operator: pod NotReady → ncp-cnpg-1 승격, timeline 3
  2. 구 primary 의 instance manager 는 API 못 봐서 demote 지시 못 받음 → PG 는 여전히 read-write
  3. 라벨 제거는 operator 가 etcd 에서 하므로 -rw endpoint 는 새 primary 로 이동 → 앱은 새 primary 로 감

→ dual writer 는 되지만 쓰기 유입 경로가 없음. 창은 endpoint 전파 지연(수초) 수준.

구 primary 복귀 시 CNPG 가 pg_rewind 로 divergence 정리. 격리 중 커밋된 게 있으면 조용히 버려짐. 그게 split brain 피해의 실체 — 영구 두 마스터가 아니라 "받아놓고 폐기된 쓰기".

실제로 더 위험한 것

minSyncReplicas: 0 maxSyncReplicas: 0 synchronous_commit: (unset → local flush only) failoverDelay: 0

전부 async + failover 즉시. primary 죽으면 replica 로 아직 안 넘어간 WAL = 손실 확정. failoverDelay: 0 이라 grace period 도 없음 → 순간 장애에도 바로 승격.

split brain 보다 이쪽이 훨씬 자주 발생할 시나리오. 33일 전 실제 failover 한 번 있었음 (ncp-cnpg-1 137d/restart 1, ncp-cnpg-2 33d/primary, timeline 2).

switchoverDelay: 3600 은 계획 switchover 때 구 primary clean shutdown 1시간까지 대기 — 안전한 쪽 기본값, 그대로 두면 됨.

운영만 되면 된다 → 손 댈 것 없음

split brain 은 현재 구성에서 실질 리스크 아님. 굳이 하나 고른다면 failoverDelay: 30 정도 (일시적 network blip 에 불필요한 승격+데이터손실 방지). sync replication 은 instances 2 에서 켜면 replica 하나 죽을 때 primary 쓰기가 멈추니 켜지 마라 — 가용성이 내려감.