Certified Kubernetes Administrator (CKA) #14 Scheduling 2: Taints/tolerations, Priority/PriorityClass, preemption

#13 Scheduling 1에서는 nodeSelector와 affinity로 Pod가 노드를 고르는 메커니즘을 다뤘습니다. 이번 글은 방향이 반대입니다. 노드가 Pod를 밀어내는 taint, 그리고 자원이 부족할 때 누구를 먼저 살릴지 정하는 PriorityClass와 preemption을 다룹니다.

affinity가 Pod의 끌어당김이라면 taint는 노드의 밀어냄입니다. 둘은 정반대 방향의 도구라 함께 두면 노드 배치를 양쪽에서 통제할 수 있습니다. 여기에 우선순위가 더해지면, 노드가 가득 찼을 때 스케줄러가 낮은 우선순위 Pod를 쫓아내고 높은 우선순위 Pod를 들이는 preemption까지 일어납니다. 운영자 관점에서 YAML과 kubectl로 차례로 손에 익히겠습니다.

Taint와 Toleration: 노드가 거부하고 Pod가 받아들인다 #

taint와 toleration은 한 쌍으로 동작합니다. taint는 노드에 거는 거부 표시이고, toleration은 그 거부를 견디겠다는 Pod의 선언입니다. 노드에 taint가 걸려 있으면, 그 taint를 견디는 toleration을 가진 Pod만 그 노드에 들어갈 수 있습니다.

노드:  "나는 key=value:NoSchedule이라는 거부 표시를 걸었다"
Pod A: toleration 없음        → 이 노드에 못 들어감
Pod B: 같은 taint를 견딤      → 이 노드에 들어갈 수 있음

여기서 핵심은 toleration이 허가가 아니라 면제라는 점입니다. toleration을 가진 Pod가 반드시 그 노드로 가는 것이 아니라, 그 노드의 거부에서 면제될 뿐입니다. 실제로 그 노드에 Pod를 보내려면 #13의 nodeAffinity나 nodeSelector를 함께 써야 합니다. 이 차이가 시험에서 자주 헷갈리는 지점입니다.

taint와 affinity의 역할은 다음처럼 갈립니다.

도구방향주체효과
nodeAffinity / nodeSelector끌어당김PodPod가 특정 노드를 고른다
taint / toleration밀어냄노드노드가 Pod를 거부하고, toleration이 그 거부를 면제한다

Taint를 거는 법 #

taint는 kubectl taint node로 겁니다. 형식은 key=value:effect입니다.

# 노드에 taint를 건다 (key=value:effect)
k taint node node01 gpu=true:NoSchedule

# taint 제거 (끝에 - 를 붙인다)
k taint node node01 gpu=true:NoSchedule-

# 노드에 걸린 taint 확인
k describe node node01 | grep -i taint
# Taints:  gpu=true:NoSchedule

value는 생략할 수 있고, 그때는 key와 effect만으로 taint가 성립합니다.

# value 없는 taint
k taint node node01 dedicated:NoSchedule

Effect 세 가지: NoSchedule / PreferNoSchedule / NoExecute #

taint의 강도는 effect가 결정합니다. 세 가지가 있고, 무엇을 막는지가 서로 다릅니다.

effect새 Pod 스케줄이미 떠 있는 Pod
NoScheduletoleration 없으면 배치 거부건드리지 않음
PreferNoSchedule가능하면 피하지만 강제는 아님건드리지 않음
NoExecutetoleration 없으면 배치 거부toleration 없으면 즉시 축출(evict)

NoSchedule은 가장 흔한 effect로, 앞으로 들어올 Pod만 막습니다. PreferNoSchedule은 약한 버전이라 다른 노드가 없으면 결국 배치됩니다. NoExecute는 가장 강합니다. 새 Pod를 막는 데 더해, 이미 그 노드에서 돌고 있던 toleration 없는 Pod까지 쫓아냅니다. 노드를 비워야 할 때 강하게 쓰는 effect입니다.

# NoExecute: 이미 떠 있는 toleration 없는 Pod까지 축출
k taint node node01 maintenance=true:NoExecute

Toleration을 다는 법 #

Pod가 taint를 견디려면 spec.tolerations에 toleration을 적습니다. taint의 key, value, effect와 맞아야 합니다.

apiVersion: v1
kind: Pod
metadata:
  name: gpu-pod
spec:
  tolerations:
  - key: "gpu"
    operator: "Equal"     # key=value가 같은 taint를 견딤
    value: "true"
    effect: "NoSchedule"
  containers:
  - name: app
    image: nginx

operator는 두 가지입니다.

operator의미
Equalkey, value, effect가 모두 일치하는 taint를 견딘다 (value 필요)
Existskey(와 effect)만 일치하면 value와 무관하게 견딘다 (value 생략)

Exists는 value를 보지 않으므로, 특정 key의 모든 taint를 한 번에 견디게 할 때 편합니다. key까지 생략하면 모든 taint를 견디는 toleration이 되는데, 보통 DaemonSet처럼 어디든 떠야 하는 워크로드에서 씁니다.

# 모든 taint를 견딤 (key 생략 + Exists)
tolerations:
- operator: "Exists"

NoExecute와 tolerationSeconds #

NoExecute taint에는 tolerationSeconds를 함께 쓸 수 있습니다. 이 값을 주면 toleration을 가진 Pod라도 무한정 남지 않고, 지정한 초만큼만 머문 뒤 축출됩니다.

tolerations:
- key: "maintenance"
  operator: "Exists"
  effect: "NoExecute"
  tolerationSeconds: 300   # 300초 동안만 견디고 그 뒤 축출

이 동작은 노드 장애 대응에서 그대로 보입니다. 노드가 NotReady가 되면 control plane이 node.kubernetes.io/not-ready:NoExecute taint를 자동으로 답니다. 모든 Pod에는 기본적으로 이 taint에 대한 tolerationSeconds: 300 toleration이 자동으로 주입되어 있어서, 노드가 잠깐 끊겼다 돌아오면 Pod가 살아남고, 5분을 넘기면 다른 노드로 옮겨집니다. tolerationSeconds가 장애 감지와 재배치 사이의 유예 시간인 셈입니다.

Control plane 노드의 기본 taint #

kubeadm으로 세운 클러스터의 control plane 노드에는 기본 taint가 걸려 있습니다. 그래서 일반 워크로드 Pod가 control plane에 올라가지 않습니다.

k describe node controlplane | grep -i taint
# Taints:  node-role.kubernetes.io/control-plane:NoSchedule

이 taint 덕분에 control plane 컴포넌트가 사용자 워크로드와 자원을 다투지 않습니다. 단일 노드 클러스터처럼 control plane에도 Pod를 띄워야 하는 상황이면, 이 taint를 제거하면 됩니다.

# control plane 노드의 기본 taint 제거 (끝에 - )
k taint node controlplane node-role.kubernetes.io/control-plane:NoSchedule-

반대로 kubeadm이 제어 평면 컴포넌트(kube-apiserver 등) Pod를 control plane에 띄울 수 있는 이유도 taint입니다. 그 static Pod들은 이 taint를 견디는 toleration을 가지고 있습니다.

Priority와 PriorityClass #

지금까지는 어디에 배치할지를 다뤘습니다. PriorityClass는 다른 차원입니다. 자원이 부족해 모두를 띄울 수 없을 때 누구를 먼저 살릴지를 정합니다.

PriorityClass는 클러스터 범위(non-namespaced) 리소스로, 정수 우선순위 값을 정의합니다. Pod는 spec.priorityClassName으로 이 클래스를 참조하고, 그 값이 Pod의 우선순위가 됩니다. 값이 클수록 우선순위가 높습니다.

apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: high-priority
value: 1000000
globalDefault: false
preemptionPolicy: PreemptLowerPriority
description: "결제 등 중요 워크로드용 우선순위"
필드의미
value우선순위 정수. 클수록 높음. 사용자 정의는 보통 10억 미만
globalDefaulttruepriorityClassName이 없는 Pod의 기본 우선순위. 클러스터에 하나만 둘 수 있음
preemptionPolicyPreemptLowerPriority(기본)면 preemption 수행. Never면 줄 앞에 서되 축출은 안 함

Pod 쪽에서는 이름으로 참조합니다.

apiVersion: v1
kind: Pod
metadata:
  name: payment
spec:
  priorityClassName: high-priority
  containers:
  - name: app
    image: nginx
# PriorityClass 목록과 값 확인
k get priorityclass
# NAME                      VALUE        GLOBAL-DEFAULT
# high-priority             1000000      false
# system-cluster-critical   2000000000   false
# system-node-critical      2000001000   false

system-node-criticalsystem-cluster-critical은 쿠버네티스가 미리 만들어 두는 시스템 PriorityClass입니다. kube-proxy나 CNI 같은 노드 필수 컴포넌트가 이 값을 써서, 자원이 부족해도 가장 마지막까지 살아남습니다.

Preemption: 낮은 우선순위 Pod를 축출한다 #

우선순위가 실제로 힘을 발휘하는 순간이 preemption입니다. 높은 우선순위 Pod가 Pending인데 클러스터에 빈자리가 없으면, 스케줄러는 더 낮은 우선순위 Pod를 축출(evict)해 자리를 만들고 그 자리에 높은 우선순위 Pod를 넣습니다.

1. high-priority Pod가 Pending. 어느 노드에도 자리가 없음
2. 스케줄러: "이 노드의 low-priority Pod를 비우면 자리가 난다"
3. low-priority Pod가 축출됨 (graceful termination)
4. high-priority Pod가 그 자리에 배치됨

축출된 낮은 우선순위 Pod는 다른 노드에 자리가 있으면 거기로 옮겨가고, 없으면 Pending으로 남습니다. 즉 preemption은 자원을 두고 경쟁할 때 우선순위 순서를 강제하는 장치입니다.

preemptionPolicyNever로 두면 동작이 달라집니다. 이 Pod는 스케줄링 줄에서는 높은 우선순위로 앞에 서지만, 다른 Pod를 축출하지는 않습니다. 자리가 날 때까지 기다리되 남을 쫓아내지는 않아야 하는, 배치 작업 같은 워크로드에 맞습니다.

# 줄 앞에는 서되 축출은 하지 않음
preemptionPolicy: Never

preemption과 PodDisruptionBudget #

preemption은 graceful하게 진행되어, 축출 대상 Pod에 terminationGracePeriod를 줍니다. 다만 PodDisruptionBudget(PDB)은 preemption을 완전히 막지는 못합니다. 스케줄러는 PDB를 가능한 한 존중하려 시도하지만, 높은 우선순위 Pod를 띄울 다른 방법이 없으면 PDB를 위반해서라도 축출할 수 있습니다. PDB는 #15 이후 리소스 관리 흐름에서 다시 만나겠습니다.

Affinity와 무엇이 다른가 #

#13의 affinity와 이번 글의 도구는 자주 한자리에서 비교됩니다. 셋을 한 단락으로 정리하면 이렇습니다. affinity는 Pod가 노드를 끌어당기는 선호이고, taint/toleration은 노드가 Pod를 밀어내는 거부이며, PriorityClass/preemption은 자원이 부족할 때의 생존 순서입니다. 앞의 둘은 “어느 노드에 갈 수 있는가"라는 배치의 문제이고, 마지막 하나는 “자리가 모자랄 때 누가 남는가"라는 경쟁의 문제입니다. 실무에서는 셋을 함께 씁니다. taint로 GPU 노드를 일반 Pod로부터 보호하고, nodeAffinity로 GPU 워크로드를 그 노드로 유도하며, PriorityClass로 그중에서도 중요한 작업을 먼저 살리는 식입니다.

시험 포인트 #

  • toleration은 허가가 아니라 면제다. toleration이 있어도 그 노드로 간다는 보장은 없고, 노드로 보내려면 affinity나 nodeSelector를 함께 쓴다.
  • effect 세 가지를 구분하라. NoSchedule(새 Pod만), PreferNoSchedule(약한 회피), NoExecute(이미 떠 있는 Pod까지 축출).
  • taint는 k taint node <노드> key=value:effect, 제거는 끝에 -를 붙인다.
  • control plane 기본 taint는 node-role.kubernetes.io/control-plane:NoSchedule이다. 단일 노드면 이 taint를 제거해야 워크로드가 올라간다.
  • tolerationSecondsNoExecute에서만 의미가 있고, not-ready/unreachable 자동 taint의 기본 유예가 300초다.
  • PriorityClass value는 클수록 높고, globalDefault: true는 클러스터에 하나만 둔다.
  • preemption은 높은 우선순위 Pod가 Pending일 때 낮은 우선순위 Pod를 축출한다. preemptionPolicy: Never면 줄 앞에 서되 축출은 안 한다.

정리 #

이번 글에서 잡은 것:

  • taint는 노드의 거부, toleration은 그 거부의 면제라는 한 쌍의 동작, 그리고 toleration이 배치를 보장하지 않는다는 핵심
  • effect 세 가지(NoSchedule/PreferNoSchedule/NoExecute)와 NoExecute의 축출, tolerationSeconds의 유예 의미
  • control plane 노드의 기본 taint와 그 제거, 그리고 not-ready 자동 taint의 동작
  • PriorityClass(value/globalDefault/preemptionPolicy)와 preemption으로 자원 경쟁의 생존 순서를 강제하는 법
  • affinity(끌어당김),taint(밀어냄),priority(생존 순서)의 역할 차이

scheduling을 마쳤으니, 이제 그 배치의 전제가 되는 자원 자체를 살펴보겠습니다.

다음: 리소스 관리 #

Pod를 어느 노드에 둘지 정했다면, 그 노드의 자원을 어떻게 나눠 쓸지가 다음 문제입니다. 자원을 적게 잡으면 노드가 과밀해지고, 많이 잡으면 노드가 비어도 다른 Pod가 못 들어옵니다.

#15 리소스 관리: requests/limits, QoS, LimitRange, ResourceQuota에서는 컨테이너가 요청하고 제한하는 CPU와 메모리(requests/limits), 그에 따라 정해지는 QoS class(Guaranteed/Burstable/BestEffort), 네임스페이스 단위로 기본값과 한도를 강제하는 LimitRange와 ResourceQuota를 다루겠습니다. 자원 설정이 스케줄링과 축출 양쪽에 어떻게 연결되는지 함께 정리하겠습니다.

X