콘텐츠로 이동

7. 스케줄링

Pod을 어느 노드에 놓을 것인가

flowchart LR
    W["watch<br/>spec.nodeName 이 비어 있는 Pod"] --> F["Filtering<br/>놓을 수 없는 노드를 거른다"]
    F --> S["Scoring<br/>남은 노드에 점수를 매긴다"]
    S --> B["Binding<br/>spec.nodeName 을 채운다"]
    B --> KL["kubelet 이 가져가 실행"]
    F -->|"남은 노드가 0"| P["Pending<br/>Events 에 이유가 찍힌다"]

    classDef key fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
    classDef bad fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
    classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
    class F,S key
    class P bad
    class W,B,KL mute
  1. spec.nodeName비어 있는 Pod을 발견한다

  2. Filtering — 놓을 수 없는 노드를 전부 거른다

  3. Scoring — 남은 노드에 점수를 매긴다

  4. 최고점 노드로 bindingspec.nodeName을 채운다

Filtering에서 걸러지는 이유들

  • 노드에 남은 request 여유가 부족하다
  • taint가 있는데 Pod에 toleration이 없다
  • nodeSelector / nodeAffinity가 안 맞는다
  • 포트 충돌(hostPort), 볼륨의 노드 제약(topology)
  • 노드가 unschedulable(cordon 됨)이다

Pending의 원인은 대부분 이 목록 안에 있다. 그리고 describe pod의 Events가 어느 항목에서 걸렸는지 문장으로 알려준다.

방향이 반대인 두 축이 핵심이다.

flowchart LR
    POD["Pod"] -->|"nodeSelector · nodeAffinity<br/>내가 갈 노드를 고른다"| NODE["노드"]
    NODE -->|"taint<br/>내가 받을 Pod을 거부한다"| POD
    POD2["Pod"] -->|"podAffinity · antiAffinity<br/>다른 Pod 기준으로"| POD3["다른 Pod"]

    classDef pod fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
    classDef node fill:#fef3c7,stroke:#d97706,color:#78350f
    class POD,POD2,POD3 pod
    class NODE node
도구 관점 성격
nodeName Pod → 특정 노드 스케줄러를 건너뛴다
nodeSelector Pod → 노드 라벨 단순, 강제
nodeAffinity Pod → 노드 라벨 표현력 있음, 강제/선호 선택 가능
podAffinity / podAntiAffinity Pod → 다른 Pod 함께 / 떨어뜨려 놓기
taints / tolerations 노드 → Pod 노드가 밀어낸다
topologySpreadConstraints Pod → 분산 균등 분포

affinity는 Pod이 노드를 고르는 것, taint는 노드가 Pod을 거부하는 것. 둘은 함께 쓰여야 완성된다.

spec:
nodeName: node01
  • 스케줄러가 개입하지 않는다. kubelet이 바로 가져간다
  • taint도, 리소스 검사도 무시한다 — 자리가 없으면 그냥 실패한다
  • 실무에서는 거의 안 쓴다. 디버깅과 스태틱 Pod의 영역
Terminal window
kubectl label node node01 disktype=ssd
kubectl get nodes --show-labels
kubectl label node node01 disktype- # 삭제
spec:
nodeSelector:
disktype: ssd
  • 모든 조건이 AND로 결합된다
  • 정확히 일치만 가능하다. “이거 아니면 저거”를 표현할 수 없다
  • 맞는 노드가 없으면 Pending

모든 노드에 자동으로 붙는 라벨도 있다 — kubernetes.io/hostname, kubernetes.io/os, topology.kubernetes.io/zone, node-role.kubernetes.io/control-plane.

spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: disktype
operator: In
values: ["ssd", "nvme"]

Filtering 단계에서 작동한다. 조건에 맞는 노드가 하나도 없으면 Pod은 Pending이다.

flowchart LR
    R["requiredDuringScheduling…"] --> FIL["Filtering 에서 작동<br/>못 맞추면 Pending ❌"]
    P["preferredDuringScheduling…"] --> SCO["Scoring 에서 작동<br/>weight 만큼 점수 ✅"]
    I["…IgnoredDuringExecution"] --> EX["이미 뜬 Pod 은<br/>조건이 깨져도 안 쫓아낸다"]

    classDef bad fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
    classDef ok fill:#dcfce7,stroke:#16a34a,color:#14532d
    classDef warn fill:#fef3c7,stroke:#d97706,color:#78350f
    classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
    class FIL bad
    class SCO ok
    class EX warn
    class R,P,I mute

requiredDuringScheduling + IgnoredDuringExecution
= “배치할 때는 반드시, 실행 중에는 무시”

