Skip to content

Scheduling-Slides


marp: true theme: default paginate: true backgroundColor: #1a1a2e color: #eaeaea style: | section { font-family: ‘Pretendard’, ‘Apple SD Gothic Neo’, sans-serif; } h1 { color: #00d4ff; } h2 { color: #00d4ff; } code { background-color: #16213e; } strong { color: #ff6b6b; } table { font-size: 0.8em; } .columns { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }

K8s 스케줄러 Deep Dive

내부 동작 원리부터 Race Condition까지


🤔 질문

Pod를 생성하면 어떤 노드에 배치될까?

apiVersion: v1
kind: Pod
metadata:
name: my-app
spec:
containers:
- name: app
image: nginx
resources:
requests:
memory: "128Mi"
cpu: "500m"

목차

  1. 스케줄링 기본 구조
  2. Scheduling Framework
  3. Scheduling Queue
  4. Preemption Deep Dive
  5. 성능 튜닝
  6. 스케줄러의 한계

1. 스케줄링 기본 구조


3단계로 이해하기

┌─────────────────────────────────────────────────────────┐
│ │
│ Pending Pod │
│ │ │
│ ▼ │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ Filter │ ─▶ │ Score │ ─▶ │ Bind │ │
│ │ (필터링) │ │ (점수화) │ │ (바인딩) │ │
│ └───────────┘ └───────────┘ └───────────┘ │
│ │
│ 5개 노드 → 2개 각 노드 점수 최고 점수 노드 선택 │
│ │
└─────────────────────────────────────────────────────────┘

Filter 단계

조건에 맞지 않는 노드를 제외

Filter역할
NodeResourcesFitCPU/Memory 충분한지
NodeAffinitynodeSelector, nodeAffinity 만족하는지
TaintTolerationTaint를 Toleration하는지
PodTopologySpread토폴로지 분산 조건 만족하는지
Node A ✓ Node B ✗ Node C ✓ Node D ✗ Node E ✓
(taint) (리소스 부족)

Score 단계

남은 노드에 점수 부여 (0~100)

Node A: 75점 ← LeastAllocated (여유 리소스 많음)
Node C: 45점
Node E: 60점

NodeResourcesFit 스코어링 전략:

전략설명용도
LeastAllocated여유 많을수록 높은 점수부하 분산 (기본값)
MostAllocated꽉 찰수록 높은 점수노드 수 최소화, 비용 절감

Bind 단계

최고 점수 노드에 바인딩

┌──────────────┐ ┌──────────────┐
│ Scheduler │ ──────▶ │ API Server │
└──────────────┘ └──────────────┘
pod.spec.nodeName = "node-a"
┌──────────────┐
│ Node A │
│ (kubelet) │
└──────────────┘

2. Scheduling Framework


15개 Extension Point

PreEnqueue ─▶ [Queue] ─▶ PreFilter ─▶ Filter ─▶ PostFilter
PreScore ─▶ Score ─▶ NormalizeScore
Reserve ─▶ Permit ─▶ PreBind ─▶ Bind ─▶ PostBind

왜 이렇게 많을까? → 커스텀 스케줄러 플러그인을 쉽게 끼워넣기 위해


Scheduling Cycle vs Binding Cycle

┌─────────────────────────────────────────────────────────────┐
│ Scheduling Cycle (Serial) │
│ ┌─────────┬─────────┬─────────┬─────────┬─────────┐ │
│ │PreFilter│ Filter │PostFilter│PreScore │ Score │ │
│ └─────────┴─────────┴─────────┴─────────┴─────────┘ │
│ ↓ │
│ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │
│ ↓ │
│ Binding Cycle (Parallel) │
│ ┌─────────┬─────────┬─────────┬─────────┐ │
│ │ Reserve │ Permit │ PreBind │ Bind │ ← goroutine │
│ └─────────┴─────────┴─────────┴─────────┘ │
└─────────────────────────────────────────────────────────────┘

