스케줄러는 딱 한 가지 결정만 한다 — 이 파드를 어느 노드에 둘 것인가.
-
① 필터링 — 둘 수 있는 노드를 추린다
- 자원이 남는가 (requests 기준)
- 노드 셀렉터·어피니티 조건에 맞는가
- 테인트를 견딜 수 있는가
- 포트가 비어 있는가 · 볼륨을 붙일 수 있는가
-
② 스코어링 — 남은 것 중 가장 좋은 곳을 고른다
- 자원이 고르게 퍼지도록 · 이미지가 이미 있는 노드 선호
-
하나도 안 남으면 Pending 이고, 이유가 이벤트에 그대로 적힌다
kubectl describe pod web-xxx | grep -A10 Events
# 0/5 nodes are available: 3 Insufficient cpu,
# 2 node(s) had untolerated taint {node-role.kubernetes.io/control-plane}
배치를 유도하는 방법
# 가장 단순 — 라벨이 맞는 노드에만
nodeSelector: { disktype: ssd }
# 어피니티 — 조건을 더 표현할 수 있다
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution: ... # 없으면 Pending
preferredDuringSchedulingIgnoredDuringExecution: ... # 없으면 그냥 다른 곳
IgnoredDuringExecution 이 이름에 붙은 이유
- 이미 떠 있는 파드는 조건이 깨져도 쫓겨나지 않는다
- 배치 시점에만 본다
anti-affinity 의 함정
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector: { matchLabels: { app: web } }
topologyKey: kubernetes.io/hostname
required 로 걸고 replicas 를 노드 수보다 크게 잡으면 남는 파드가 '영원히' Pending 이 된다 — 매우 흔한 실수다
- 노드가 3대인데 replicas: 5 면 2개는 절대 안 뜬다
- preferred 로 바꾸거나 topologySpreadConstraints 를 쓴다
topologySpreadConstraints 가 더 낫다
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
labelSelector: { matchLabels: { app: web } }
anti-affinity 는 '같이 두지 마라' 만 말할 수 있다 spread 는 '얼마나 고르게' 를 수치로 말한다
whenUnsatisfiable
DoNotSchedule 못 지키면 Pending
ScheduleAnyway 못 지켜도 배치 (점수만 반영) ← 대개 이쪽이 안전하다
스케줄러가 못 고치는 것
스케줄러는 '배치 시점' 에만 개입한다 이미 떠 있는 파드를 재배치하지 않는다
- 노드를 새로 추가해도 기존 파드가 옮겨가지 않는다
- 노드가 한쪽에 몰려도 스스로 균형을 맞추지 않는다
재배치가 필요하면
- descheduler 를 돌린다 (규칙에 맞춰 파드를 퇴출 → 재스케줄)
- 또는 rollout restart 로 새로 띄운다
커스텀 스케줄러
schedulerName 을 지정하면 다른 스케줄러가 처리한다
spec:
schedulerName: my-scheduler
기본 스케줄러는 그 파드를 건드리지 않는다
- 커스텀 스케줄러가 없으면 영원히 Pending
- 이름 오타로 Pending 이 나는 사례가 있다
배치 워크로드에서는 gang scheduling(전부 아니면 전무)이 필요해 Volcano · Kueue 같은 별도 스케줄러를 쓰기도 한다
면접 함정
- ❌ "노드에 자원이 남으면 배치된다" → requests 합 기준이다. 실제 여유와 다르다.
- ❌ "anti-affinity를 걸면 안전하다" → required면 replicas가 노드 수를 넘는 순간 영구 Pending이다.