infra
Platform

모듈 맵

[Kubernetes] Node Affinity와 Taint/Toleration 기반 스케줄링 제어

0 / 29 완료

펼치기
0 / 29 완료0%

Kubernetes · 19 / 29

[Kubernetes] Node Affinity와 Taint/Toleration 기반 스케줄링 제어

Node Affinity와 Taint/Toleration으로 GPU 노드, 고메모리 노드 등 특수 노드에 적합한 파드만 배치되도록 설계합니다

🚨INCIDENT ALERT
HIGH

GPU가 필요한 파드가 일반 노드에 배치되어 계속 Pending 상태가 됩니다. 운영팀은 노드 라벨, affinity, taint/toleration을 조합해 워크로드를 맞는 장비에 보내야 합니다. 스케줄링 규칙을 이해하면 비용이 큰 노드와 민감한 워크로드를 안전하게 분리할 수 있습니다.

Node Affinity와 Taint/Toleration — 파드 스케줄링 제어

머신러닝 추론 서버가 일반 CPU 노드에 배치되어 GPU를 사용하지 못하고 있습니다. 반대로 비용이 비싼 GPU 노드에 일반 nginx 파드가 올라가 GPU를 낭비하고 있습니다. 배치 작업용 고메모리 노드에는 일반 API 서버가 올라가 메모리를 독점하고, 실제 배치 작업은 메모리가 부족한 노드에 배치되어 OOM이 발생합니다. Kubernetes 스케줄러는 기본적으로 가용 자원이 있는 노드에 파드를 고르게 분산시키지만, 클러스터가 커질수록 "이 파드는 반드시 이런 노드에 있어야 한다"는 요구가 생깁니다. Node Affinity는 파드 관점에서 "나는 이런 노드를 원한다"고 표현하고, Taint/Toleration은 노드 관점에서 "나는 특정 파드만 받겠다"고 선언하는 상호 보완적인 메커니즘입니다.

이번 챕터에서 배울 것
  • 1NodeSelector: 단순한 노드 선택
  • 2Node Affinity: required(필수)와 preferred(선호) 규칙
  • 3Taint: 노드에 파드 접근 제한 설정
  • 4Toleration: 파드에 Taint 허용 선언
  • 5GPU 전용 노드 설정 실무 패턴
  • 6스케줄링 실패(Pending) 원인 디버깅
실습 환경 준비
노드 목록과 레이블 확인
kubectl get nodes --show-labels
노드 Taint 확인
kubectl describe nodes | grep Taints
실습용 노드 레이블 추가
kubectl label node <노드이름> disk=ssd environment=production
스케줄러 설정 확인
kubectl get configmap kube-scheduler-config -n kube-system 2>/dev/null || echo 'default scheduler'

NodeSelector: 가장 단순한 노드 선택

Node Affinity 이전에 존재하던 단순한 방식입니다. 노드의 레이블을 키-값으로 정확하게 매칭합니다.

YAML
apiVersion: v1
kind: Pod
metadata:
  name: ssd-workload
spec:
  nodeSelector:
    disk: ssd              # disk=ssd 레이블이 있는 노드에만 배치
  containers:
    - name: app
      image: nginx
Kubernetes
# 노드에 레이블 추가
kubectl label node node-1 disk=ssd
kubectl label node node-2 disk=hdd

# nodeSelector가 있는 파드 배포 후 배치 노드 확인
kubectl get pod ssd-workload -o wide
# NAME          NODE     STATUS
# ssd-workload  node-1   Running   ← disk=ssd 레이블이 있는 노드에 배치됨
🔍실행 후 확인할 것
  • NAME조회 대상 리소스 이름이 예상한 대상과 일치하는지 확인합니다.
  • STATUS/READYRunning, Ready, Available처럼 정상 상태를 나타내는 필드가 있는지 봅니다.
  • RESTARTS/EVENTS재시작 횟수나 Warning 이벤트가 증가하지 않는지 확인합니다.
💡개념

Node Affinity: required(필수)와 preferred(선호)