연산자 의미
In 값이 목록 안에 있다
NotIn 목록에 없다 (= 안티 어피니티)
Exists 키가 있기만 하면 된다 (values 없음)
DoesNotExist 키가 없어야 한다
Gt / Lt 숫자 비교 (nodeAffinity 전용)

AND / OR 구조

flowchart TB
    NST["nodeSelectorTerms"]
    NST -->|OR| T1["term 1"]
    NST -->|OR| T2["term 2"]
    T1 -->|AND| E1["matchExpression a"]
    T1 -->|AND| E2["matchExpression b"]
    T2 -->|AND| E3["matchExpression c"]

    classDef or fill:#fef3c7,stroke:#d97706,color:#78350f
    classDef and fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
    class T1,T2 or
    class E1,E2,E3 and
  • nodeSelectorTerms 의 항목들끼리는 OR
  • 한 term 안의 matchExpressions 끼리는 AND
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: web
topologyKey: kubernetes.io/hostname # ← 무엇을 "같은 곳"으로 볼 것인가

topologyKey가 무엇을 “같은 곳”으로 볼지 정한다. 이게 핵심이다.

flowchart TB
    subgraph Z1["zone ap-northeast-2a"]
      subgraph N1["node01"]
        W1["web"]
      end
      subgraph N2["node02"]
        W2["web"]
      end
    end
    subgraph Z2["zone ap-northeast-2c"]
      subgraph N3["node03"]
        W3["web"]
      end
    end

    K1["topologyKey<br/>kubernetes.io/hostname"] -.->|"노드마다 하나씩"| N1
    K2["topologyKey<br/>topology.kubernetes.io/zone"] -.->|"zone 마다 하나씩"| Z1

    classDef key fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
    classDef pod fill:#dcfce7,stroke:#16a34a,color:#14532d
    class K1,K2 key
    class W1,W2,W3 pod
  • kubernetes.io/hostname노드 단위로 (한 노드에 하나씩)
  • topology.kubernetes.io/zone가용영역 단위
  • podAffinity = 조건에 맞는 Pod 곁에, podAntiAffinity = 떨어뜨려

podAffinity는 계산 비용이 크다. 대규모 클러스터에서 스케줄링이 느려지는 원인이 되기도 한다.

Terminal window
kubectl taint node node01 key=value:NoSchedule
kubectl taint node node01 key=value:NoSchedule- # 제거 (끝에 하이픈)
kubectl taint node node01 gpu=true:NoSchedule
kubectl describe node node01 | grep -A3 Taints

effect 셋의 차이는 “새 Pod”과 “이미 있는 Pod”을 각각 어떻게 하느냐다.

flowchart LR
    T{"taint effect"}
    T -->|NoSchedule| A1["새 Pod: 배치 안 함 ❌"]
    T -->|NoSchedule| A2["기존 Pod: 그대로 둔다 ✅"]
    T -->|PreferNoSchedule| B1["새 Pod: 되도록 피함<br/>자리 없으면 배치 ⚠️"]
    T -->|PreferNoSchedule| B2["기존 Pod: 그대로 둔다 ✅"]
    T -->|NoExecute| C1["새 Pod: 배치 안 함 ❌"]
    T -->|NoExecute| C2["기존 Pod: 쫓아낸다 ❌"]

    classDef bad fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
    classDef ok fill:#dcfce7,stroke:#16a34a,color:#14532d
    classDef warn fill:#fef3c7,stroke:#d97706,color:#78350f
    class A1,C1,C2 bad
    class A2,B2 ok
    class B1 warn
effect 의미
NoSchedule 새 Pod을 배치하지 않는다. 이미 있는 것은 그대로
PreferNoSchedule 되도록 피한다. 자리가 없으면 배치한다
NoExecute 배치도 안 하고, 이미 있는 Pod도 쫓아낸다

**taint는 “노드에 붙이는 조건”**이고, **toleration은 “Pod이 그 조건을 견딜 수 있다는 선언”**이다. toleration이 있다고 그 노드에 가는 것은 아니다 — 갈 수 있게 될 뿐이다.

spec:
tolerations:
- key: "gpu"
operator: "Equal" # Equal | Exists
value: "true"
effect: "NoSchedule"
- key: "node.kubernetes.io/not-ready"
operator: "Exists"
effect: "NoExecute"
tolerationSeconds: 300 # 300초까지는 버틴다
- operator: "Exists" # key 생략 = 모든 taint를 견딘다
  • operator: Existsvalue를 쓰지 않는다
  • effect를 생략하면 모든 effect에 대해 적용된다
  • key까지 생략하고 Exists만 두면 전부 무시 — DaemonSet에서 쓰는 패턴
