Skip to content
Published on

CloudNativePG로 쿠버네티스에 Postgres 띄우고 죽여보기 — 페일오버 23초 실측

공유하기
Authors

들어가며 — Postgres를 오퍼레이터에게 맡긴다는 것

쿠버네티스에서 상태 있는(stateful) 데이터베이스를 돌리는 건 오래 금기처럼 여겨졌습니다. CloudNativePG(CNPG)는 그 금기를 정면으로 깨는 오퍼레이터입니다 — Postgres의 고가용성·페일오버·백업·롤링 업그레이드를 CRD 하나로 선언하면, 오퍼레이터가 프라이머리 선출과 레플리카 관리를 대신해 줍니다. 이 글은 소개가 아니라 실측입니다: 진짜 8노드 클러스터에 CNPG를 깔고, 3인스턴스 Postgres를 띄우고, 정말로 프라이머리를 죽여서 페일오버가 몇 초 걸리는지 재봤습니다. 오퍼레이터를 직접 짜 본 Rust GPU 오퍼레이터 편과 짝을 이루는, "잘 만든 오퍼레이터를 쓰는" 쪽의 이야기입니다.

1부 — 설치: 매니페스트 하나

CNPG 설치는 단출합니다. 릴리스 매니페스트 하나를 apply하면 끝입니다.

kubectl apply --server-side -f \
  https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.30/releases/cnpg-1.30.0.yaml

이 한 줄이 CRD 여러 개(clusters, poolers, scheduledbackups, publications, subscriptions 등), cnpg-system 네임스페이스의 컨트롤러 디플로이먼트, RBAC, 웹훅을 모두 깝니다. 컨트롤러가 뜨는 데 20초쯤 걸렸습니다:

$ kubectl -n cnpg-system rollout status deploy/cnpg-controller-manager
deployment "cnpg-controller-manager" successfully rolled out

NAME                      READY   UP-TO-DATE   AVAILABLE   AGE
cnpg-controller-manager   1/1     1            1           16s

2부 — 3인스턴스 클러스터 만들기

Postgres 클러스터는 Cluster CR로 선언합니다. 프라이머리 1 + 레플리카 2, 즉 3인스턴스로 잡았습니다.

apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: pg-test
  namespace: cnpg-test
spec:
  instances: 3
  storage:
    size: 1Gi
    storageClass: nfs-client     # 홈랩의 NFS — 함정은 5부에서
  bootstrap:
    initdb:
      database: appdb
      owner: appuser

apply하면 오퍼레이터가 부트스트랩을 시작합니다. 상태 전이를 실시간으로 지켜봤습니다:

Setting up primary
→ Waiting for the instances to become active
→ ready=1  Creating a new replica       ← 프라이머리 뜬 뒤 레플리카 복제 시작
→ ready=2  Creating a new replica
→ ready=3  Cluster in healthy state      ← 약 2분 만에 3/3 정상

3개 인스턴스는 서로 다른 노드(cubi02·cubi03·cubi04)에 자동 분산됐습니다 — 오퍼레이터가 anti-affinity로 한 노드에 몰리지 않게 합니다. CNPG는 접속용 서비스도 셋 만들어 줍니다:

서비스           역할
────────────  ─────────────────────────────
pg-test-rw     읽기·쓰기 → 항상 현재 프라이머리로 라우팅
pg-test-ro     읽기 전용 → 레플리카들로 로드밸런싱
pg-test-r      아무 인스턴스나 (읽기)

-rw 서비스가 핵심입니다 — 애플리케이션은 이 이름 하나만 바라보면, 페일오버로 프라이머리가 바뀌어도 자동으로 새 프라이머리에 연결됩니다.

3부 — 복제 확인

프라이머리에 1000행을 넣고 세 인스턴스가 모두 같은지 봤습니다.

$ 프라이머리(pg-test-1)에 INSERT 1000행
INSERT 0 1000

$ 각 인스턴스 행 수
  pg-test-1 (primary): 1000 rows
  pg-test-2 (replica): 1000 rows      ← 복제됨
  pg-test-3 (replica): 1000 rows      ← 복제됨