ML 추론 서버가 GPU 노드 대신 일반 CPU 노드에 배치되어 성능이 100배 낮아졌습니다. NodeSelector로 accelerator=nvidia-gpu를 설정했지만 레이블 키 오타로 인해 조건이 무시됐고 스케줄러는 아무 노드나 선택했습니다. 또한 노드가 us-east-1a와 us-east-1b에 분산되어 있을 때 특정 zone을 선호하되 전체가 없으면 다른 zone도 수용하는 유연한 조건이 필요한데 NodeSelector는 이를 표현하지 못합니다. Node Affinity는 필수 조건(required)과 선호 조건(preferred)을 분리하고 In/NotIn/Exists 같은 연산자를 지원해 복잡한 배치 전략을 선언할 수 있습니다. 이 CB에서는 GPU 파드 배치 예시로 required와 preferred 조건을 조합하는 방법을 다룹니다. 필수 조건(required)과 선호 조건(preferred)을 분리하고, In/NotIn/Exists 같은 연산자를 사용할 수 있습니다.

Node Affinity: required(필수)와 preferred(선호)

YAML
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ml-inference
spec:
  replicas: 2
  selector:
    matchLabels:
      app: ml-inference
  template:
    metadata:
      labels:
        app: ml-inference
    spec:
      affinity:
        nodeAffinity:
          # 필수 조건: GPU 노드에만 배치 (충족 못하면 Pending)
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
              - matchExpressions:
                  - key: accelerator
                    operator: In
                    values:
                      - nvidia-gpu
                      - amd-gpu
                  - key: kubernetes.io/arch
                    operator: In
                    values:
                      - amd64
          # 선호 조건: us-east-1a를 선호하지만 없어도 됨 (weight로 우선순위 조정)
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 80
              preference:
                matchExpressions:
                  - key: topology.kubernetes.io/zone
                    operator: In
                    values:
                      - ap-northeast-2a
            - weight: 20
              preference:
                matchExpressions:
                  - key: node-type
                    operator: In
                    values:
                      - high-memory
      containers:
        - name: inference-server
          image: ml-team/inference:v2.3
          resources:
            limits:
              nvidia.com/gpu: 1
              memory: "16Gi"
            requests:
              nvidia.com/gpu: 1
              memory: "12Gi"
Kubernetes
# 노드에 GPU 레이블 추가 (EKS에서 GPU 노드 그룹 생성 시 자동 추가됨)
kubectl label node gpu-node-1 accelerator=nvidia-gpu

# 배포 및 배치 확인
kubectl apply -f ml-inference.yaml
kubectl get pods -l app=ml-inference -o wide
# NAME              NODE          STATUS
# ml-inference-aaa  gpu-node-1   Running
# ml-inference-bbb  gpu-node-2   Running
💡개념

Taint와 Toleration: 노드를 특정 파드만 받도록

GPU 노드에 Node Affinity만 설정하면 GPU 파드가 올바른 노드에 배치됩니다. 그런데 일반 nginx 파드도 GPU 노드를 선택할 수 있어 비용이 비싼 GPU 자원이 낭비됩니다. Affinity는 파드가 어떤 노드를 선호하는지를 표현하지만, 노드 입장에서 특정 파드 외에는 받지 않겠다고 선언하는 방법이 별도로 필요합니다. Taint는 노드에 "이 조건을 견딜 수 없는 파드는 들어오지 마라"는 표시를 하고, Toleration은 파드에 "나는 이 Taint를 견딜 수 있다"고 선언합니다. 이 CB에서는 GPU 노드를 Taint로 보호하고 GPU 파드에만 Toleration을 부여하는 패턴을 다룹니다.

Taint와 Toleration: 노드를 특정 파드만 받도록

위험 명령어새 Pod 스케줄링이 막히거나 특정 워크로드만 배치되어 용량 부족과 Pending 상태를 만들 수 있습니다.

노드 스케줄링 제한

안전한 실행 조건: 노드 격리 목적과 해제 명령을 명확히 알고 있을 때만 실행하세요.

실행 전 반드시 확인

  • 현재 컨텍스트와 Namespace가 의도한 대상인지 확인했는가
  • 운영 트래픽이나 상태 저장 데이터에 미치는 영향을 확인했는가
  • 되돌릴 매니페스트, 백업, 또는 복구 절차가 준비되어 있는가
kubectl taint node gpu-node-1 dedicated=gpu:NoSchedule

위 항목을 모두 확인한 후 복사할 수 있습니다

Kubernetes
# GPU 노드에 Taint 추가
kubectl taint node gpu-node-1 dedicated=gpu:NoSchedule
kubectl taint node gpu-node-2 dedicated=gpu:NoSchedule

# Taint 확인
kubectl describe node gpu-node-1 | grep -A 3 "Taints:"
# Taints:  dedicated=gpu:NoSchedule

