스케줄러 — Pod이 어떤 노드에 배치되는지 결정하는 과정
kubectl apply로 Pod을 배포하면 "어떤 노드"에서 실행될지 누가, 어떤 기준으로 결정할까요?
Kubernetes 스케줄러(kube-scheduler)는 새로 생성된 Pod을 적절한 노드에 배치하는 역할을 합니다. 단순히 빈 노드를 찾는 것이 아니라, 리소스 상황, 제약 조건, 분산 정책 등을 종합적으로 고려합니다.
스케줄링 과정
스케줄러는 두 단계를 거칩니다.
1단계: 필터링 (Filtering)
Pod을 실행할 수 없는 노드를 제외합니다.
- **리소스 부족 **: CPU/메모리 requests를 만족하지 못하는 노드
- nodeSelector/Affinity: 조건에 맞지 않는 노드
- Taints: 해당 Taint를 Tolerate하지 않는 Pod은 배제
- **PV 토폴로지 **: 볼륨이 특정 AZ에 있으면 해당 AZ 노드만 통과
- **Port 충돌 **: hostPort가 이미 사용 중인 노드
2단계: 스코어링 (Scoring)
필터링을 통과한 노드들에 점수를 매깁니다.
- LeastRequestedPriority: 리소스 여유가 많은 노드 선호
- BalancedResourceAllocation: CPU/메모리 사용 비율이 균형 잡힌 노드 선호
- InterPodAffinity: Pod Affinity 규칙에 부합하는 노드에 가산점
- NodeAffinity: preferredDuringScheduling 규칙에 따른 가산점
- ImageLocality: 이미 필요한 이미지를 가진 노드 선호
Pod 생성 → 필터링 (부적합 노드 제외) → 스코어링 (점수 부여) → 최고 점수 노드에 바인딩
nodeSelector — 가장 단순한 노드 선택
spec:
nodeSelector:
disk-type: ssd
region: ap-northeast-2
# 노드에 레이블 추가
kubectl label nodes node-1 disk-type=ssd
nodeSelector는 AND 조건의 정확한 매칭만 가능합니다. 더 유연한 조건이 필요하면 nodeAffinity를 사용합니다.
Node Affinity — 유연한 노드 선택
spec:
affinity:
nodeAffinity:
# 필수 조건: 반드시 충족해야 스케줄링
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values:
- ap-northeast-2a
- ap-northeast-2b
preferred 조건은 충족하면 스코어링에서 가산점을 부여하지만, 충족하지 못해도 스케줄링은 진행됩니다.
# 선호 조건: 충족하면 가산점
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 80
preference:
matchExpressions:
- key: disk-type
operator: In
values:
- ssd
- weight: 20
preference:
matchExpressions:
- key: instance-type
operator: In
values:
- compute-optimized
연산자 종류
| 연산자 | 설명 | 예시 |
|---|---|---|
In | 값이 목록에 포함 | zone In [a, b] |
NotIn | 값이 목록에 미포함 | zone NotIn [c] |
Exists | 키가 존재 | gpu Exists |
DoesNotExist | 키가 없음 | spot DoesNotExist |
Gt | 값이 초과 | cpu-count Gt 4 |
Lt | 값이 미만 | memory-gb Lt 32 |
Pod Affinity / Anti-Affinity
Pod 간의 관계를 기반으로 스케줄링합니다.
Pod Affinity — 함께 배치
spec:
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- cache
topologyKey: kubernetes.io/hostname # 같은 노드에
캐시 서버와 같은 노드에 배치하여 네트워크 지연을 최소화하는 경우에 사용합니다.
Pod Anti-Affinity — 분산 배치
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- web
topologyKey: kubernetes.io/hostname # 서로 다른 노드에
같은 앱의 Pod을 서로 다른 노드에 분산시켜 한 노드 장애 시 전체 서비스가 중단되지 않도록 합니다.
topologyKey
분산의 기준이 되는 토폴로지를 지정합니다.
| topologyKey | 의미 |
|---|---|
kubernetes.io/hostname | 노드 단위 분산 |
topology.kubernetes.io/zone | AZ 단위 분산 |
topology.kubernetes.io/region | 리전 단위 분산 |
Topology Spread Constraints
Pod을 토폴로지에 걸쳐 균등하게 분산합니다. Anti-Affinity보다 세밀한 제어가 가능합니다.
spec:
topologySpreadConstraints:
- maxSkew: 1 # AZ 간 최대 편차
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule # 불만족 시 스케줄 거부
labelSelector:
matchLabels:
app: web
- maxSkew: 1 # 노드 간 최대 편차
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway # 불만족이어도 스케줄
labelSelector:
matchLabels:
app: web
- maxSkew: 토폴로지 간 Pod 수 최대 차이 (1이면 균등 분배)
- whenUnsatisfiable:
DoNotSchedule(거부) 또는ScheduleAnyway(최선 노력)
예를 들어 3개의 AZ에 6개 Pod을 배포하면 각 AZ에 2개씩 배치됩니다.
커스텀 스케줄러
기본 스케줄러 외에 추가 스케줄러를 배포할 수 있습니다.
spec:
schedulerName: my-custom-scheduler # 기본: default-scheduler
containers:
- name: app
image: my-app:1.0
머신러닝 워크로드의 GPU 최적화, 특수한 배치 전략 등이 필요할 때 사용합니다.
스케줄링 디버깅
# Pod이 Pending인 이유 확인
kubectl describe pod my-pod
# Events:
# Warning FailedScheduling 0/3 nodes are available:
# 1 node(s) had taint {gpu: true}, that the pod didn't tolerate,
# 2 Insufficient memory.
# 노드 리소스 상황 확인
kubectl describe node node-1 | grep -A 5 "Allocated resources"
# 스케줄러 이벤트 확인
kubectl get events --field-selector source=default-scheduler
자주 보는 스케줄링 실패 원인
| 메시지 | 원인 | 해결 |
|---|---|---|
| Insufficient cpu/memory | 리소스 부족 | 노드 추가 또는 requests 조정 |
| node(s) didn't match selector | nodeSelector 불일치 | 레이블 확인 |
| node(s) had taints | Taint 미허용 | Toleration 추가 |
| no persistent volumes available | PV 없음 | PV/StorageClass 확인 |
실무 권장 설정
apiVersion: apps/v1
kind: Deployment
metadata:
name: production-web
spec:
replicas: 6
template:
spec:
affinity:
# AZ 분산
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app: web
topologyKey: topology.kubernetes.io/zone
topologySpreadConstraints를 추가하면 AZ 간 Pod 수의 편차를 maxSkew 이내로 유지하여 더 균등한 분산을 보장합니다.
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: web
containers:
- name: web
image: web-app:1.0
resources:
requests:
cpu: 200m
memory: 256Mi
주의할 점
1. nodeSelector만 쓰면 해당 라벨의 노드가 없을 때 Pod이 영원히 Pending된다
nodeSelector는 "반드시 이 라벨이 있는 노드에 배치하라"는 하드 조건입니다. 노드가 축소되거나 라벨이 변경되면 조건을 만족하는 노드가 0개가 되어 Pod이 Pending 상태에 머뭅니다. 유연한 배치가 필요하면 nodeAffinity의 preferredDuringSchedulingIgnoredDuringExecution을 사용하세요.
2. Pod Anti-Affinity를 requiredAntiAffinity로 설정하면 스케일 아웃이 막힌다
"같은 노드에 같은 앱의 Pod을 배치하지 마라"를 hard 조건으로 걸면, 노드 수보다 replicas가 많아지는 순간 새 Pod이 스케줄링되지 않습니다. 노드가 3대인데 replicas를 4로 올리면 1개는 Pending 상태가 됩니다. 대부분의 경우 preferred 조건으로 충분합니다.
3. requests를 설정하지 않으면 스케줄러가 노드 리소스를 과대평가한다
스케줄러는 requests 값을 기준으로 노드에 빈 공간이 있는지 판단합니다. requests가 없으면 0으로 간주하여 한 노드에 Pod을 과도하게 배치하고, 실제 사용량이 몰리면 OOM이나 CPU 쓰로틀링이 발생합니다.
정리
Kubernetes 스케줄러는 필터링으로 부적합한 노드를 제외하고, 스코어링으로 최적의 노드를 선택합니다. nodeSelector는 단순하지만 제한적이고, nodeAffinity와 podAffinity/AntiAffinity로 더 세밀한 배치 전략을 구현할 수 있습니다. 프로덕션에서는 topologySpreadConstraints로 AZ 분산을 설정하여 고가용성을 확보하는 것이 중요합니다.