왜 분리? → Bind는 API Server 호출이라 느림. 병렬로 처리해야 throughput 확보


3. Scheduling Queue


3개의 큐

┌─────────────────┐
새 Pod ────────▶ │ ActiveQ │ ────▶ 스케줄링 시도
│ (heap, 우선순위) │
└─────────────────┘
스케줄링 실패
┌─────────────────┐
│ BackoffQ │ ────▶ 대기 후 ActiveQ로
│ (지수 백오프) │
└─────────────────┘
계속 실패 (임계값 초과)
┌─────────────────┐
│ Unschedulable │ ────▶ 클러스터 변경시 복귀
│ Pod Pool │
└─────────────────┘

QueueingHint (v1.32 GA)

문제: Unschedulable Pod를 언제 다시 시도할까?

기존: 아무 이벤트나 발생하면 일단 ActiveQ로 이동 → 비효율

QueueingHint: 이벤트별로 “이 Pod가 스케줄 가능해질 수 있나?” 판단

// 반환값
const (
QueueSkip // 이 이벤트로는 안 됨, 스킵
Queue // 가능성 있음, 큐로 이동
QueueImmediately // 확실함, 즉시 이동
)

4. Preemption Deep Dive


Preemption이란

높은 우선순위 Pod가 낮은 우선순위 Pod를 쫓아내고 자리 확보

┌────────────────────────────────────────┐
│ Node A (capacity: 4 CPU) │
│ │
│ ┌──────────┐ ┌──────────┐ │
│ │ Pod Low │ │ Pod Low │ ← 2 CPU │
│ │ (pri:10) │ │ (pri:10) │ │
│ └──────────┘ └──────────┘ │
│ │
│ High Priority Pod (pri:100) 요청: 3CPU │
│ │ │
│ ▼ │
│ Low Pod 1개 evict → High Pod 배치 │
└────────────────────────────────────────┘

⚠️ 제한사항 1: Inter-Pod Affinity

높은 우선순위 Pod가 낮은 우선순위 Pod에 affinity가 있으면?

cache-pod (priority: 10) ←─── affinity ─── web-pod (priority: 100)

문제:

  1. web-pod 스케줄링 시도
  2. preemption 시뮬레이션: cache-pod 제거하면 자리 생김
  3. 근데 cache-pod 제거하면 affinity 만족 불가
  4. 결과: web-pod는 영원히 Pending

해결책: affinity 대상 Pod의 우선순위를 같거나 높게 설정


⚠️ 제한사항 2: Cross-Node Preemption 불가

다른 노드의 Pod는 쫓아내지 않음

┌──────────────┐ ┌──────────────┐
│ Node 1 │ │ Node 2 │
│ │ │ │
│ ┌────────┐ │ │ (빈 공간) │
│ │ pod-a │ │ │ │
│ │pri:10 │ │ │ │
│ │anti- │ │ │ web-pod │
│ │affinity│──┼─────┼─▶ 못 들어감 │
│ │to web │ │ │ │
│ └────────┘ │ │ │
└──────────────┘ └──────────────┘
zone-a
pod-a를 쫓아내면 해결되는데, 스케줄러는 Node 1을 건드리지 않음

⚠️ 제한사항 3: nominatedNodeName Race

Timeline
────────────────────────────────────────────────────────────▶
T1: Pod P 스케줄링 실패, preemption 시작
T2: │ 스케줄러가 Node N 선택, nominatedNodeName=N 설정 요청
T3: │ 그 사이 Pod Q가 생성됨
│ │
T4: │ └─▶ Pod Q가 Node N에 스케줄링됨 (자리 있었으니까)
T5: └─▶ Pod P의 nominatedNodeName=N 설정 완료
하지만 이미 자리 없음!
결과: Node N은 P를 위해 "예약"됐다고 생각해서
다른 작은 Pod들도 안 들어감 → 리소스 낭비