# 일반 파드 배포 시도 (Toleration 없음)
kubectl run ordinary-pod --image=nginx
kubectl get pod ordinary-pod -o wide
# NAME          NODE    STATUS
# ordinary-pod  <none>  Pending   ← gpu-node에 배치 불가, 다른 노드에 배치됨
YAML
# GPU 파드 — Taint를 Tolerate하는 파드
apiVersion: v1
kind: Pod
metadata:
  name: gpu-job
spec:
  tolerations:
    # Taint key=dedicated, value=gpu, effect=NoSchedule을 허용
    - key: "dedicated"
      operator: "Equal"
      value: "gpu"
      effect: "NoSchedule"
  affinity:
    nodeAffinity:
      # Toleration으로 들어갈 수는 있지만, 반드시 GPU 노드로 가도록 Affinity도 설정
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
          - matchExpressions:
              - key: dedicated
                operator: In
                values:
                  - gpu
  containers:
    - name: cuda-workload
      image: nvidia/cuda:12.0-base
      resources:
        limits:
          nvidia.com/gpu: 1
Kubernetes
kubectl apply -f gpu-job.yaml
kubectl get pod gpu-job -o wide
# NAME      NODE          STATUS
# gpu-job   gpu-node-1   Running   ← GPU 노드에 정상 배치
💡개념

NoSchedule vs PreferNoSchedule vs NoExecute

노드 유지보수를 위해 Taint를 추가했는데 이미 실행 중인 파드가 그대로 남아 있습니다. 반대로 노드에 디스크 압박이 생겨 NoExecute Taint가 자동으로 추가됐더니 실행 중이던 파드가 모두 퇴출됐습니다. Taint effect 선택에 따라 기존 파드에 미치는 영향이 전혀 다릅니다. NoSchedule은 새 파드만 막고 기존 파드는 유지하며, NoExecute는 기존 파드도 퇴출시킵니다. 유지보수 상황과 긴급 격리 상황에서 어떤 effect를 써야 하는지 이해하면 의도치 않은 서비스 중단을 막을 수 있습니다. 이 CB에서는 세 가지 Taint effect의 차이와 실무 사용 상황을 다룹니다.

위험 명령어새 Pod 스케줄링이 막히거나 특정 워크로드만 배치되어 용량 부족과 Pending 상태를 만들 수 있습니다.

노드 스케줄링 제한

안전한 실행 조건: 노드 격리 목적과 해제 명령을 명확히 알고 있을 때만 실행하세요.

실행 전 반드시 확인

  • 현재 컨텍스트와 Namespace가 의도한 대상인지 확인했는가
  • 운영 트래픽이나 상태 저장 데이터에 미치는 영향을 확인했는가
  • 되돌릴 매니페스트, 백업, 또는 복구 절차가 준비되어 있는가
kubectl taint node node-1 maintenance=true:NoSchedule

위 항목을 모두 확인한 후 복사할 수 있습니다

Kubernetes
# NoSchedule: 새 파드 스케줄 차단 (기존 파드 영향 없음)
kubectl taint node node-1 maintenance=true:NoSchedule

# PreferNoSchedule: 가능하면 배치 안 함 (다른 노드 없으면 배치)
kubectl taint node node-1 disk-pressure=high:PreferNoSchedule

# NoExecute: 새 파드 차단 + 기존 파드도 Toleration 없으면 퇴출
kubectl taint node node-1 maintenance=true:NoExecute

# Taint 제거
kubectl taint node node-1 maintenance=true:NoSchedule-   # 끝에 - 붙이면 제거
YAML
# NoExecute Taint를 일정 시간 허용하는 Toleration
spec:
  tolerations:
    - key: "node.kubernetes.io/not-ready"
      operator: "Exists"
      effect: "NoExecute"
      tolerationSeconds: 300   # 노드 not-ready 상태를 5분간 허용 후 퇴출
Effect새 파드 스케줄기존 파드 퇴출
NoSchedule차단없음
PreferNoSchedule기피없음
NoExecute차단즉시 퇴출 (tolerationSeconds 설정 가능)

실습: 전용 노드 설정 패턴

고메모리 노드 그룹을 배치 작업 전용으로 설정하고, 일반 파드가 들어오지 못하도록 합니다.

위험 명령어새 Pod 스케줄링이 막히거나 특정 워크로드만 배치되어 용량 부족과 Pending 상태를 만들 수 있습니다.

노드 스케줄링 제한

안전한 실행 조건: 노드 격리 목적과 해제 명령을 명확히 알고 있을 때만 실행하세요.

