스테이트풀셋: 복제된 스테이트풀 애플리케이션 배포
- 10.1 상태 있는 Pod를 복제하기 어려운 이유
- 10.2 StatefulSet이 제공하는 것
- 10.3 StatefulSet 사용하기
- 10.4 피어 디스커버리 — headless service + SRV
- 10.5 노드 장애 처리
10장은 한 마디로 "상태를 가진 클러스터드 애플리케이션을 쿠버네티스에서 어떻게 돌리나"에 대한 장이야. 분산 DB 같은 거. 지금까지의 ReplicaSet/Deployment는 Pod를 다 똑같은 복제품으로 취급했거든. 근데 분산 DB는 각 인스턴스가 자기만의 스토리지와 안정적인 identity(이름·호스트명)를 가져야 해. 이를 위해 만들어진 게 StatefulSet이야.
먼저 ReplicaSet으로 왜 안 되는지부터 보자. ReplicaSet의 Pod 템플릿이 PVC를 참조하면 모든 replica가 같은 PVC를 공유해. 같은 PV를 가리키니 파일시스템 충돌은 물론이고, RWO 액세스 모드면 한 노드에만 마운트돼서 나머지 Pod는 뜨지도 못해. 즉 ReplicaSet으로는 "각자 자기 디스크를 가진 DB 노드 3개"를 못 만들어. 억지로 우회하는 방법이 몇 가지 있긴 한데 다 어설퍼. Pod를 수동으로 하나씩 만들면 노드 장애 시 자동 복구가 안 되고, ReplicaSet을 Pod 수만큼 하나씩 만들면 replicas 조절이 귀찮고 확장성이 최악이고, 같은 PV 안에 디렉터리를 나눠서 각 Pod가 자기 디렉터리를 쓰게 하는 건 코디네이션을 직접 구현해야 하고 스토리지가 병목이 돼.
또 하나의 문제는 안정적인 identity야. 많은 분산 앱은 설정 파일에 클러스터 멤버 목록(hostname이나 IP)을 박아두거든. Kafka 브로커가 broker-0, broker-1, broker-2 같은 식으로. 근데 RS의 Pod는 재스케줄되면 이름도 IP도 바뀌어. 매번 클러스터 멤버십을 재구성해야 하는 꼴이지. 서비스를 Pod마다 하나씩 두는 꼼수도 있는데, 이번엔 Pod 자신이 "내가 어느 서비스 뒤에 있는지"를 몰라. 자기 정체성을 자기가 모르는 문제야. 결론적으로 ReplicaSet과 Service로는 이 요구를 깔끔하게 풀 수 없어. 그래서 StatefulSet이라는 전용 컨트롤러가 필요해.
StatefulSet이 ReplicaSet과 다른 점은 세 가지야. 첫째 안정적인 identity. Pod 이름이 랜덤이 아니라 <statefulset>-<ordinal> 형태야. mongodb-0, mongodb-1, mongodb-2처럼. 이 이름은 Pod가 지워지고 다시 떠도 같은 이름으로 돌아와. 호스트명도 마찬가지야. Pod가 노드 장애로 다른 노드에 재스케줄돼도 identity는 유지돼.
둘째 각 Pod마다 전용 스토리지. Pod 템플릿이 PVC를 참조하는 게 아니라, StatefulSet에 **volumeClaimTemplates**가 있어. Pod마다 PVC가 하나씩 따로 찍혀 나와 — data-mongodb-0, data-mongodb-1 같은 식으로. 각 PVC는 자기만의 PV에 바인딩돼. Pod가 삭제·재생성돼도 같은 ordinal의 PVC와 다시 결합하니까 데이터가 유지돼. 재밌는 디테일이 하나 있어 — StatefulSet을 scale down 해도 PVC는 안 지워져. 일부러 그렇게 설계됐어. 상태 있는 데이터를 scale down 실수로 날려먹으면 안 되니까. 정말 지우고 싶으면 수동으로 PVC를 삭제해야 해. scale up하면 동일한 ordinal의 PVC에 다시 붙고.
셋째 순서 보장. Pod를 ordinal 순서대로 생성하고 삭제해. mongodb-0이 Ready가 된 다음에야 mongodb-1이 시작되고, scale down은 역순이라 mongodb-2가 먼저 내려가고 그 다음 mongodb-1이 내려가. 분산 DB 부트스트랩 순서가 중요한 경우에 필수야. 그리고 한 가지 — StatefulSet은 반드시 headless Service가 연관되어야 해 (5장의 clusterIP: None). 이유는 Pod마다 개별 DNS 이름이 필요하기 때문이야. headless service를 만들어두면 각 Pod가 <pod-name>.<service-name>.<namespace>.svc.cluster.local 형태의 개별 FQDN을 갖게 돼. mongodb-0.mongodb.default.svc.cluster.local 이런 식. 이 이름은 Pod 수명을 넘어 안정적이라서 분산 앱 설정 파일에 박아두기 좋아.
StatefulSet YAML 구조는 Deployment와 비슷하지만 두 가지가 추가돼. serviceName으로 헤드리스 서비스를 지정하고, volumeClaimTemplates에 PVC의 틀을 적어. 각 Pod에 대해 PVC가 하나씩 찍혀 나와. Pod 하나를 수동으로 삭제해보면 재밌는 걸 볼 수 있어 — 같은 ordinal, 같은 이름, 같은 호스트명으로 새 Pod가 뜨고, 새 Pod에 같은 PVC가 마운트되어 이전 데이터가 그대로 보여. 반면 ReplicaSet이었다면 이름이 완전히 다른 Pod가 떴을 거야.
분산 앱은 보통 "내 형제 노드들이 누구야?"를 알아야 하는데, 쿠버네티스에선 headless service의 DNS SRV 레코드로 해결해. dig SRV kubia.default.svc.cluster.local처럼 SRV 조회를 하면 그 서비스에 속한 모든 Ready Pod의 DNS 이름 목록이 돌아와. kubia-0.kubia...., kubia-1.kubia.... 이런 식. 분산 앱이 시작할 때 이 목록을 조회해서 peer들에게 연결하면 돼. 추가 디테일 — readiness probe가 실패한 Pod는 기본적으로 이 SRV 응답에 포함되지 않아. "아직 준비 안 된 Pod까지 peer로 보고 싶다"면 서비스에 publishNotReadyAddresses: true를 설정해야 해. Cassandra 같은 앱이 부트스트랩 중에 서로를 찾아야 할 때 필요한 설정이야. StatefulSet 업데이트는 kubectl edit나 kubectl apply로 할 수 있지만 Deployment에 비해 업데이트 기능이 제한적이야 (책 집필 시점 기준). 실전에선 수동으로 Pod를 지우면 새 템플릿으로 재생성되는 식으로 구르는 경우도 많았어.
마지막으로 노드 장애 처리. 여기가 StatefulSet의 제약이 드러나는 부분이야. 노드가 네트워크에서 떨어져서 Unreachable 상태가 되면 쿠버네티스는 그 노드의 Pod를 바로 다른 노드로 옮기지 않아. 이유가 중요한데, 만약 옮겨버리면 "같은 identity를 가진 Pod가 두 곳에 동시에 존재"할 위험이 있거든. ReplicaSet이면 Pod 이름이 달라도 괜찮지만, StatefulSet은 identity와 스토리지가 묶여있으니까 두 Pod가 같은 PV에 동시에 쓰는 split-brain이 발생할 수 있어. 데이터 박살나는 거지. 그래서 쿠버네티스는 Unreachable 노드의 Pod를 자동 재스케줄하지 않아. 해당 Pod는 Unknown 상태로 남아. 그 노드가 정말 죽었다고 확신할 수 있을 때 수동으로 Pod를 강제 삭제해야 StatefulSet이 다른 노드에 재생성해 — kubectl delete po mongodb-0 --force --grace-period 0. --force --grace-period 0 플래그 조합이 "정말 확신하니까 즉시 삭제해라"라는 신호야. 프로덕션에서 함부로 쓰면 안 되는 명령이고, 진짜로 노드가 죽었는지 먼저 확인해야 해. 그렇지 않으면 위에 말한 split-brain이 일어날 수 있어. 이 "수동 개입이 필요하다"는 점이 StatefulSet의 운영 부담이야. 근데 데이터 안정성을 위해 필요한 트레이드오프야. "자동화보다 안전"이 원칙.
정리
10장 읽고 기억할 거 세 가지:
- StatefulSet은 각 Pod에 안정적인 identity(
name-0,name-1...)와 전용 스토리지(volumeClaimTemplates로 PVC 찍어내기)를 제공한다. Pod가 죽었다 살아나도 같은 이름, 같은 호스트명, 같은 PVC로 돌아온다. scale down해도 PVC는 지워지지 않아서 데이터가 보존된다. - 헤드리스 Service와 세트로 써야 한다. Pod마다 개별 DNS 이름(
pod-name.service-name...)이 생기고, DNS SRV 레코드로 peer 목록을 조회할 수 있어서 분산 앱이 클러스터 멤버십을 찾는 표준 메커니즘이 된다. 분산 DB 설정에 Pod 이름을 박아둘 수 있는 이유. - 노드 장애 시 자동 재스케줄이 안 된다. Unreachable 노드의 StatefulSet Pod는
Unknown으로 남고, 수동으로--force --grace-period 0으로 지워야 재생성된다. 이건 버그가 아니라 같은 identity의 Pod가 동시에 존재해 split-brain을 일으키는 걸 막기 위한 의도된 설계다.