⚠️ 제한사항 4: Graceful Termination 중 끼어들기

victim Pod 종료 대기 중 (기본 30초) 다른 Pod가 끼어듦

T1: Pod P (pri:100) preemption 시작, victim 종료 요청
T2: │ victim 종료 중... (30초 대기)
T3: │ Pod Q (pri:200) 생성됨
T4: │ victim 종료 완료, 자리 생김
│ │
T5: │ └─▶ Q가 먼저 스케줄링됨 (더 높은 우선순위)
T6: └─▶ P는 다시 Pending 상태
완화책: 낮은 우선순위 Pod에 terminationGracePeriodSeconds: 0

⚠️ 제한사항 5: PDB는 Best-Effort

PodDisruptionBudget을 존중하려고 노력하지만 보장 아님

Race Condition 시나리오:

PDB: minAvailable: 2
현재 Pod: 3개
┌─────────────┐ ┌─────────────┐
│ Scheduler A │ │ Scheduler B │ (HA 구성)
│ │ │ │
│ "1개 evict │ │ "1개 evict │
│ 가능하네" │ │ 가능하네" │
│ │ │ │ │ │
│ ▼ │ │ ▼ │
│ evict 1개 │ │ evict 1개 │
└─────────────┘ └─────────────┘
결과: Pod 1개만 남음 (minAvailable: 2 위반!)

ref: kubernetes/kubernetes#91492


⚠️ 제한사항 6: Priority Inflation

모든 Pod가 높은 우선순위면 preemption 무의미

해결책: ResourceQuota로 제한

apiVersion: v1
kind: ResourceQuota
metadata:
name: high-priority-quota
spec:
hard:
pods: "10" # high-priority Pod는 최대 10개
scopeSelector:
matchExpressions:
- operator: In
scopeName: PriorityClass
values: ["high-priority"]

5. 성능 튜닝


percentageOfNodesToScore

모든 노드를 스코어링하면 느림 → 일정 비율만 확인

┌──────────────────────────────────────────┐
│ 클러스터 크기 │ 기본값 │
├──────────────────────────────────────────┤
│ 100 노드 │ 50% │
│ 500 노드 │ 20% │
│ 5000+ 노드 │ 10% │
│ 최소값 │ 5% (하드코딩) │
└──────────────────────────────────────────┘
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
percentageOfNodesToScore: 50

노드 순회 최적화

같은 노드만 선택되는 것 방지

// 시작 인덱스를 랜덤하게
startIndex := rand.Intn(len(nodes))
// round-robin으로 순회
for i := 0; i < numNodesToCheck; i++ {
idx := (startIndex + i) % len(nodes)
checkNode(nodes[idx])
}

50% 스코어링이어도 매번 다른 노드 집합 검사


6. 스케줄러의 한계


스케줄러가 모르는 것들

항목설명
NUMA 토폴로지kubelet의 Topology Manager가 처리. 스케줄러는 모름
GPU 세부 정보nvidia.com/gpu: 1만 앎. 어떤 GPU인지 모름
Ephemeral Storage요청량만 봄. 실제 사용량 모름
실시간 리소스캐시된 정보 사용. 수 초 지연 있음

결과: 스케줄링 성공 → kubelet Admit 실패 가능

Scheduler: "Node A에 배치!"
Kubelet: "NUMA 조건 안 맞는데요" → Pod 거부

정리

  1. 3단계 기본 구조: Filter → Score → Bind

  2. Scheduling Framework: 15개 extension point로 커스터마이징

  3. Queue 구조: ActiveQ → BackoffQ → Unschedulable Pool

  4. Preemption 주의사항:

    • nominatedNodeName race condition
    • PDB는 best-effort
    • Cross-node preemption 안 됨
  5. 성능: percentageOfNodesToScore로 튜닝

  6. 한계: NUMA, GPU 상세 정보는 스케줄러가 모름


Q&A


References