스트리밍 복제가 즉시 세 노드를 동기화했습니다. 이제 진짜 실험입니다.

4부 — 프라이머리를 죽인다: 페일오버 23.1초

가장 궁금했던 것 — 프라이머리가 갑자기 사라지면 몇 초 만에 복구될까? --grace-period=0 --force로 프라이머리 파드를 즉사시키고, 새 프라이머리가 나올 때까지 0.5초 간격으로 폴링하며 시간을 쟀습니다.

=== FAILOVER TEST: killing primary pg-test-1 ===
deleted at t0; polling for new primary...
=== NEW PRIMARY: pg-test-2 (was pg-test-1) ===
failover time: 23.1 s

  rows after failover: 1000        ← 데이터 무손실

23.1초 만에 pg-test-2가 새 프라이머리로 승격됐고, 1000행이 그대로 살아 있었습니다. 그리고 죽었던 pg-test-1은 버려지지 않습니다 — 오퍼레이터가 자동으로 다시 데려와 레플리카로 재편입시킵니다:

=== 자가 복구 후 최종 역할 ===
  pg-test-1: replica     ← 죽었다 살아나 레플리카로 강등 복귀
  pg-test-2: primary     ← 승격된 새 프라이머리
  pg-test-3: replica

$ 새 프라이머리에 추가 쓰기 → 정상, 총 1500행

새 프라이머리는 곧바로 쓰기를 받았고(1000→1500행), 클러스터는 다시 3/3 healthy로 돌아왔습니다. 사람 개입 0번. 이게 오퍼레이터의 값어치입니다.

5부 — 정직한 숫자와 함정

블로그는 검증된 것만 쓴다는 원칙대로, 이 실험의 한계도 그대로 남깁니다.

  • 23초는 빠른가? 상황에 따라 다릅니다. CNPG의 페일오버 시간은 프라이머리 사망 감지(헬스체크 주기), 레플리카 승격, 그리고 -rw 서비스 엔드포인트 갱신의 합입니다. 프로덕션에서는 파드 삭제가 아니라 노드 장애가 더 흔하고, 그 경우 노드 감지 시간(node-monitor-grace-period 등)이 더해져 더 길어질 수 있습니다. 반대로 튜닝하면 더 짧아집니다. "23초"는 이 홈랩·이 설정의 실측치이지 보편 상수가 아닙니다.
  • NFS 스토리지의 함정. 저는 nfs-client(NFS provisioner) 스토리지를 썼는데, Postgres를 NFS 위에 올리는 건 프로덕션에서는 권장되지 않습니다 — fsync 보장·파일 잠금 이슈 때문입니다. 홈랩 테스트로는 잘 돌았지만, 실서비스라면 로컬 SSD나 블록 스토리지(Ceph RBD 등)를 써야 합니다.
  • 동기 vs 비동기 복제. 이 테스트는 기본(비동기) 복제였습니다. 비동기에서는 이론상 프라이머리 사망 직전 극소량의 미복제 트랜잭션이 유실될 수 있습니다. 무손실이 필요하면 CNPG의 동기 복제(minSyncReplicas)를 켜야 하고, 그 대가로 쓰기 지연이 늘어납니다.

마치며

CNPG는 "쿠버네티스에서 DB는 위험하다"는 통념을 실측으로 반박합니다 — 매니페스트 하나로 3노드 HA Postgres가 뜨고, 프라이머리를 즉사시켜도 23초 만에 스스로 복구하며, 죽은 노드는 레플리카로 되돌아옵니다. 물론 스토리지·복제 모드·페일오버 튜닝이라는 진짜 숙제가 남지만, 그건 "DB를 오퍼레이터에게 맡길 수 있는가"의 문제가 아니라 "어떻게 잘 맡길까"의 문제입니다. 다음엔 백업(ScheduledBackup)과 특정 시점 복구(PITR)를 같은 방식으로 죽여보며 검증해 볼 참입니다.

참고 자료