K8s 고가용성을 위한 Pod Anti-Affinity
1. 특정 노드에 파드가 몰려서 터지는 문제
백엔드 데이터베이스로 활용되던 NoSQL의 분산 클러스터(Cassandra, ElasticSearch 등)를 쿠버네티스의 스테이트풀셋(StatefulSet)으로 포팅하던 시기입니다. 복제본(Replica) 설정이 3인 데이터베이스를 쿠버네티스에 던졌더니, K8s 스케줄러가 유휴 자원이 많은 워커 노드 1대에 파드 3개를 모두 몰빵해서 스케줄링해버리는 참사가 벌어졌습니다. 만약 해당 노드의 하드웨어가 셧다운되면 복제본 3개가 동시에 죽어 전체 DB 클러스터가 붕괴되는 단일 장애점(SPOF, Single Point of Failure) 아키텍처가 된 상황이었습니다.
2. Affinity와 Anti-Affinity 규칙
쿠버네티스의 기본 스케줄러 로직은 노드의 가용 자원만 볼 뿐이지, ‘가용성(HA)’ 측면에서 서로 떨어뜨려야 한다는 로직을 자동 강제하지 않습니다. 특정 라벨(Label)을 가진 파드끼리 절대 같은 노드(혹은 존 단위)에 공존하지 마라고 룰을 주어야 하는데, 이를 Pod Anti-Affinity (파드 안티-어피니티)라고 부릅니다.
3. Topology Key 기준의 분산 배치 룰 작성 및 구현
당시 카산드라(Cassandra) 스테이트풀셋을 예시로, 파드들이 서로 각기 다른 랙(Rack) 혹은 노드 호스트 이름(kubernetes.io/hostname) 단위로만 뜨게 강제 룰을 지정한 매니페스트 수정본입니다.
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: cassandra
spec:
serviceName: cassandra-headless
replicas: 2
template:
metadata:
labels:
app: cassandra
spec:
# 파드 스케줄링 배치 전략 (Affinity) 설정 부분
affinity:
podAntiAffinity:
# requiredDuringScheduling... : '반드시 지켜야 하는 강력한 필수 룰' (Hard 규칙)
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
# 자신과 'app: cassandra'가 동일한 라벨을 가진 Pod를 회피함
- key: app
operator: In
values:
- cassandra
# 평가 기준위상(Topology): 즉, 서로 호스트네임이 무조건 달라야 함 (한 노드에 중복 불가)
topologyKey: "kubernetes.io/hostname"
# (생략: Cassandra 컨테이너 설정부)
# ...위 설정을 적용하면, K8s 스케줄러는 app: cassandra 라벨이 달린 파드가 이미 떠있는 노드가 있다면, 두 번째 카산드라 파드는 무조건 다른 hostname을 가진 워커 노드 중 하나를 찾아서 배정합니다. 만약 가용 노드가 없다면 해당 파드는 Pending 상태로 대기하며 절대 한 바구니에 올라타지 않게 됩니다.
4. 고가용성 확보 완료
100% 무중단을 지향하는 플랫폼 엔지니어링 파트에서 Anti-Affinity 속성 이해는 그 무엇보다 중요했습니다. “Required(Hard)” 모드로 쓰면 가용 노드가 없을 때 배포 행 자체가 중단되지만, 운영환경에서는 때때로 유연성을 위해 “Preferred(Soft)” 룰을 함께 섞어 써서 ‘가능하면 떨어뜨리고, 자원이 없으면 어쩔 수 없이 겹쳐서라도 띄워라’라는 하이브리드 고가용성 패턴으로 아키텍처 룰을 다듬게 되었습니다.