앞 글에서는 같은 서비스의 복제본을 서로 다른 Node에 두기 위해 pod anti-affinity를 사용했다. 하지만 운영 환경에서는 “같은 Node만 피하자”보다 조금 더 구체적인 요구가 자주 나온다.
- API 복제본 6개를 3개 가용 영역(zone)에 2개씩 두고 싶다.
- Node가 늘어나도 한 Node에만 복제본이 몰리지 않았으면 한다.
- 장애 대응을 위해 분산은 지키되, 일시적인 증설이
Pending으로 멈추지는 않았으면 한다.
이럴 때는 topologySpreadConstraints가 잘 맞는다. 이 기능은 특정 label을 가진 Pod가 topology 단위로 얼마나 고르게 퍼져 있는지를 보고, 다음 Pod의 위치를 정한다.
anti-affinity와 무엇이 다를까?
pod anti-affinity는 “이 Pod가 있는 곳은 피하자”에 가깝다. 반면 topology spread constraints는 각 영역의 복제본 수 차이를 직접 관리한다.
| 원하는 결과 | 더 잘 맞는 도구 |
|---|---|
| 같은 Node에 같은 서비스 Pod를 하나도 두지 않기 | hard pod anti-affinity |
| zone별 복제본 수 차이를 1개 이하로 유지하기 | topology spread constraints |
| Node와 zone 모두에 걸쳐 최대한 고르게 배치하기 | topology spread constraints 여러 개 |
topologyKey가 분산을 계산할 경계다. kubernetes.io/hostname을 쓰면 Node 단위, topology.kubernetes.io/zone을 쓰면 zone 단위로 Pod 수를 센다. Node에 해당 label이 일관되게 붙어 있어야 한다.
먼저 알아야 할 설정 세 가지
Kubernetes 공식 문서의 Pod Topology Spread Constraints를 보면, 보통 아래 세 필드부터 이해하면 된다.
| 필드 | 하는 일 |
|---|---|
topologyKey | Pod 수를 나눠서 볼 경계. Node 또는 zone label을 넣는다. |
maxSkew | 가장 많은 영역과 가장 적은 영역의 Pod 수 차이로 허용할 최대값이다. 0보다 커야 한다. |
whenUnsatisfiable | 분산 규칙을 못 맞출 때 Pending으로 둘지, 덜 고른 곳을 선호해 계속 배치할지 정한다. |
whenUnsatisfiable: DoNotSchedule이면 maxSkew를 넘기는 Node는 후보에서 빠진다. 규칙을 끝까지 지키는 대신, 조건을 만족할 Node가 없으면 Pod는 Pending으로 남는다. ScheduleAnyway는 배치를 막지 않는다. scheduler가 불균형을 줄일 수 있는 Node에 점수를 더 주지만, 다른 스케줄링 조건 때문에 완벽히 고르지 않을 수 있다.
결제 API를 가용 영역과 Node에 나누기
세 개 zone에 Node가 있는 클러스터에서 payments-api 복제본 6개를 운영한다고 해 보자. Node 하나가 내려가도 남은 복제본이 살아 있어야 하고, zone 하나에 복제본이 몰리는 일도 피하고 싶다.
아래 설정은 두 제약을 동시에 둔다. 첫 번째는 zone 간, 두 번째는 Node 간 분산을 본다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: payments-api
spec:
replicas: 6
selector:
matchLabels:
app.kubernetes.io/name: payments-api
template:
metadata:
labels:
app.kubernetes.io/name: payments-api
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app.kubernetes.io/name: payments-api
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app.kubernetes.io/name: payments-api
containers:
- name: api
image: example.registry/payments-api:1.0
이 예시에서 zone 분산은 반드시 지킨다. 복제본이 2 / 2 / 2일 때 다음 Pod는 어느 zone에 놓아도 maxSkew: 1을 넘길 수 있으므로, 다른 조건도 함께 본다. 반면 Node 분산은 가능한 한 지키는 선호 조건이다. 특정 zone의 Node가 부족한 상황에서도 새 버전 배포가 영원히 멈추는 일은 줄일 수 있다.
여러 constraint를 적으면 모두 만족해야 한다. DoNotSchedule을 두 개 이상 쓴다면, 어떤 제약이 후보를 없애는지 따로 생각해야 한다. Kubernetes 공식 예제도 여러 제약의 교집합만 유효한 배치라고 설명한다.
maxSkew: 1은 무엇을 뜻할까?
예를 들어 zone별 Pod 수가 아래와 같다고 하자.
zone-a: 2
zone-b: 2
zone-c: 1
maxSkew: 1이면 다음 Pod는 zone-c에 배치하는 편이 가장 자연스럽다. zone-a에 하나를 더 넣으면 3 / 2 / 1이 되어 가장 많은 zone과 가장 적은 zone의 차이가 2가 된다.
이때 DoNotSchedule이면 zone-a는 후보에서 제외될 수 있다. ScheduleAnyway이면 zone-c가 더 높은 점수를 받지만, 예를 들어 zone-c에 request를 수용할 Node가 없다면 다른 zone에 배치될 수 있다. 즉, 후자는 분산 보장이 아니라 분산 선호다.
가용 영역 하나를 사용할 수 없다면
zone-c의 Node를 늘리는 동안 zone-a와 zone-b만 사용할 수 있다고 해 보자. 기존에 minDomains: 3, maxSkew: 1, DoNotSchedule을 설정했다면 새 Pod가 멈출 수 있다. 이는 오류가 아니라 “사용 가능한 영역 세 곳에 나눠 배치하겠다”는 설정의 결과다.
이런 정책은 결제처럼 가용 영역 분리가 꼭 필요한 서비스에는 맞을 수 있다. 반대로 내부 어드민 API처럼 일시적인 불균형보다 배포 진행이 더 중요하다면 zone constraint를 ScheduleAnyway로 두는 선택도 있다.
| 상황 | 추천 |
|---|---|
| zone 장애를 견디는 것이 배포 속도보다 중요 | zone + DoNotSchedule, maxSkew: 1 |
| 일시적 Node 부족에도 rollout을 진행해야 함 | ScheduleAnyway 또는 조금 더 큰 maxSkew |
| Node 한 대 장애만 우선 막고 싶음 | kubernetes.io/hostname 기준 분산 |
minDomains는 DoNotSchedule과 함께 쓸 수 있다. 사용할 수 있는 영역 수가 이 값보다 적으면 Kubernetes는 비어 있는 영역이 있다고 보고 Pod 수 차이를 계산한다. 이 값을 추가하기 전에는 반드시 유지해야 할 가용 영역 수와 Node label 상태를 확인해야 한다.
anti-affinity와 함께 고르기
topology spread constraints는 대개 anti-affinity보다 세밀하게 복제본 수를 제어한다. 그렇다고 항상 완전한 대체 관계는 아니다.
- Node마다 정확히 하나만 두어야 한다면 hard anti-affinity가 의도를 더 직접적으로 표현한다.
- 여러 zone에 6개를
2 / 2 / 2처럼 나누고 싶다면 topology spread constraints가 더 잘 읽힌다. requiredanti-affinity, taint, resource request, topology spread을 한 Pod에 모두 쌓으면 교집합이 너무 작아질 수 있다.
마지막 경우에는 kubectl describe pod <pod-name>의 이벤트를 먼저 보자. didn't match pod topology spread constraints와 Insufficient cpu는 완전히 다른 문제인데, 둘 다 결과는 Pending으로 보인다.
핵심 정리
topologySpreadConstraints는 “서로 떨어뜨리자”보다 한 단계 더 구체적인 배치 정책이다. 먼저 장애 경계가 Node인지 zone인지 정하고, 그다음에 불균형을 어디까지 허용할지 정하면 된다.
topologyKey는 분산의 단위다.maxSkew는 영역 간 Pod 수 차이의 상한이다.DoNotSchedule은 분산을 보장하고,ScheduleAnyway는 분산을 선호한다.
다음 글에서는 scheduler가 Node의 여유를 판단할 때 실제 사용량이 아니라 무엇을 보는지, resources.requests와 limits를 중심으로 정리한다.