8분 읽기

쿠버네티스에서 Pod를 고르게 배치하기

앞 글에서는 같은 서비스의 복제본을 서로 다른 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이 일관되게 붙어 있어야 한다.

topology spread constraints에서 topology, maxSkew, whenUnsatisfiable가 함께 복제본 분산을 결정하는 모습
분산의 경계, 허용할 수 있는 불균형, 제약을 지키는 강도를 따로 정한다.

먼저 알아야 할 설정 세 가지

Kubernetes 공식 문서의 Pod Topology Spread Constraints를 보면, 보통 아래 세 필드부터 이해하면 된다.

필드하는 일
topologyKeyPod 수를 나눠서 볼 경계. 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 기준 분산

minDomainsDoNotSchedule과 함께 쓸 수 있다. 사용할 수 있는 영역 수가 이 값보다 적으면 Kubernetes는 비어 있는 영역이 있다고 보고 Pod 수 차이를 계산한다. 이 값을 추가하기 전에는 반드시 유지해야 할 가용 영역 수와 Node label 상태를 확인해야 한다.

anti-affinity와 함께 고르기

topology spread constraints는 대개 anti-affinity보다 세밀하게 복제본 수를 제어한다. 그렇다고 항상 완전한 대체 관계는 아니다.

  • Node마다 정확히 하나만 두어야 한다면 hard anti-affinity가 의도를 더 직접적으로 표현한다.
  • 여러 zone에 6개를 2 / 2 / 2처럼 나누고 싶다면 topology spread constraints가 더 잘 읽힌다.
  • required anti-affinity, taint, resource request, topology spread을 한 Pod에 모두 쌓으면 교집합이 너무 작아질 수 있다.

마지막 경우에는 kubectl describe pod <pod-name>의 이벤트를 먼저 보자. didn't match pod topology spread constraintsInsufficient cpu는 완전히 다른 문제인데, 둘 다 결과는 Pending으로 보인다.

핵심 정리

topologySpreadConstraints는 “서로 떨어뜨리자”보다 한 단계 더 구체적인 배치 정책이다. 먼저 장애 경계가 Node인지 zone인지 정하고, 그다음에 불균형을 어디까지 허용할지 정하면 된다.

  • topologyKey는 분산의 단위다.
  • maxSkew는 영역 간 Pod 수 차이의 상한이다.
  • DoNotSchedule은 분산을 보장하고, ScheduleAnyway는 분산을 선호한다.

다음 글에서는 scheduler가 Node의 여유를 판단할 때 실제 사용량이 아니라 무엇을 보는지, resources.requestslimits를 중심으로 정리한다.

참고 자료