실행 전 반드시 확인

  • 현재 컨텍스트와 Namespace가 의도한 대상인지 확인했는가
  • 운영 트래픽이나 상태 저장 데이터에 미치는 영향을 확인했는가
  • 되돌릴 매니페스트, 백업, 또는 복구 절차가 준비되어 있는가
kubectl taint node high-mem-node-1 dedicated=batch:NoSchedule

위 항목을 모두 확인한 후 복사할 수 있습니다

Kubernetes
# 1. 고메모리 노드에 레이블과 Taint 추가
kubectl label node high-mem-node-1 node-type=high-memory workload=batch
kubectl label node high-mem-node-2 node-type=high-memory workload=batch
kubectl taint node high-mem-node-1 dedicated=batch:NoSchedule
kubectl taint node high-mem-node-2 dedicated=batch:NoSchedule

# 2. 일반 파드 → 고메모리 노드에 배치 불가 확인
kubectl run normal-api --image=nginx --replicas=3
kubectl get pods -l run=normal-api -o wide
# 모두 일반 노드에 배치됨 (Taint가 막아줌)
YAML
# batch-job.yaml — 배치 작업 전용 Job
apiVersion: batch/v1
kind: Job
metadata:
  name: data-processing-job
spec:
  template:
    spec:
      tolerations:
        - key: "dedicated"
          operator: "Equal"
          value: "batch"
          effect: "NoSchedule"
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
              - matchExpressions:
                  - key: node-type
                    operator: In
                    values:
                      - high-memory
      containers:
        - name: data-processor
          image: myrepo/data-processor:v1.5
          resources:
            requests:
              memory: "32Gi"
              cpu: "4"
            limits:
              memory: "48Gi"
              cpu: "8"
      restartPolicy: Never
Kubernetes
kubectl apply -f batch-job.yaml
kubectl get pod -l job-name=data-processing-job -o wide
# NAME                    NODE              STATUS
# data-processing-job-xxx high-mem-node-1  Running  ← 전용 노드에 배치

ML 추론 서버가 GPU 노드가 아닌 일반 CPU 노드에 배치되어 추론 속도가 100배 느려지는 상황이 발생했습니다. 로그를 보면 분명 실행 중인데 GPU를 인식하지 못합니다.

Kubernetes
# 증상: 파드가 GPU 노드가 아닌 일반 노드에 배치됨
kubectl get pod ml-inference-xxx -o wide
# NAME              NODE      STATUS   NODE
# ml-inference-xxx  node-5   Running   ← GPU 노드(gpu-node-1,2)가 아닌 일반 노드!

# 원인 조사: Affinity 설정 확인
kubectl get pod ml-inference-xxx -o yaml | grep -A 20 affinity
# nodeAffinity:
#   requiredDuringSchedulingIgnoredDuringExecution:
#     nodeSelectorTerms:
#       - matchExpressions:
#           - key: acceleretor    ← 오타! accelerator가 아닌 acceleretor
#             operator: In
#             values:
#               - nvidia-gpu

# 노드 레이블 확인
kubectl get nodes --show-labels | grep accelerator
# gpu-node-1   accelerator=nvidia-gpu   ← 노드에는 accelerator (올바른 스펠링)
Kubernetes
# 스케줄러 결정 과정 확인
kubectl describe pod ml-inference-xxx | grep -A 10 "Events:"
# Events:
#   Warning  FailedScheduling  ...  0/5 nodes are available:
#     5 node(s) didn't match Pod's node affinity/selector.
# ← required 조건이 실패해야 하는데, 어딘가에 파드가 배치됨

# 이 경우: required 조건의 key 오타로 인해
# 조건 자체가 무효가 되어 스케줄러가 무시한 것
# 오타가 있는 key는 어떤 노드에도 해당 레이블이 없으므로
# In 연산자로 매칭되는 노드가 0개 → preferred 조건으로 fallback 동작
Kubernetes
# 수정: Deployment의 Affinity 설정 수정
kubectl edit deployment ml-inference
# acceleretor → accelerator 로 수정

# 수정 후 파드 재배치 확인
kubectl rollout restart deployment ml-inference
kubectl get pods -l app=ml-inference -o wide
# NAME              NODE          STATUS
# ml-inference-aaa  gpu-node-1   Running  ← 올바른 노드로 재배치
# ml-inference-bbb  gpu-node-2   Running

# 예방: 배포 전 dry-run으로 affinity 매칭 노드 확인
kubectl get nodes -o json | jq '.items[] | select(.metadata.labels.accelerator != null) | .metadata.name'
# "gpu-node-1"
# "gpu-node-2"
# ← 이 노드들이 파드를 받아야 함

