LabHub
블로그

블로그

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

한국어English日本語中文

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-rostandby 들만읽기 분산
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 는 맨 마지막에 손댑니다. 그 마지막 단계를 어떻게 할지가 두 값으로 정해집니다.

마이너 판올림(18.4 → 18.5)은 imageName 만 바꾸면 이 절차로 굴러갑니다.

설정은 CR 에, reload 와 restart 는 오퍼레이터가 가른다

postgresql.parameters 에 적은 값은 오퍼레이터가 postgresql.conf 로 옮기고, 그 항목이 reload 로 되는지 restart 가 필요한지를 판단해 알맞은 쪽으로 처리합니다. 이 클러스터가 손댄 것은 둘입니다.

백업 — 두 종류가 합쳐져야 복구가 된다

가장 오해가 많은 부분입니다. "백업을 찍었다" 는 말은 절반만 말한 것입니다.

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 의 방향입니다.

그 밖의 기능

실제 클러스터에서 확인되는 것과 함정

여기서부터는 문서가 아니라 겪은 것입니다.

상태 필드가 멈춰 있다. 백업은 매일 찍히는데 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 가 누구인지, 그 앞의 주소가 무엇인지, 어제로 되돌릴 수 있는지" 를 사람이 아니라 컨트롤러가 항상 답하게 하는 것입니다. 이 클러스터는 그 셋이 갖춰져 있고, 남은 숙제는 복구 훈련을 주기적으로 하는 것, 인스턴스 셋과 동기 복제, 그리고 지표를 켜는 것입니다.

로그인하면 좋아요를 누를 수 있습니다

댓글

아직 댓글이 없습니다.

로그인하면 댓글을 쓸 수 있습니다