flowchart LR
    Q1{"노드에 taint 가 있나"}
    Q1 -->|없다| R1["아무 Pod 이나 올 수 있다"]
    Q1 -->|있다| Q2{"Pod 에 맞는 toleration 이 있나"}
    Q2 -->|없다| R2["이 노드에는 못 온다 ❌"]
    Q2 -->|있다| R3["올 수 있다<br/>단 온다는 보장은 없다 ⚠️"]
    R3 --> Q3{"유도하고 싶은가"}
    Q3 -->|"그렇다"| R4["nodeSelector · nodeAffinity 를<br/>같이 써야 그 노드로 간다 ✅"]

    classDef ok fill:#dcfce7,stroke:#16a34a,color:#14532d
    classDef bad fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
    classDef warn fill:#fef3c7,stroke:#d97706,color:#78350f
    classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
    class R1,R4 ok
    class R2 bad
    class R3 warn
    class Q1,Q2,Q3 mute
taint 언제
node.kubernetes.io/not-ready 노드가 Ready가 아닐 때 (NoExecute)
node.kubernetes.io/unreachable 노드와 통신이 안 될 때 (NoExecute)
node.kubernetes.io/memory-pressure 메모리 부족
node.kubernetes.io/disk-pressure 디스크 부족
node.kubernetes.io/unschedulable cordon 했을 때
node-role.kubernetes.io/control-plane 컨트롤 플레인 노드 (NoSchedule)

노드가 죽어도 Pod이 5분 동안 안 옮겨가는 이유가 여기 있다.

sequenceDiagram
    participant N as 노드
    participant CM as node 컨트롤러
    participant P as Pod
    N--xCM: 하트비트 중단
    CM->>N: not-ready / unreachable taint 부착 · NoExecute
    Note over P: 모든 Pod 에 자동 toleration<br/>tolerationSeconds 300
    P->>P: 300초 버틴다
    P--xN: 300초 후 축출
    Note over P: 상위 컨트롤러가 다른 노드에 새로 만든다
  • 모든 Pod에는 not-ready / unreachable에 대한 5분(300초) toleration이 자동으로 붙는다
  • 그래서 노드가 죽어도 5분 동안은 Pod이 안 옮겨간다. 이게 그 유명한 지연의 정체다
stateDiagram-v2
    [*] --> Ready: 정상
    Ready --> SchedulingDisabled: kubectl cordon
    SchedulingDisabled --> Ready: kubectl uncordon
    Ready --> Drained: kubectl drain
    Drained --> Ready: kubectl uncordon

    note right of SchedulingDisabled
      새 Pod 만 막는다.
      기존 Pod 은 그대로 돈다.
    end note

    note right of Drained
      cordon + 기존 Pod 축출.
      PDB 를 존중해 기다린다.
    end note
Terminal window
kubectl cordon node01 # 새 Pod 배치 금지 (기존은 유지)
kubectl uncordon node01 # 해제
kubectl drain node01 \
--ignore-daemonsets \ # DaemonSet Pod은 어차피 못 옮기니 무시
--delete-emptydir-data \ # emptyDir을 쓰는 Pod도 지운다
--force # 컨트롤러 없는 Pod(단독 Pod)도 지운다
  • cordon = 노드에 unschedulable 표시 (taint가 붙는다)
  • drain = cordon + 기존 Pod을 전부 축출(evict)
  • 축출은 PDB를 존중한다 — 위반하면 기다린다 (5장)

topologySpreadConstraints — 균등 분산

섹션 제목: “topologySpreadConstraints — 균등 분산”
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule # 또는 ScheduleAnyway
labelSelector:
matchLabels:
app: web

maxSkew는 도메인 간 개수 차이의 허용치다.

flowchart LR
    subgraph OK["maxSkew 1 · 허용 ✅"]
      A1["zone A<br/>Pod 3개"]
      A2["zone B<br/>Pod 2개"]
      A3["차이 1"]
    end
    subgraph NG["maxSkew 1 · 위반 ❌"]
      B1["zone A<br/>Pod 3개"]
      B2["zone B<br/>Pod 1개"]
      B3["차이 2 → DoNotSchedule 이면 Pending"]
    end

    classDef ok fill:#dcfce7,stroke:#16a34a,color:#14532d
    classDef bad fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
    class A1,A2,A3 ok
    class B1,B2,B3 bad
  • maxSkew: 1 이면 zone A에 3개일 때 zone B는 최소 2개여야 한다
  • whenUnsatisfiable: DoNotSchedule = 강제, ScheduleAnyway = 선호

podAntiAffinity보다 표현이 정확하고 계산이 싸다. “노드/존에 고르게 퍼뜨려라”가 목적이라면 이쪽이 정답이다.

apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: high-priority
value: 1000000
globalDefault: false
preemptionPolicy: PreemptLowerPriority # 또는 Never
description: "중요한 워크로드용"
spec:
priorityClassName: high-priority
sequenceDiagram
    participant H as 높은 우선순위 Pod
    participant S as scheduler
    participant N as 노드 · 자리 없음
    participant L as 낮은 우선순위 Pod
    H->>S: 스케줄 요청
    S->>N: Filtering — 맞는 노드가 없다
    S->>L: 축출 (preempt)
    Note over L: describe 에 Preempted by … 기록
    S->>H: 확보된 자리에 binding
    L->>S: 다시 스케줄링 시도 (다른 노드일 수 있다)
  • 자리가 없으면 스케줄러가 낮은 우선순위 Pod을 쫓아내고 자리를 만든다
  • 쫓겨난 Pod은 다시 스케줄링을 시도한다 (다른 노드로 갈 수도 있다)
  • preemptionPolicy: Never — 우선순위는 높지만 남을 쫓아내지는 않는다
  • 내장 클래스: system-cluster-critical, system-node-critical

축출된 Pod은 describePreempted by ... 로 기록된다.

spec:
schedulerName: my-scheduler
  • 기본값은 default-scheduler
  • 커스텀 스케줄러를 Pod으로 띄우고 이름을 지정하면 그 스케줄러가 담당한다
  • 지정한 이름의 스케줄러가 없으면 Pod은 영원히 Pending (아무도 안 집어간다)
Terminal window
kubectl get pods -n kube-system -l component=kube-scheduler
kubectl get events | grep -i schedul

커리큘럼의 “Pod admission” 항목. 네임스페이스 라벨로 켠다.

Terminal window
kubectl label ns dev \
pod-security.kubernetes.io/enforce=baseline \
pod-security.kubernetes.io/enforce-version=latest \
pod-security.kubernetes.io/warn=restricted

레벨(얼마나 조이나) × 모드(어기면 어떻게 하나)의 조합이다.

privileged

제한 없음.

baseline

알려진 권한 상승을 막는다. hostNetwork, privileged 등 금지.

restricted

강하게 제한. runAsNonRoot, capabilities drop ALL 등 요구.

모드 동작
enforce 거부한다
audit 감사 로그에 남긴다
warn 사용자에게 경고를 보여준다

PodSecurityPolicy(PSP)는 v1.25에서 제거되었다. 옛 자료에 나오면 무시할 것.

Terminal window
kubectl get pods -o wide # Pending인 것 확인
kubectl describe pod web # ★ Events 를 읽는다
kubectl get events --sort-by=.lastTimestamp
kubectl describe node node01 # Taints / Allocated resources
kubectl get nodes # SchedulingDisabled 표시 확인

Events 메시지로 원인이 거의 확정된다.

flowchart LR
    E{"describe pod → Events"}
    E -->|"Insufficient cpu / memory"| R1["request 여유 부족<br/>describe node 의 Allocated resources"]
    E -->|"had untolerated taint"| R2["toleration 없음<br/>describe node 의 Taints"]
    E -->|"didn't match node affinity/selector"| R3["라벨 불일치<br/>get nodes --show-labels"]
    E -->|"were unschedulable"| R4["cordon 되어 있다<br/>kubectl uncordon"]
    E -->|"unbound immediate PersistentVolumeClaims"| R5["PVC 가 Bound 안 됨 · 13장"]
    E -->|"메시지가 없다"| R6["스케줄러가 죽었거나<br/>schedulerName 오타"]

    classDef norm fill:#fef3c7,stroke:#d97706,color:#78350f
    classDef alarm fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
    class R1,R2,R3,R4,R5 norm
    class R6 alarm
메시지 원인
Insufficient cpu / Insufficient memory request 여유 부족
node(s) had untolerated taint {...} toleration 없음
node(s) didn't match Pod's node affinity/selector 라벨 불일치
node(s) were unschedulable cordon 되어 있다
pod has unbound immediate PersistentVolumeClaims PVC가 Bound 안 됨 (13장)
(메시지 없음) 스케줄러가 죽었거나 schedulerName 오타
  • 스케줄러는 Filtering → Scoring → binding(nodeName 채우기)
  • affinity는 Pod이 노드를 고르고, taint는 노드가 Pod을 밀어낸다. 방향이 반대다
  • required…IgnoredDuringExecution = “배치할 때만 강제, 실행 중엔 무시”
  • toleration은 “갈 수 있게” 할 뿐 “가게” 하지 않는다 — 유도하려면 affinity를 같이
  • 노드 장애 시 Pod 이동이 5분 걸리는 이유는 자동 toleration 300초
  • drain--ignore-daemonsets 필수, PDB를 존중해 기다린다
  • 분산이 목적이면 topologySpreadConstraints (antiAffinity보다 정확·저렴)
  • Pending 진단은 describe pod의 Events 한 줄로 끝나는 경우가 대부분