근본 원인: required 조건에 오타가 있는 레이블 키를 사용하면, 해당 레이블이 어떤 노드에도 없으므로 조건이 "의도와 다르게 통과"되는 경우가 있습니다. Taint+Affinity를 함께 사용하면 오타가 있더라도 Taint가 1차 방어선 역할을 합니다. 또한 kubectl auth can-i 처럼 파드와 노드 간 affinity 매칭을 dry-run으로 검증하는 절차를 CI/CD에 추가하는 것이 좋습니다.

💼
실무 맥락
현업 패턴

시나리오: EKS에서 스팟 인스턴스와 온디맨드 인스턴스 혼합 사용

비용 절감을 위해 EKS에 스팟 인스턴스 노드 그룹을 추가했습니다. 상태 비저장 배치 작업은 스팟에, 프로덕션 API는 온디맨드에 배치해야 합니다.

Kubernetes
# 노드 그룹 레이블 확인 (EKS 관리형 노드 그룹이 자동으로 붙임)
kubectl get nodes --show-labels | grep lifecycle
# node-1  lifecycle=Ec2Spot
# node-2  lifecycle=Ec2Spot
# node-3  lifecycle=OnDemand
# node-4  lifecycle=OnDemand
YAML
# 프로덕션 API — 온디맨드 필수 + 스팟은 절대 안됨
apiVersion: apps/v1
kind: Deployment
metadata:
  name: payment-api
spec:
  template:
    spec:
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
              - matchExpressions:
                  - key: lifecycle
                    operator: In
                    values:
                      - OnDemand
---
# 배치 작업 — 스팟 선호, 온디맨드도 허용
apiVersion: batch/v1
kind: Job
metadata:
  name: etl-job
spec:
  template:
    spec:
      affinity:
        nodeAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 100
              preference:
                matchExpressions:
                  - key: lifecycle
                    operator: In
                    values:
                      - Ec2Spot
      # 스팟 인스턴스 회수(interruption) 시 빠르게 종료되도록
      terminationGracePeriodSeconds: 30
      containers:
        - name: etl
          image: myrepo/etl-job:v3

실무 포인트: EKS Karpenter를 사용하면 Node Affinity와 Taint를 기반으로 노드를 동적으로 프로비저닝합니다. nodeSelector: karpenter.sh/capacity-type: spot처럼 단순 레이블로도 스팟/온디맨드를 제어할 수 있어 Affinity 설정이 더 직관적이 됩니다.

핵심 요약

개념명령/설정실무 사용 빈도
노드 레이블 추가kubectl label node <node> key=value노드 그룹 설정 시
Taint 추가kubectl taint node <node> key=value:effect전용 노드 격리 시
Taint 제거kubectl taint node <node> key=value:effect-격리 해제 시
required AffinityrequiredDuringSchedulingIgnoredDuringExecution반드시 특정 노드에
preferred AffinitypreferredDuringSchedulingIgnoredDuringExecution선호 노드 지정
Toleration 설정spec.tolerations[].key/effect전용 노드 파드에
Pending 원인 확인kubectl describe pod <pod> Events 섹션스케줄 실패 디버깅
노드 레이블 확인kubectl get nodes --show-labelsAffinity 작성 전

지식 확인

퀴즈 — 4문제

Q1

requiredDuringSchedulingIgnoredDuringExecution Node Affinity의 의미는?

Q2

Taint NoSchedule과 NoExecute의 차이는?

Q3

GPU 전용 노드에 일반 파드가 스케줄되지 않도록 하는 가장 확실한 방법은?

Q4

파드가 Pending 상태로 멈춰있고 kubectl describe pod에 'no nodes are available that match all of the following predicates'가 나타난다면?

0 / 4 답변

🧪 실습으로 확인하기

K8s 기초 — Pod/Deployment/Service 생성

초급

kubectl로 nginx Pod를 생성하고 Deployment와 Service를 차례로 만들어 클러스터 외부에서 접근 가능한 상태까지 구성한다. K8s 3대 리소스의 역할과 관계를 직접 손으로 익힌다.

40📋 5단계💻 직접 환경
실습 시작하기 →

이것도 배워보세요

kubernetes중급 · 65
[Kubernetes] 복잡한 매니페스트를 차트(Chart) 단위로 원클릭 배포하기
Kubernetes 트랙 계속
docker입문 · 30
[Docker] 백엔드 개발자에게 Docker와 컨테이너 가상화가 필수인 이유
Docker 트랙 시작점