Kubernetes Pod Topology Spread

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에서 minDomainsmatchLabelKeys가 안정화되면서, PTSC만으로 대부분의 분산 요구사항을 커버할 수 있게 되었다. 프로덕션 Deployment에는 AZ 분산(하드) + 노드 분산(소프트)의 이중 제약을 기본으로 적용하고, PDB와 Readiness Probe를 함께 설정하여 노드 드레인·롤링 업데이트 중에도 고가용성을 유지하자.

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux