8.4 taint와 toleration
8.3의 affinity는 Pod가 노드를 고르는 방향의 선언이다. taint는 방향이 반대다. 노드 쪽에 조건을 걸어, 그 조건을 명시적으로 허용하지 않는 Pod를 노드가 거부한다. affinity가 “이 Pod는 이런 노드를 원한다"라면, taint와 toleration은 “이 노드는 아무나 받지 않는다"와 “이 Pod는 그 조건을 감수한다"의 짝이다. 스케줄러의 filtering은 두 방향의 선언을 모두 검사한다.
문법과 effect 3종
taint는 노드에 key=value:effect 형태로 붙인다.
kubectl taint nodes node-1 dedicated=db:NoSchedule
kubectl taint nodes node-1 dedicated=db:NoSchedule- # 끝에 -를 붙이면 제거effect가 거부의 강도를 정한다.
| effect | 새 Pod 배치 | 실행 중인 Pod |
|---|---|---|
NoSchedule | 차단 | 유지 |
PreferNoSchedule | 가급적 회피 | 유지 |
NoExecute | 차단 | 축출 |
NoSchedule은 filtering에서 toleration 없는 Pod를 탈락시키지만 이미 실행 중인 Pod는 건드리지 않는다. PreferNoSchedule은 강제가 아닌 회피라서, 다른 노드가 전부 부적합하면 toleration 없는 Pod도 들어온다. NoExecute는 유일하게 실행 중인 Pod에 소급 적용된다. taint가 붙는 순간 toleration 없는 Pod는 축출되고, toleration이 있어도 tolerationSeconds가 지정되어 있으면 그 시간까지만 머문 뒤 축출된다. 유예 시간을 지정하지 않은 toleration은 무기한 버틴다.
Pod 쪽 toleration은 이렇게 선언한다.
spec:
tolerations:
- key: "dedicated"
operator: "Equal"
value: "db"
effect: "NoSchedule"operator는 두 가지다. Equal은 key와 value가 모두 일치해야 하고, Exists는 key만 맞으면 value를 보지 않는다. Exists에 key까지 생략하면 모든 taint를 허용하는 toleration이 되는데, 모든 노드에 상주해야 하는 DaemonSet 계열이 아니라면 쓸 이유가 없는 형태다.
자주 놓치는 지점은 toleration의 성격이다. toleration은 배치를 허가하는 자격일 뿐 그 노드로 배치를 유도하는 조건이 아니다. toleration만 가진 Pod는 taint 없는 다른 노드로도 얼마든지 배치된다.
시스템이 자동으로 붙이는 taint
taint는 DBA와 클러스터 관리자가 수동으로 붙이는 것만이 아니다. control plane의 node controller와 kubelet은 노드 상태에 따라 taint를 자동으로 부여하고, 상태가 해소되면 제거한다.
| taint | effect | 부여 조건 |
|---|---|---|
node.kubernetes.io/not-ready | NoExecute | Ready 조건이 False |
node.kubernetes.io/unreachable | NoExecute | 노드 응답 두절 (Ready가 Unknown) |
node.kubernetes.io/memory-pressure | NoSchedule | kubelet이 메모리 압박 감지 |
node.kubernetes.io/disk-pressure | NoSchedule | kubelet이 디스크 압박 감지 |
node.kubernetes.io/pid-pressure | NoSchedule | PID 고갈 임박 |
node.kubernetes.io/network-unavailable | NoSchedule | 노드 네트워크 미구성 |
node.kubernetes.io/unschedulable | NoSchedule | kubectl cordon 실행 |
앞의 두 항목이 NoExecute라는 사실이 노드 장애 시나리오의 뼈대다. 노드가 응답을 멈추면 node controller가 unreachable taint를 부여하고, 그 노드의 Pod들은 NoExecute 규칙에 따라 축출 대상이 된다. 그런데 대부분의 Pod는 이 taint에 대한 toleration을 선언한 적이 없는데도 즉시 축출되지 않는다. admission 단계에서 모든 Pod에 not-ready와 unreachable에 대한 toleration이 tolerationSeconds: 300으로 자동 주입되기 때문이다. 노드 장애 후 Pod가 5분을 기다렸다가 다른 노드로 옮겨지는 기본 동작이 이 값에서 나온다. 더 빠른 재배치가 필요한 워크로드는 해당 toleration을 직접 선언해 유예를 줄이고, 반대로 네트워크 순단마다 Pod가 재배치되면 곤란한 워크로드는 유예를 늘린다.
flowchart TD
A["노드 응답 두절"] --> B["node controller가<br/>unreachable taint 부여"]
B --> C["NoExecute 판정"]
C --> D["tolerationSeconds<br/>기본 300초 대기"]
D --> E["Pod 축출"]
E --> F["controller가 새 Pod 생성<br/>다른 노드에 배치"]
전용 노드 패턴
특정 워크로드에 노드를 전용으로 배정하는 요구는 taint 하나로 끝나지 않는다. taint는 다른 Pod의 유입을 막을 뿐이고, 대상 워크로드가 그 노드로 가게 만들려면 label과 nodeSelector(또는 nodeAffinity)가 따로 필요하다. 두 방향을 모두 선언해야 전용이 완성된다.
kubectl taint nodes node-db-1 dedicated=db:NoSchedule
kubectl label nodes node-db-1 dedicated=dbapiVersion: v1
kind: Pod
metadata:
name: postgres-0
spec:
tolerations:
- key: "dedicated"
operator: "Equal"
value: "db"
effect: "NoSchedule"
nodeSelector:
dedicated: db
containers:
- name: postgres
image: postgres:16taint만 있고 label 유도가 없으면 DB Pod가 일반 노드로 흩어지고, label 유도만 있고 taint가 없으면 일반 Pod가 DB 노드의 메모리와 CPU를 잠식해 전용의 의미가 사라진다.
GPU 노드도 같은 패턴이다. GPU를 쓰지 않는 Pod가 고가 노드의 자원을 점유하지 않도록 노드에 taint를 걸고, GPU 워크로드에만 toleration을 준다. GPU 자체는 nvidia.com/gpu 같은 extended resource requests가 있어야 배치되므로 GPU 워크로드의 배치는 이미 제약되어 있고, taint가 담당하는 몫은 GPU 요청이 없는 일반 Pod의 유입 차단이다.
정리
taint와 toleration은 노드가 배치를 거부하는 선언과 그 거부를 통과하는 자격의 짝이며, effect 3종 가운데 NoExecute만 실행 중인 Pod까지 축출한다. 노드 장애 시 Pod가 5분 뒤에 옮겨지는 기본 동작은 자동 주입되는 tolerationSeconds 300초에서 나오고, 전용 노드는 taint에 의한 차단과 label 기반 유도를 함께 선언해야 성립한다.