K8s Affinity 스케줄링 심화

Affinity란?

Kubernetes 스케줄러는 기본적으로 리소스 여유가 있는 노드에 Pod을 배치한다. 하지만 실무에서는 “GPU 노드에만 ML Pod 배치”, “같은 서비스의 Pod을 다른 노드에 분산”, “캐시 서버와 같은 존에 배치” 같은 세밀한 제어가 필요하다. Affinity는 이런 스케줄링 선호도를 선언적으로 정의하는 메커니즘이다.

Affinity는 크게 Node Affinity(어떤 노드에 배치할 것인가)와 Pod Affinity/Anti-Affinity(다른 Pod과의 관계로 배치를 결정)로 나뉜다.

Node Affinity

nodeSelector의 상위 호환이다. 더 풍부한 연산자와 선호/필수 구분을 지원한다.

Required (필수 조건)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: ml-training
spec:
  template:
    spec:
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              # GPU 노드에만 배치
              - key: gpu-type
                operator: In
                values: ["nvidia-a100", "nvidia-v100"]
              # 특정 존 제외
              - key: topology.kubernetes.io/zone
                operator: NotIn
                values: ["ap-northeast-2c"]
      containers:
      - name: trainer
        image: ml-trainer:latest
        resources:
          limits:
            nvidia.com/gpu: 1

requiredDuringSchedulingIgnoredDuringExecution은 조건을 만족하는 노드가 없으면 Pod이 Pending 상태로 남는다. nodeSelectorTerms 내 여러 항목은 OR 관계, matchExpressions 내 항목은 AND 관계다.

Preferred (선호 조건)

affinity:
  nodeAffinity:
    preferredDuringSchedulingIgnoredDuringExecution:
    - weight: 80  # 1~100, 높을수록 강한 선호
      preference:
        matchExpressions:
        - key: node-type
          operator: In
          values: ["high-memory"]
    - weight: 20
      preference:
        matchExpressions:
        - key: topology.kubernetes.io/zone
          operator: In
          values: ["ap-northeast-2a"]

Preferred는 조건을 만족하지 못해도 다른 노드에 스케줄링된다. weight로 여러 선호 조건의 우선순위를 조절한다. 스케줄러는 각 노드에 대해 weight 합산 점수를 계산해 가장 높은 노드를 선택한다.

연산자 종류

연산자 의미 예시
In 값 목록 중 하나와 일치 gpu-type In [a100, v100]
NotIn 값 목록에 없음 zone NotIn [2c]
Exists 키가 존재 (값 무관) gpu Exists
DoesNotExist 키가 없음 spot DoesNotExist
Gt / Lt 숫자 비교 cpu-cores Gt 16

Pod Affinity: 함께 배치

특정 Pod이 실행 중인 노드/존에 함께 배치하고 싶을 때 사용한다. 캐시 서버와 API 서버를 같은 존에 두면 네트워크 지연을 줄일 수 있다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-server
spec:
  template:
    spec:
      affinity:
        podAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values: ["redis-cache"]
            # 같은 존(zone)에 배치
            topologyKey: topology.kubernetes.io/zone

          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 50
            podAffinityTerm:
              labelSelector:
                matchExpressions:
                - key: app
                  operator: In
                  values: ["redis-cache"]
              # 가능하면 같은 노드까지
              topologyKey: kubernetes.io/hostname

topologyKey가 핵심이다. 이 키의 라벨 값이 같은 노드를 “같은 토폴로지 도메인”으로 본다.

topologyKey 범위
kubernetes.io/hostname 같은 노드
topology.kubernetes.io/zone 같은 가용 영역
topology.kubernetes.io/region 같은 리전

Pod Anti-Affinity: 분산 배치

고가용성의 핵심이다. 같은 서비스의 Pod을 다른 노드/존에 분산해 단일 장애점을 제거한다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-server
spec:
  replicas: 3
  template:
    metadata:
      labels:
        app: api-server
    spec:
      affinity:
        podAntiAffinity:
          # 필수: 같은 노드에 2개 이상 배치 금지
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values: ["api-server"]
            topologyKey: kubernetes.io/hostname

          # 선호: 가능하면 다른 존에 분산
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 100
            podAffinityTerm:
              labelSelector:
                matchExpressions:
                - key: app
                  operator: In
                  values: ["api-server"]
              topologyKey: topology.kubernetes.io/zone

주의: required Anti-Affinity는 노드 수보다 replicas가 많으면 Pod이 Pending된다. replicas가 3인데 노드가 2대뿐이면 1개는 스케줄링 불가다. 이런 경우 preferred를 사용하자.

실전 조합 패턴

실무에서는 Node Affinity + Pod Anti-Affinity를 함께 사용하는 것이 일반적이다.

# 패턴: 웹 서버를 웹 전용 노드에 배치하되, 노드별로 분산
affinity:
  nodeAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
      nodeSelectorTerms:
      - matchExpressions:
        - key: node-role
          operator: In
          values: ["web"]
  podAntiAffinity:
    preferredDuringSchedulingIgnoredDuringExecution:
    - weight: 100
      podAffinityTerm:
        labelSelector:
          matchExpressions:
          - key: app
            operator: In
            values: ["web-server"]
        topologyKey: kubernetes.io/hostname

Affinity vs Topology Spread

Pod Anti-Affinity는 “같은 곳에 두지 마”라는 규칙이고, Topology Spread Constraints는 “균등하게 분산하라”는 규칙이다.

기능 Anti-Affinity Topology Spread
목적 특정 Pod과 같은 곳에 배치 금지 도메인 간 균등 분산
skew 제어 불가 (0 or 1만) maxSkew로 세밀 제어
추천 절대 금지 조건 비율 기반 분산

디버깅

# Pod이 Pending인 이유 확인
kubectl describe pod <pod-name> | grep -A 10 Events

# 노드 라벨 확인
kubectl get nodes --show-labels

# 스케줄러가 고려하는 노드 점수 확인
kubectl get events --field-selector reason=FailedScheduling

# 특정 노드에 어떤 Pod이 있는지 확인
kubectl get pods -o wide --field-selector spec.nodeName=node-1

가장 흔한 실수는 topologyKey 오타, 라벨 불일치, required + 리소스 부족 조합이다. describe pod의 Events에서 원인을 정확히 알려준다.

정리

Affinity는 K8s 스케줄링의 핵심 도구다. Node Affinity로 워크로드를 적합한 하드웨어에 배치하고, Pod Anti-Affinity로 고가용성을 확보하며, Pod Affinity로 지연 시간에 민감한 서비스를 가까이 배치한다. required는 절대 조건, preferred는 최선 노력으로 구분하고, 노드 수와 replicas 관계를 항상 고려하자.

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