4.4 StatefulSet
Deployment의 Pod는 서로 교환 가능하다. 하나가 죽으면 아무 노드에나 새것을 만들고, 이름도 IP도 디스크도 새로 받는다. 무상태 웹 서버에는 이상적인 성질이지만 데이터베이스에는 그대로 적용되지 않는다. PostgreSQL replica는 자기 데이터 디렉터리와 분리되는 순간 의미를 잃고, primary와 replica는 애초에 교환 가능한 존재가 아니다. StatefulSet은 Pod 하나하나에 지워지지 않는 신원을 부여하는 controller다.
ordinal과 headless Service
StatefulSet의 Pod 이름은 임의 문자열이 아니라 0부터 시작하는 ordinal이다. pg-0, pg-1, pg-2. Pod가 죽어 재생성되어도, 다른 노드로 옮겨져도 이 이름은 유지된다. 여기에 headless Service(clusterIP: None)를 serviceName으로 연결하면 Pod마다 고유한 DNS 이름이 생긴다.
pg-0.pg-hs.default.svc.cluster.local일반 Service가 여러 Pod를 하나의 가상 IP 뒤에 숨기는 것과 달리, headless Service는 개별 Pod의 주소를 그대로 드러낸다. “replica는 pg-0을 primary로 바라본다” 같은 복제 설정을 이름으로 적어 둘 근거가 이렇게 만들어진다. Service와 DNS의 동작 원리는 Part V. 네트워킹에서 다룬다.
volumeClaimTemplates: Pod마다 자기 디스크
Deployment에서 volume은 Pod template에 한 번 정의되어 모든 replica가 같은 정의를 공유한다. StatefulSet의 volumeClaimTemplates는 다르다. Pod마다 별도의 PVC를 만들고 이름으로 결합한다. template 이름이 data라면 data-pg-0, data-pg-1이 생기고, pg-0은 언제나 data-pg-0에만 마운트된다. Pod가 재생성되어도, 노드를 옮겨도(스토리지가 노드 간 attach를 지원하는 한) 같은 PVC에 다시 연결된다.
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: pg
spec:
serviceName: pg-hs
replicas: 3
selector:
matchLabels:
app: pg
template:
metadata:
labels:
app: pg
spec:
containers:
- name: postgres
image: postgres:17
env:
- name: POSTGRES_PASSWORD
value: "changeme"
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 10Giscale down을 해도 PVC는 기본적으로 삭제되지 않는다. pg-2를 줄였다가 다시 늘리면 새 pg-2는 남아 있던 data-pg-2에 다시 연결되어 데이터가 이어진다. 삭제까지 자동화하려면 1.32에서 GA된 persistentVolumeClaimRetentionPolicy로 정책을 명시한다. PV와 PVC, StorageClass의 구조는 Part VI. 스토리지에서 다룬다.
순서 보장과 업데이트 전략
기본 podManagementPolicy인 OrderedReady에서 생성은 ordinal 순서를 지킨다. pg-0이 Running이고 Ready가 된 뒤에야 pg-1이 만들어지고, 축소는 역순으로 높은 ordinal부터 내려간다. primary를 먼저 기동하고 replica가 뒤따르는 데이터베이스 초기화와 잘 맞는 순서다. 순서가 필요 없으면 Parallel로 바꿔 동시에 만든다.
pg-0이 Ready에 도달하지 못하면 pg-1 이후는 만들어지지 않고, 롤링 업데이트도 Ready가 되지 않는 Pod 앞에서 정지한다. readiness probe가 데이터베이스의 실제 준비 상태를 정확히 반영하는지가 StatefulSet 운영의 전제가 된다.업데이트 전략의 기본은 RollingUpdate이고, 교체는 높은 ordinal부터 역순으로 하나씩 진행된다. 각 Pod가 Ready로 돌아온 뒤에야 다음으로 넘어간다. 여기에 partition을 지정하면 ordinal이 그 값 이상인 Pod만 교체된다. replicas 3에 partition: 2면 pg-2만 새 버전이 되어 카나리 검증 자리가 되고, 문제가 없으면 partition을 0까지 내려 전체에 반영한다. OnDelete는 자동 교체를 끄고, 직접 삭제한 Pod만 새 template로 재생성한다.
StatefulSet만으로 데이터베이스가 되지 않는 이유
StatefulSet이 보장하는 것을 다시 나열하면 신원, 디스크 고정, 순서다. 전부 데이터베이스의 전제 조건이지만, 데이터베이스 운영 그 자체는 아니다. StatefulSet은 pg-0이 primary라는 사실을 모른다. pg-0의 노드가 죽으면 같은 이름의 Pod를 다시 만들어 줄 뿐, 그동안 쓰기가 멈춘다는 것도, pg-1을 promote해야 할지 말지도 판단하지 않는다. replica 수를 늘려도 basebackup을 받아 복제를 구성하는 일은 저절로 일어나지 않고, 백업과 PITR, failover 뒤 옛 primary의 재합류 같은 절차도 전부 바깥의 일이다.
이 빈자리를 채우는 것이 Operator다. 데이터베이스를 아는 custom controller가 custom resource를 watch하면서 “PostgreSQL 클러스터 하나"라는 선언을 primary 감시, promote 판단, 복제 구성, 백업 스케줄로 번역한다. reconciliation 루프라는 뼈대는 같고, 그 위에 도메인 지식이 더해진 형태다. Operator 패턴의 구조는 Part XII. 패키징과 확장에서 다루고, PostgreSQL Operator의 실제 동작은 별도의 CloudNativePG 노트에 정리해 두었다. 미리 적어 둘 만한 사실 하나는, CloudNativePG가 StatefulSet을 아예 쓰지 않고 Pod와 PVC를 직접 관리한다는 점이다. failover 순서와 볼륨 결합을 StatefulSet의 일반 규칙보다 세밀하게 통제하기 위한 설계다.
정리
Deployment가 “몇 개"를 유지한다면 StatefulSet은 “누가"까지 유지한다. ordinal 이름과 headless DNS, volumeClaimTemplates가 Pod의 주소와 디스크를 신원에 묶고, 생성과 교체는 순서를 지킨다. 다만 이것은 데이터베이스를 실행할 자리를 만들어 주는 것이지 운영해 주는 것이 아니어서, failover 판단과 백업과 복제 구성은 Operator의 영역으로 남는다.