[Daily morning study] Kubernetes 고급 스케줄링 - Taints, Tolerations, Node Affinity
#daily morning study
기본 스케줄링의 한계
쿠버네티스 기본 스케줄러는 요청한 리소스(CPU, 메모리)와 노드의 가용 리소스를 기준으로 Pod를 배치한다. 하지만 실제 운영 환경에서는 이것만으로는 부족한 경우가 많다.
- GPU 노드에는 ML 워크로드만 올리고 싶다
- 특정 팀의 Pod는 특정 노드 그룹에만 스케줄링되어야 한다
- 프로덕션 워크로드와 개발 워크로드를 물리적으로 분리하고 싶다
- 고가용성을 위해 Pod 레플리카를 여러 존(AZ)에 분산시키고 싶다
이를 위해 쿠버네티스는 Taints/Tolerations와 Node Affinity/Anti-Affinity, Pod Affinity/Anti-Affinity 메커니즘을 제공한다.
Taints와 Tolerations
개념
Taint는 노드에 “오점”을 표시하는 것이다. Taint가 있는 노드에는 해당 Taint를 “참을 수 있는(Tolerate)” Pod만 스케줄링된다.
노드에 Taint 추가:
kubectl taint nodes node1 key=value:effect
Pod에 Toleration 추가:
Pod가 해당 Taint를 무시(tolerate)할 수 있음을 선언
Taint Effect의 종류
| Effect | 동작 |
|---|---|
NoSchedule | Toleration 없는 Pod는 이 노드에 스케줄링되지 않음 |
PreferNoSchedule | 가능하면 스케줄링하지 않음, 다른 노드가 없으면 허용 |
NoExecute | 스케줄링 금지 + 이미 실행 중인 Pod도 축출(Evict) |
예시
# 노드에 Taint 추가 (GPU 전용 노드)
kubectl taint nodes gpu-node-1 gpu=true:NoSchedule
# Pod에 Toleration 추가
apiVersion: v1
kind: Pod
metadata:
name: ml-training-pod
spec:
tolerations:
- key: "gpu"
operator: "Equal"
value: "true"
effect: "NoSchedule"
containers:
- name: trainer
image: ml-trainer:latest
resources:
limits:
nvidia.com/gpu: 1
operator에는 Equal(key=value 일치)과 Exists(key만 일치)가 있다.
NoExecute와 tolerationSeconds
NoExecute Taint는 이미 실행 중인 Pod도 내쫓는다. tolerationSeconds를 설정하면 일정 시간 후에 축출된다.
tolerations:
- key: "node.kubernetes.io/not-ready"
operator: "Exists"
effect: "NoExecute"
tolerationSeconds: 300 # 노드가 NotReady가 되어도 5분간 유지
쿠버네티스는 기본적으로 모든 Pod에 node.kubernetes.io/not-ready:NoExecute와 node.kubernetes.io/unreachable:NoExecute에 대한 Toleration을 자동으로 추가하며, tolerationSeconds: 300이 기본값이다.
시스템 컴포넌트의 Taint
마스터 노드(Control Plane)에는 기본적으로 아래 Taint가 설정되어 있다.
node-role.kubernetes.io/control-plane:NoSchedule
그래서 일반 Pod는 마스터 노드에 스케줄링되지 않는다. kube-system의 DaemonSet들은 이 Taint에 대한 Toleration을 가지고 있어 마스터에도 실행된다.
Node Affinity
nodeSelector vs Node Affinity
nodeSelector는 단순한 key=value 매칭만 지원한다. Node Affinity는 더 표현력 있는 조건을 지원한다.
# nodeSelector - 단순
spec:
nodeSelector:
disktype: ssd
# nodeAffinity - 표현력 풍부
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: disktype
operator: In
values:
- ssd
- nvme
두 가지 모드
requiredDuringSchedulingIgnoredDuringExecution
- 스케줄링 시 반드시 만족해야 함 (Hard constraint)
- 조건을 만족하는 노드가 없으면 Pending 상태
- 실행 중에 노드 레이블이 바뀌어도 Pod를 축출하지 않음 (Ignored)
preferredDuringSchedulingIgnoredDuringExecution
- 선호하는 조건 (Soft constraint)
- 조건을 만족하는 노드를 우선 선택하되, 없으면 다른 노드에도 배치
weight값(1~100)으로 선호도 강도 설정
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/os
operator: In
values:
- linux
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
지원하는 Operator
| Operator | 의미 |
|---|---|
In | 값이 목록에 포함됨 |
NotIn | 값이 목록에 포함되지 않음 |
Exists | 키가 존재함 |
DoesNotExist | 키가 존재하지 않음 |
Gt | 값이 지정된 값보다 큼 |
Lt | 값이 지정된 값보다 작음 |
Pod Affinity / Anti-Affinity
Node Affinity가 노드의 레이블을 기준으로 하는 반면, Pod Affinity는 이미 실행 중인 다른 Pod를 기준으로 스케줄링을 결정한다.
Pod Affinity
같은 노드(또는 존, 리전)에 특정 Pod와 함께 배치하고 싶을 때 사용한다.
spec:
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- cache
topologyKey: kubernetes.io/hostname # 같은 노드에 배치
사용 사례: 웹 서버 Pod를 캐시(Redis) Pod와 같은 노드에 배치하여 네트워크 지연 최소화.
Pod Anti-Affinity
특정 Pod와 같은 위치에 배치되지 않도록 한다. 고가용성 구성에 필수적이다.
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- web-frontend
topologyKey: topology.kubernetes.io/zone # 다른 AZ에 배치
사용 사례: 웹 프론트엔드 레플리카 3개를 서로 다른 AZ에 분산하여 단일 장애점 제거.
topologyKey
topologyKey는 “어떤 단위”로 같은 위치/다른 위치를 판단할지 결정한다.
| topologyKey | 범위 |
|---|---|
kubernetes.io/hostname | 같은 노드 |
topology.kubernetes.io/zone | 같은 가용 영역(AZ) |
topology.kubernetes.io/region | 같은 리전 |
실전 패턴: 세 가지 메커니즘 조합
패턴 1: 특정 팀 전용 노드 풀
# 팀 노드에 Taint 추가
kubectl taint nodes team-a-node-1 team=a:NoSchedule
# 팀 노드에 레이블 추가
kubectl label nodes team-a-node-1 team=a
# 팀 A의 Pod
spec:
tolerations:
- key: "team"
operator: "Equal"
value: "a"
effect: "NoSchedule"
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: team
operator: In
values:
- a
Taint만 있으면 toleration을 가진 다른 팀의 Pod도 들어올 수 있다. nodeAffinity까지 함께 사용해야 완전히 격리된다.
패턴 2: 고가용성 Deployment
레플리카 3개를 3개의 AZ에 분산시키는 구성:
spec:
replicas: 3
template:
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: my-web
topologyKey: topology.kubernetes.io/zone
이 설정으로 같은 AZ에 2개 이상의 레플리카가 배치되지 않는다. 단, AZ가 2개뿐이라면 레플리카 3개 중 하나가 Pending 상태가 된다.
패턴 3: Topology Spread Constraints (권장)
쿠버네티스 1.18+에서는 topologySpreadConstraints를 사용하면 더 유연하게 분산 배치를 설정할 수 있다.
spec:
topologySpreadConstraints:
- maxSkew: 1 # 존 간 최대 Pod 수 차이
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule # 조건 불만족 시 스케줄링 금지
labelSelector:
matchLabels:
app: my-web
maxSkew: 1이면 AZ 간 Pod 수 차이가 최대 1개로 유지된다. Pod Anti-Affinity보다 더 정밀한 분산 제어가 가능하다.
디버깅
Pod가 Pending 상태일 때 스케줄링 실패 이유를 확인하는 방법:
# Pod 이벤트 확인
kubectl describe pod <pod-name>
# 주요 이유들
# "0/5 nodes are available: 5 node(s) had taint {...}, that the pod didn't tolerate"
# "0/5 nodes are available: 5 node(s) didn't match pod affinity rules"
# "0/5 nodes are available: 5 node(s) didn't match node selector"
kubectl get nodes --show-labels로 노드 레이블을, kubectl describe node <node> 하단의 Taints 섹션으로 Taint를 확인할 수 있다.
정리
| 메커니즘 | 기준 | 용도 |
|---|---|---|
| Taints/Tolerations | 노드에 오점 표시 | 노드 격리, 특수 노드 보호 |
| Node Affinity | 노드의 레이블 | 특정 노드 그룹 선호/필수 |
| Pod Affinity | 실행 중인 Pod의 레이블 | 함께 배치 (코로케이션) |
| Pod Anti-Affinity | 실행 중인 Pod의 레이블 | 분리 배치 (고가용성) |
| Topology Spread | Pod 분산 정도 | 균등 분산 |
Taint/Toleration은 “이 노드는 특별해, 허가된 Pod만 들어와”의 관점이고, Affinity는 “이 Pod는 어디 가고 싶어”의 관점이다. 두 메커니즘은 서로 독립적으로 동작하므로 완전한 격리가 필요하다면 둘 다 사용해야 한다.