Pod Topology Spread Constraints란? — Pod 분산 배치의 정밀 제어
Kubernetes에서 Deployment의 replicas를 3으로 설정해도, 세 Pod이 모두 같은 노드에 몰릴 수 있다. 해당 노드가 장애를 겪으면 서비스 전체가 중단된다. Pod Topology Spread Constraints(PTSC)는 Pod을 노드·존(AZ)·리전 같은 토폴로지 도메인에 걸쳐 균등하게 분산시키는 선언적 메커니즘이다.
podAntiAffinity로도 분산이 가능하지만, “각 존에 정확히 동일한 수”를 보장하기 어렵고, 노드 수보다 replica가 많으면 스케줄링이 실패한다. PTSC는 maxSkew로 허용 가능한 불균형 정도를 숫자로 지정하여 이 문제를 해결한다. Kubernetes Resource Management 심화에서 다룬 QoS 설계와 결합하면, 리소스 효율과 고가용성을 동시에 달성할 수 있다.
1. 핵심 필드 4가지 — maxSkew·topologyKey·whenUnsatisfiable·labelSelector
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
spec:
replicas: 6
template:
spec:
topologySpreadConstraints:
- maxSkew: 1 # ① 최대 허용 불균형
topologyKey: topology.kubernetes.io/zone # ② 분산 기준 도메인
whenUnsatisfiable: DoNotSchedule # ③ 위반 시 동작
labelSelector: # ④ 대상 Pod 셀렉터
matchLabels:
app: order-service
| 필드 | 설명 | 기본값 |
|---|---|---|
maxSkew |
도메인 간 Pod 수 차이의 최대 허용치. 1이면 완전 균등, 2면 한 도메인이 최대 2개 더 많을 수 있음 | 필수 |
topologyKey |
노드 레이블 키. 같은 값을 가진 노드가 하나의 도메인을 구성 | 필수 |
whenUnsatisfiable |
DoNotSchedule(하드) 또는 ScheduleAnyway(소프트) |
DoNotSchedule |
labelSelector |
skew 계산 대상 Pod을 선택하는 셀렉터 | 필수 |
maxSkew 계산 원리
스케줄러는 새 Pod을 배치할 때 기존 Pod의 분포를 확인한다. 3개 AZ에 Pod이 [3, 2, 1]로 분포되어 있다면 현재 skew는 3 – 1 = 2다. maxSkew: 1이면 이 상태는 위반이므로, 새 Pod은 반드시 Pod 수가 가장 적은 도메인(여기선 세 번째 AZ)에 배치된다.
# 예시: replicas=6, 3개 AZ, maxSkew=1
# 이상적 분배: [2, 2, 2] — skew = 0 ✅
# 허용 가능: [3, 2, 2] — skew = 1 ✅
# 위반: [3, 2, 1] — skew = 2 ❌ (maxSkew=1 초과)
# 위반: [4, 1, 1] — skew = 3 ❌
2. topologyKey 선택 전략 — 존·노드·커스텀 도메인
2-1. AZ(가용 영역) 분산 — 가장 흔한 패턴
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: order-service
AWS의 us-east-1a, 1b, 1c 같은 AZ 단위로 Pod을 분산한다. 하나의 AZ가 통째로 장애가 나도 나머지 AZ에서 서비스가 유지된다.
2-2. 노드 분산 — 단일 AZ 환경
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: order-service
온프레미스나 단일 AZ 환경에서는 노드 단위 분산이 최선이다. 다만 replica 수가 노드 수를 초과하면 DoNotSchedule 시 Pending 상태에 빠진다.
2-3. 커스텀 도메인 — 랙·팀·GPU 타입
# 노드에 커스텀 레이블 부여
kubectl label node worker-01 rack=rack-a
kubectl label node worker-02 rack=rack-a
kubectl label node worker-03 rack=rack-b
# 랙 단위 분산
topologySpreadConstraints:
- maxSkew: 1
topologyKey: rack
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: ml-inference
3. whenUnsatisfiable — DoNotSchedule vs ScheduleAnyway
| 모드 | 동작 | 사용 시점 |
|---|---|---|
DoNotSchedule |
제약 위반 시 Pod을 Pending 상태로 둠 (하드 제약) | 금융·결제 등 분산이 필수인 워크로드 |
ScheduleAnyway |
최대한 분산하되, 불가능하면 어디든 배치 (소프트 제약) | 가용성 > 분산인 일반 서비스, 노드 부족 환경 |
운영 팁: AZ 분산은 DoNotSchedule, 노드 분산은 ScheduleAnyway를 조합하는 것이 실무에서 가장 안전하다. AZ 분산은 강제하되, 노드 분산은 “최선을 다하는” 수준으로 두면 스케줄링 실패를 방지할 수 있다.
4. 다중 제약 조합 — AZ + 노드 동시 분산
실무에서 가장 많이 쓰는 패턴이다. 두 개의 topologySpreadConstraints를 동시에 지정하면, 스케줄러가 모든 제약을 동시에 만족하는 노드를 찾는다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: payment-service
spec:
replicas: 6
template:
metadata:
labels:
app: payment-service
spec:
topologySpreadConstraints:
# 제약 1: AZ 간 균등 (하드)
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: payment-service
# 제약 2: 노드 간 균등 (소프트)
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: payment-service
containers:
- name: payment
image: payment-service:v2.1
resources:
requests:
cpu: 500m
memory: 512Mi
이 설정의 결과: 3개 AZ × 2개 노드씩 총 6개 노드가 있다면, 각 AZ에 2개씩(skew=0), 각 노드에 1개씩(skew=0) 이상적으로 분배된다.
5. minDomains (v1.30+ stable) — 빈 도메인 처리
Kubernetes 1.30에서 stable로 승격된 minDomains 필드는 skew 계산 시 고려할 최소 도메인 수를 지정한다. 클러스터에 AZ가 3개이지만 현재 2개만 노드가 있을 때, minDomains: 3으로 설정하면 빈 도메인(Pod 0개)도 skew 계산에 포함된다.
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
minDomains: 3 # 최소 3개 AZ에 분산 보장
labelSelector:
matchLabels:
app: critical-service
시나리오: 2개 AZ에 노드가 있고 replicas=4일 때:
minDomains미설정: [2, 2] — skew=0 ✅ (2개 도메인만 고려)minDomains: 3: [2, 2, 0] — skew=2 ❌ (3번째 도메인의 0도 포함)
이를 통해 “최소 N개 AZ에 걸쳐 분산”을 강제할 수 있다.
6. matchLabelKeys (v1.29+ beta) — Rolling Update 시 정확한 skew 계산
Rolling Update 중 새/구 버전 Pod이 동시에 존재하면, labelSelector가 두 버전 모두를 잡아 skew가 왜곡된다. matchLabelKeys는 새로 배치할 Pod의 레이블 값을 동적으로 셀렉터에 추가하여 같은 리비전(revision)끼리만 skew를 계산한다.
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: order-service
matchLabelKeys:
- pod-template-hash # Deployment가 자동 부여하는 리비전 해시
동작 원리: 새 Pod의 pod-template-hash: abc123 값이 셀렉터에 pod-template-hash=abc123으로 추가된다. 구 버전(hash: xyz789)의 Pod은 제외되므로, 새 버전 Pod만으로 정확한 분산이 이루어진다.
7. nodeAffinityPolicy·nodeTaintsPolicy (v1.26+) — 필터링 동작 제어
PTSC의 skew를 계산할 때, nodeAffinity나 Taint로 인해 Pod이 배치될 수 없는 노드를 어떻게 취급할지 제어하는 필드다.
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
nodeAffinityPolicy: Honor # nodeAffinity/nodeSelector를 존중
nodeTaintsPolicy: Honor # 노드 Taint를 존중
labelSelector:
matchLabels:
app: order-service
| 필드 | Honor (기본) | Ignore |
|---|---|---|
nodeAffinityPolicy |
nodeAffinity/nodeSelector에 매칭되는 노드만 skew 계산에 포함 | 모든 노드 포함 |
nodeTaintsPolicy |
Pod이 toleration을 가진 Tainted 노드만 포함 | Taint 무시, 모든 노드 포함 |
Kubernetes RBAC 심화와 마찬가지로, 전용 노드 풀(Taints & Tolerations으로 격리된)에서 PTSC를 쓸 때 Honor로 설정해야 의도대로 동작한다.
8. 클러스터 기본 제약 — DefaultConstraints 설정
모든 Deployment에 PTSC를 일일이 넣기 번거롭다면, kube-scheduler의 defaultConstraints로 클러스터 전역 기본값을 설정할 수 있다.
# kube-scheduler 설정 (KubeSchedulerConfiguration)
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
- schedulerName: default-scheduler
pluginConfig:
- name: PodTopologySpread
args:
defaultConstraints:
- maxSkew: 3
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
- maxSkew: 5
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
defaultingType: List # List 또는 System
주의: Pod에 topologySpreadConstraints를 직접 지정하면 기본 제약은 완전히 무시된다(오버라이드, 병합 아님). 기본 제약과 커스텀 제약을 합치려면 Pod spec에 둘 다 명시해야 한다.
9. podAntiAffinity와의 비교 — 언제 PTSC를 쓸까?
| 기준 | podAntiAffinity | Topology Spread Constraints |
|---|---|---|
| 목표 | “같은 도메인에 놓지 마” | “도메인 간 균등하게 분산해” |
| 불균형 허용 | 불가 (바이너리: 허용/거부) | maxSkew로 정밀 제어 |
| replica > 도메인 수 | hard 모드 시 스케줄링 실패 | maxSkew로 초과분 허용 |
| 다중 도메인 | 복잡한 조합 필요 | 배열로 간결하게 선언 |
| Rolling Update | 구/신 버전 구분 어려움 | matchLabelKeys로 버전별 분산 |
결론: “도메인당 최대 1개”처럼 엄격한 배타 배치는 podAntiAffinity, “전체적으로 균등하게” 분산은 PTSC가 적합하다. 대부분의 운영 환경에서는 PTSC가 더 유연하고 실패에 강하다.
10. 운영 트러블슈팅 — Pending Pod 디버깅
10-1. 흔한 Pending 원인
# Pod이 Pending 상태일 때
kubectl describe pod order-service-abc123
# Events 섹션에서 확인
# "0/6 nodes are available: 2 node(s) didn't match pod topology spread constraints,
# 4 node(s) didn't match pod anti-affinity rules"
원인별 대응:
- “didn’t match pod topology spread constraints”: maxSkew를 늘리거나,
whenUnsatisfiable: ScheduleAnyway로 전환 - 특정 AZ에 노드 부족: 해당 AZ에 노드 추가 또는 Cluster Autoscaler 설정 확인
- 리소스 부족과 PTSC 동시 위반: PTSC가 지정한 도메인에는 리소스가 없고, 여유 있는 도메인은 PTSC 위반 → maxSkew를 완화하거나 리소스를 추가
10-2. skew 현황 점검 스크립트
#!/bin/bash
# AZ별 Pod 분포 확인
APP_LABEL="app=order-service"
echo "=== Pod Distribution by Zone ==="
kubectl get pods -l $APP_LABEL -o json |
jq -r '.items[] | .spec.nodeName' |
while read node; do
zone=$(kubectl get node "$node" -o jsonpath='{.metadata.labels.topology.kubernetes.io/zone}')
echo "$zone"
done | sort | uniq -c | sort -rn
echo ""
echo "=== Pod Distribution by Node ==="
kubectl get pods -l $APP_LABEL -o wide --no-headers |
awk '{print $7}' | sort | uniq -c | sort -rn
11. 실전 프로덕션 템플릿 — 모범 사례 종합
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
namespace: production
spec:
replicas: 6
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: order-service
template:
metadata:
labels:
app: order-service
spec:
# ── Topology Spread ──
topologySpreadConstraints:
# AZ 분산: 하드 제약
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
matchLabelKeys:
- pod-template-hash # Rolling Update 시 새 리비전만 계산
labelSelector:
matchLabels:
app: order-service
# 노드 분산: 소프트 제약
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
matchLabelKeys:
- pod-template-hash
labelSelector:
matchLabels:
app: order-service
# ── Affinity (보완) ──
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: node-role
operator: In
values: ["app"]
# ── Container ──
containers:
- name: order
image: order-service:v3.2.1
ports:
- containerPort: 8080
name: http
- containerPort: 8081
name: management
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
memory: 1Gi
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: management
periodSeconds: 5
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: management
initialDelaySeconds: 30
periodSeconds: 10
# ── PDB와 함께 사용 ──
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: order-service-pdb
namespace: production
spec:
minAvailable: 4 # 최소 4개 유지
selector:
matchLabels:
app: order-service
Kubernetes PDB·Eviction·Readiness 심화에서 다룬 PodDisruptionBudget과 PTSC를 함께 쓰면, 노드 드레인 시에도 AZ 분산을 유지하면서 최소 가용 Pod 수를 보장할 수 있다.
12. 운영 체크리스트
| 항목 | 권장 설정 | 위반 시 증상 |
|---|---|---|
| AZ 분산 | DoNotSchedule + maxSkew: 1 | AZ 장애 시 서비스 전체 중단 |
| 노드 분산 | ScheduleAnyway + maxSkew: 1 | 노드 장애 시 과도한 Pod 손실 |
| Rolling Update | matchLabelKeys: [pod-template-hash] | 업데이트 중 분산 왜곡 |
| PDB 병행 | minAvailable ≥ replicas – maxUnavailable | 드레인 중 가용성 손실 |
| labelSelector | Deployment의 selector와 일치 | skew 계산 오류 |
| Pending 모니터링 | kube_pod_status_phase{phase=”Pending”} 알림 | 배포 실패를 뒤늦게 발견 |
마무리 — 분산은 선언하는 것이다
Pod Topology Spread Constraints는 “Pod이 어디에 배치되는가”를 운에 맡기지 않고 코드로 선언하는 메커니즘이다. maxSkew로 허용 가능한 불균형을 정의하고, whenUnsatisfiable로 위반 시 동작을 결정하며, matchLabelKeys로 Rolling Update 시 정확한 분산을 보장한다.
특히 Kubernetes 1.29~1.30에서 minDomains와 matchLabelKeys가 안정화되면서, PTSC만으로 대부분의 분산 요구사항을 커버할 수 있게 되었다. 프로덕션 Deployment에는 AZ 분산(하드) + 노드 분산(소프트)의 이중 제약을 기본으로 적용하고, PDB와 Readiness Probe를 함께 설정하여 노드 드레인·롤링 업데이트 중에도 고가용성을 유지하자.