왜 파드 분산이 문제가 되는가

레플리카를 3개로 늘렸다고 가용성이 확보되는 것은 아니다. 스케줄러는 기본적으로 리소스 여유가 있는 노드를 우선하기 때문에, 아무 제약이 없으면 3개 파드가 같은 노드나 같은 가용영역(AZ)에 몰릴 수 있다. 이 상태에서 노드 한 대가 죽거나 AZ 하나가 장애를 일으키면 서비스 전체가 내려간다. 오토스케일링으로 파드 수만 늘려도 배치가 편향되면 SPOF는 그대로 남는다.

과거에는 podAntiAffinity로 이를 막았지만, 안티어피니티는 "같이 두지 마라"는 이진 규칙이라 균등 분포를 표현하기 어렵고 노드 수가 많아지면 스케줄링 비용도 커진다. 그래서 등장한 것이 topologySpreadConstraints다.

Topology Spread의 핵심 개념

Topology Spread는 특정 토폴로지 도메인(노드, AZ, 리전 등) 사이에서 파드 수의 편차를 제한한다. 핵심 필드는 세 가지다.

  • topologyKey: 분산 기준이 되는 노드 레이블. 예) kubernetes.io/hostname, topology.kubernetes.io/zone
  • maxSkew: 도메인 간 허용 파드 수 차이. 작을수록 균등해진다.
  • whenUnsatisfiable: 제약을 못 지킬 때 동작. DoNotSchedule(강제) 또는 ScheduleAnyway(선호).

기본 설정 예시

Deployment에 AZ 단위와 노드 단위 제약을 함께 건다. AZ는 강제로 균등 분산하고, 노드 단위는 여유가 없을 때를 대비해 선호로 둔다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  replicas: 6
  selector:
    matchLabels: { app: api }
  template:
    metadata:
      labels: { app: api }
    spec:
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: topology.kubernetes.io/zone
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels: { app: api }
        - maxSkew: 1
          topologyKey: kubernetes.io/hostname
          whenUnsatisfiable: ScheduleAnyway
          labelSelector:
            matchLabels: { app: api }
      containers:
        - name: api
          image: registry.example.com/api:1.4.2

레플리카 6개, AZ 3개라면 각 AZ에 2개씩 배치되고 편차는 1을 넘지 않는다.

maxSkew와 whenUnsatisfiable 선택

둘의 조합이 실제 운영 성격을 결정한다.

whenUnsatisfiable동작적합한 경우
DoNotSchedule제약 위반 시 파드 PendingAZ 균등이 가용성에 필수일 때
ScheduleAnyway최대한 지키되 안 되면 그냥 배치배치가 늦어도 파드 기동이 더 중요할 때

DoNotSchedule은 강력하지만, 특정 AZ에 노드가 부족하면 파드가 영원히 Pending에 걸릴 수 있다. 이 위험을 줄이려면 minDomains(v1.25+ 안정)로 최소 도메인 수를 명시하거나, ScheduleAnyway와 병행한다.

기본 클러스터 레벨 제약

모든 워크로드에 매번 제약을 붙이기 번거롭다면 kube-scheduler 설정으로 클러스터 기본값을 줄 수 있다. 개별 파드에 제약이 없을 때만 적용된다.

apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
  - pluginConfig:
      - name: PodTopologySpread
        args:
          defaultConstraints:
            - maxSkew: 3
              topologyKey: topology.kubernetes.io/zone
              whenUnsatisfiable: ScheduleAnyway
          defaultingType: List

전역 기본은 완만하게(ScheduleAnyway, maxSkew 여유 있게) 두고, 가용성이 중요한 워크로드만 개별적으로 조인다.

실무에서 자주 놓치는 주의점

  • 스케줄링 시점만 본다. Topology Spread는 배치 순간에만 평가한다. 이미 배치된 뒤 노드가 늘거나 줄어도 자동 재분산은 하지 않는다. 편향이 굳어지면 descheduler의 RemovePodsViolatingTopologySpreadConstraints 전략으로 주기적으로 재조정해야 한다.
  • labelSelector 누락. 셀렉터가 실제 파드 레이블과 어긋나면 제약이 아무 파드도 세지 않아 조용히 무력화된다.
  • 롤링 업데이트 중 스큐. 배포 중에는 신·구 파드가 공존해 일시적으로 skew가 커진다. matchLabelKeys(v1.27+)로 pod-template-hash를 넣으면 리비전별로 분리해 계산한다.
  • 노드 레이블 확인. 클라우드 관리형이면 zone 레이블이 자동으로 붙지만, 온프레미스는 직접 레이블을 달아야 topologyKey가 동작한다.

확인 명령 예시는 다음과 같다.

# AZ 레이블이 노드에 있는지 확인
kubectl get nodes -L topology.kubernetes.io/zone

# 파드가 노드/AZ에 어떻게 분포했는지 확인
kubectl get pods -l app=api -o wide \
  --sort-by='.spec.nodeName'

정리하면, Topology Spread는 레플리카 수가 아니라 "어디에 놓이는가"를 제어해 실제 가용성을 만든다. AZ 단위 강제 분산과 노드 단위 선호 분산을 함께 걸고, 배포·스케일 변동에 따른 편향은 descheduler로 보완하는 조합이 실무에서 안정적이다.