- Published on
CloudNativePG 딥다이브 — PostgreSQL 을 쿠버네티스 안에서 누가 돌보는가
- Authors

- Name
- Youngju Kim
- @fjvbn20031
PostgreSQL 은 스스로를 돌보지 못한다
PostgreSQL 은 훌륭한 데이터베이스이지만, 컨테이너에 넣는다고 해서 자기 복제본을 만들고, 주(primary)가 죽었을 때 대기(standby)를 승격시키고, 매일 백업을 찍고, 어제 오후 세 시로 되돌리는 일을 스스로 하지는 않습니다. 그 일은 지금까지 사람이 했습니다.
CloudNativePG(CNPG)는 그 사람의 일을 쿠버네티스 컨트롤러가 계속 조정(reconcile)하는 것으로 바꾼 오퍼레이터입니다. 이 글은 CNPG 가 무엇을 어떻게 하는지를, 실제로 돌고 있는 운영 클러스터의 값으로 설명합니다.
오퍼레이터 cloudnative-pg 1.30.0 + plugin-barman-cloud v0.14.0
클러스터 labhub-db-prod — PostgreSQL 18.4, 인스턴스 2, 볼륨 5Gi(NFS)
지금 상태 primary = labhub-db-prod-1, standby = labhub-db-prod-2, timeline 3
설계가 남다른 두 지점
StatefulSet 을 쓰지 않는다
쿠버네티스에서 상태 있는 것은 StatefulSet 으로 띄운다는 것이 상식입니다. CNPG 는 그러지 않습니다. 파드 하나하나를 오퍼레이터가 직접 만들고 지웁니다.
이유는 데이터베이스의 인스턴스가 서로 같지 않기 때문입니다. StatefulSet 은 "0번부터 순서대로, 똑같은 템플릿으로" 를 전제합니다. 그런데 데이터베이스에서는 "2번 인스턴스만 새 볼륨으로 다시 만들어 복제를 처음부터 받게 한다", "primary 는 마지막에 건드린다" 같은 결정이 필요합니다. 그 결정을 내리려면 파드를 개별로 다룰 수 있어야 합니다.
파드의 PID 1 은 postgres 가 아니다
CNPG 의 파드 안에서 1번 프로세스는 인스턴스 매니저입니다. 그것이 postgres 를 자식으로 띄우고, 준비 상태·활성 상태 프로브에 답하고, 승격 명령을 받고, WAL 을 보관소로 밀어 내고, 설정이 바뀌면 다시 읽습니다.
이 구조의 이점은 두 가지입니다. 오퍼레이터가 죽어도 데이터베이스는 계속 돕니다(매니저는 파드 안에 있습니다). 그리고 "이 인스턴스가 정말 살아 있는가" 를 쿠버네티스가 TCP 포트가 아니라 PostgreSQL 의 실제 응답으로 판단합니다.
역할별로 뜯어 보기
클러스터는 CR 하나다
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata: { name: labhub-db-prod }
spec:
instances: 2
imageName: ghcr.io/cloudnative-pg/postgresql:18.4-system-trixie
storage: { size: 5Gi, storageClass: nfs-synology }
primaryUpdateStrategy: unsupervised
primaryUpdateMethod: restart
postgresql:
parameters: { wal_level: logical, archive_timeout: 5min }
replicationSlots: { highAvailability: { enabled: true } }
plugins:
- name: barman-cloud.cloudnative-pg.io
isWALArchiver: true
이 하나의 객체가 아래에 나오는 파드·서비스·비밀·인증서·복제·백업을 전부 만듭니다. 설정을 바꾸는 방법도 하나뿐입니다 — 이 CR 을 고치면 오퍼레이터가 차이를 계산해 필요한 만큼만 움직입니다.
복제와 역할
인스턴스 하나가 primary 이고 나머지는 스트리밍 복제로 따라오는 standby 입니다. primary 에서 본 지금 상태입니다.
application_name | state | sync_state
labhub-db-prod-2 | streaming | async
async 는 primary 가 커밋을 확정할 때 standby 의 확인을 기다리지 않는다는 뜻입니다. 빠르지만, primary 가 죽는 순간 standby 에 아직 도착하지 않은 트랜잭션은 잃습니다. 이 클러스터는 지연이 사실상 0 이지만 그것은 보장이 아니라 지금의 부하가 작아서입니다.
복제 슬롯 HA 가 켜져 있어 standby 가 잠시 끊겨도 primary 가 그 standby 가 아직 못 받은 WAL 을 지우지 않습니다. 이것이 없으면 네트워크가 몇 분만 끊겨도 standby 를 처음부터 다시 만들어야 합니다.
세 개의 서비스
앱은 파드 이름으로 붙지 않습니다. 오퍼레이터가 만든 세 서비스 중 하나로 붙습니다.
| 서비스 | 가리키는 곳 | 용도 |
|---|---|---|
labhub-db-prod-rw | 지금 primary 하나 | 쓰기. 앱은 여기에 붙는다 |
labhub-db-prod-ro | standby 들만 | 읽기 분산 |
labhub-db-prod-r | 아무 인스턴스 | 읽기(primary 포함) |
페일오버가 나면 오퍼레이터가 -rw 의 엔드포인트만 바꿉니다. 앱은 연결이 끊긴 것을 보고 다시 붙기만 하면 됩니다. 앱 설정에 primary 의 주소가 적혀 있지 않다는 것, 이것이 데이터베이스를 옮기고 교체할 자유의 출발점입니다.
페일오버와 스위치오버, 그리고 timeline
primary 가 죽으면 오퍼레이터가 가장 앞선 standby 를 골라 승격시킵니다. 이때 PostgreSQL 은 timeline 을 하나 올립니다 — 역사가 갈라졌다는 표시입니다. 이 클러스터는 timelineID: 3 입니다. 지금까지 두 번 primary 가 바뀐 이력이 그 숫자에 남아 있습니다.
계획된 교체는 스위치오버입니다. kubectl cnpg promote labhub-db-prod labhub-db-prod-2 로 standby 를 지목하면, 오퍼레이터가 옛 primary 를 정리하고 새 primary 를 세우고 옛 것을 standby 로 되돌립니다. 데이터 손실 없이 역할만 바뀝니다.
롤링 업데이트
이미지나 재시작이 필요한 설정이 바뀌면 오퍼레이터는 standby 부터 갈아 끼우고, primary 는 맨 마지막에 손댑니다. 그 마지막 단계를 어떻게 할지가 두 값으로 정해집니다.
primaryUpdateStrategy: unsupervised— 사람 승인 없이 진행한다(supervised면 primary 앞에서 멈추고 기다린다).primaryUpdateMethod: restart— primary 를 제자리에서 재시작한다(switchover면 먼저 standby 를 승격시켜 primary 의 중단 시간을 줄인다).
마이너 판올림(18.4 → 18.5)은 imageName 만 바꾸면 이 절차로 굴러갑니다.
설정은 CR 에, reload 와 restart 는 오퍼레이터가 가른다
postgresql.parameters 에 적은 값은 오퍼레이터가 postgresql.conf 로 옮기고, 그 항목이 reload 로 되는지 restart 가 필요한지를 판단해 알맞은 쪽으로 처리합니다. 이 클러스터가 손댄 것은 둘입니다.
wal_level: logical— 논리 복제와 변경 데이터 캡처(CDC)가 가능하도록.archive_timeout: 5min— 다음 절의 WAL 보관 주기.
백업 — 두 종류가 합쳐져야 복구가 된다
가장 오해가 많은 부분입니다. "백업을 찍었다" 는 말은 절반만 말한 것입니다.
WAL 보관(archiving). PostgreSQL 은 모든 변경을 먼저 WAL(쓰기 전 로그)에 씁니다. CNPG 는 WAL 조각이 차거나 archive_timeout(5분)이 지날 때마다 그 조각을 오브젝트 스토리지로 밀어 냅니다. 즉 가장 최근 5분 안의 변경은 아직 보관소에 없을 수 있습니다 — 이것이 이 클러스터의 RPO(복구 시점 목표)입니다.
베이스 백업. 데이터 디렉터리 전체의 사본입니다. 이 클러스터는 ScheduledBackup 이 매일 03:30 UTC 에 찍고, 최근 사흘 것이 모두 completed 입니다.
복구는 둘을 합칩니다. 베이스 백업을 풀고, 그 뒤의 WAL 을 원하는 시각까지 재생합니다. 그래서 "어제 오후 세 시" 로 돌아갈 수 있고(PITR), 돌아갈 수 있는 가장 이른 시각이 firstRecoverabilityPoint 로 상태에 적힙니다.
이 클러스터는 백업을 CNPG 에 내장된 barmanObjectStore 가 아니라 CNPG-I 플러그인(plugin-barman-cloud)으로 합니다. 그래서 spec.backup 이 비어 있고 대신 plugins[].isWALArchiver: true 가 서 있습니다. 백업 로직을 오퍼레이터 본체에서 떼어 낸 것이 최근 CNPG 의 방향입니다.
그 밖의 기능
- 선언적 롤·DB —
managed.roles와DatabaseCR 로 사용자와 데이터베이스를 YAML 로 관리. - 인증서 — 서버·클라이언트 TLS 인증서를 오퍼레이터가 발급하고 갱신.
- Pooler —
PoolerCR 하나로 PgBouncer 를 앞에 세운다. 연결이 수백 개로 늘면 그때. - 지표 —
monitoring.enablePodMonitor: true면 Prometheus 가 긁는다. - 복제 클러스터 — 다른 클러스터(다른 데이터센터)에 standby 를 두는 DR 구성.
- 볼륨 스냅샷 백업, 하이버네이션(볼륨은 두고 파드만 내림), 스토리지 온라인 확장.
실제 클러스터에서 확인되는 것과 함정
여기서부터는 문서가 아니라 겪은 것입니다.
상태 필드가 멈춰 있다. 백업은 매일 찍히는데 status.lastSuccessfulBackup 은 8월 23일에 멈춰 있습니다. 플러그인 방식으로 옮긴 뒤로 Cluster 의 상태 필드와 cnpg_collector_* 지표가 갱신되지 않기 때문입니다. 이 값으로 알림을 걸면 거짓 경보가 됩니다. 판정은 Backup 객체의 phase 와 barman_cloud_* 지표로 해야 합니다.
첫 백업은 복구가 안 될 수 있다. 아카이빙을 켠 직후에 찍은 백업은 completed 로 떠도, 그 백업이 시작된 WAL 위치(beginWal)가 보관소에 없어 복구에 실패한 적이 있습니다. pg_switch_wal() 로 WAL 을 한 번 넘긴 뒤 백업을 다시 찍고, 반드시 복구를 해 봐야 합니다. 백업은 "찍혔다" 가 아니라 "되살려 봤다" 로만 확인됩니다.
async 복제와 인스턴스 둘. 위에서 본 대로 async 는 페일오버 때 마지막 몇 트랜잭션을 잃을 수 있습니다. 동기 복제를 켜면 해결되지만 인스턴스가 둘뿐일 때는 standby 가 죽는 순간 쓰기가 멈추는 반대 위험이 생깁니다. 셋으로 늘린 뒤에 켜는 것이 순서입니다.
NFS 위의 데이터베이스. 볼륨이 NFS(5Gi)입니다. 지금 데이터는 7.8MB 라 용량은 문제가 아니지만, WAL 의 fsync 가 로컬 디스크보다 느리고 CNPG 문서도 로컬 스토리지를 권합니다. 부하가 커지면 첫 번째로 옮길 것입니다.
지표가 꺼져 있다. enablePodMonitor 가 꺼져 있어 Prometheus 에 DB 지표가 없습니다. 연결 수·복제 지연·WAL 보관 실패를 보려면 켜야 합니다.
한 줄로
CNPG 의 역할은 "지금 primary 가 누구인지, 그 앞의 주소가 무엇인지, 어제로 되돌릴 수 있는지" 를 사람이 아니라 컨트롤러가 항상 답하게 하는 것입니다. 이 클러스터는 그 셋이 갖춰져 있고, 남은 숙제는 복구 훈련을 주기적으로 하는 것, 인스턴스 셋과 동기 복제, 그리고 지표를 켜는 것입니다.