본문으로 건너뛰기
8.1 스케줄링 동작 원리

8.1 스케줄링 동작 원리

새로 생성된 Pod의 spec.nodeName은 비어 있다. kube-scheduler는 API 서버를 감시하다가 노드가 정해지지 않은 Pod를 발견하면 클러스터의 노드 가운데 하나를 골라 이 빈칸을 채우는 control plane 구성요소다. 노드가 기록되면 그다음은 해당 노드의 kubelet이 이어받아 컨테이너를 기동한다. 이 장은 스케줄러가 노드를 고르는 파이프라인, 그 파이프라인을 확장하는 구조, 그리고 자리를 얻지 못한 Pod를 읽는 방법을 다룬다.

큐에서 binding까지

스케줄링을 기다리는 Pod는 스케줄링 큐에 쌓인다. 큐는 기본적으로 Pod priority가 높은 순으로 정렬되고, 스케줄러는 큐에서 Pod를 하나씩 꺼내 노드 선택을 진행한다. 선택은 거르는 단계와 고르는 단계로 나뉜다.

첫 단계인 filtering은 조건에 미달하는 노드를 전부 탈락시킨다. 할당 가능량(allocatable)이 Pod의 requests를 수용하지 못하는 노드, nodeSelector나 required nodeAffinity 조건에 맞지 않는 노드, Pod가 toleration으로 허용하지 않은 taint를 가진 노드, hostPort가 충돌하는 노드, 요구한 볼륨을 연결하지 못하는 노드가 여기서 걸러진다. 검사 항목 하나하나가 이 Part의 뒤 장에서 다루는 requests, affinity, taint와 직접 연결된다. 살아남은 노드가 하나도 없으면 Pod는 Pending 상태로 큐에 돌아가고, 노드 증설이나 다른 Pod의 종료로 클러스터 상태가 바뀌면 재시도된다.

다음 단계인 scoring은 filtering을 통과한 노드들의 순위를 정한다. 플러그인마다 노드에 점수를 매기고 가중치를 곱해 합산한다. 자원 사용의 균형이 좋은 노드, 컨테이너 이미지를 이미 내려받아 둔 노드, preferred affinity 조건에 맞는 노드가 높은 점수를 받는다. 최고점 노드가 선택되고, 동점이면 그중 무작위로 하나를 고른다.

마지막 binding은 선택 결과를 API 서버에 기록하는 단계다. 기록이 끝나면 해당 노드의 kubelet이 이를 감지해 컨테이너 기동을 시작한다. 스케줄러의 일은 여기까지다. 컨테이너가 실제로 기동에 성공하는지, 기동 후 정상인지는 kubelet과 probe의 영역이다.

    flowchart TD
  Q["스케줄링 큐"] --> F["filtering<br/>조건 미달 노드 탈락"]
  F -->|"후보 없음"| P["Pending 유지"]
  P --> Q
  F -->|"후보 1개 이상"| S["scoring<br/>남은 노드 점수화"]
  S --> B["binding<br/>nodeName 기록"]
  B --> K["kubelet이 컨테이너 기동"]
  

스케줄링 프레임워크와 확장 지점

이 파이프라인은 scheduling framework라는 플러그인 구조 위에 구현되어 있다. 큐 정렬(QueueSort), filtering 전후(PreFilter, Filter, PostFilter), scoring 전후(PreScore, Score), 자원 선점 기록(Reserve), 배치 승인 대기(Permit), binding 전후(PreBind, Bind, PostBind)까지 단계마다 확장 지점(extension point)이 정의되어 있고, 각 지점에 플러그인이 끼워지는 방식이다. 기본 스케줄러 자체도 NodeResourcesFit, TaintToleration, InterPodAffinity 같은 내장 플러그인의 조합으로 동작한다. filtering에서 후보가 전무할 때 실행되는 PostFilter 지점에는 낮은 우선순위 Pod를 축출해 자리를 만드는 preemption이 구현되어 있다.

대부분의 배치 요구는 내장 플러그인과 Pod 쪽 선언(affinity, toleration)으로 해결된다. 조직 고유의 배치 정책이 필요할 때 플러그인을 작성해 KubeSchedulerConfiguration의 profile로 구성하는 확장 경로가 있다는 정도로 알아 두면 충분하다.

스케줄러를 우회하는 nodeName 직접 지정

spec.nodeName에 노드 이름을 직접 적으면 스케줄러는 이 Pod를 처리하지 않는다. filtering도 scoring도 없이 지정된 노드의 kubelet이 바로 Pod를 받는다. 자원이 부족하거나 taint가 있어도 스케줄러 수준의 검사가 전혀 작동하지 않으며, kubelet의 자체 검증에서 뒤늦게 거부되면 Pod는 OutOfcpu, OutOfmemory 같은 상태로 실패한 채 끝난다. 다른 노드로의 재시도는 없다. 지정한 이름의 노드가 존재하지 않으면 Pod는 실행되지 못한 채 남는다.

특정 노드에서 실행하려는 요구는 nodeName이 아니라 nodeSelector나 nodeAffinity로 표현한다. 조건 선언은 스케줄러의 검사를 통과한 뒤 배치되므로 안전장치가 유지된다. nodeName 직접 지정이 정당한 자리는 스케줄러 자체를 우회해야 하는 디버깅 상황 정도다.

Pending에 머무는 Pod 읽는 법

배치할 노드를 찾지 못한 Pod는 Pending 상태로 남는다. 원인은 Pod의 Events에 FailedScheduling으로 기록되므로 kubectl describe를 실행해 확인한다.

kubectl describe pod web-7c9d8f6b5-x2k4j
Events:
  Type     Reason            Message
  ----     ------            -------
  Warning  FailedScheduling  0/5 nodes are available: 3 Insufficient cpu,
                             2 node(s) had untolerated taint {dedicated: db}.

메시지는 filtering에서 노드들이 탈락한 이유를 원인별 개수로 요약한다. 위 예시는 다섯 노드 중 셋이 CPU requests를 수용하지 못했고, 둘은 Pod가 허용하지 않은 taint 때문에 탈락했다는 뜻이다. 원인별로 진단 방향이 갈린다. Insufficient cpu/memory면 requests 조정이나 노드 증설을, untolerated taint면 toleration 추가 여부를, didn't match Pod's node affinity/selector면 노드 label과 선언한 조건의 대조를 검토한다.

Pending이 오래 지속되면 이벤트가 만료되어 사라지기도 한다. 이때는 Pod를 지웠다 다시 만들기보다 kubectl get events --field-selector reason=FailedScheduling으로 namespace 전체의 스케줄링 실패를 모아 보는 편이 빠르다.

정리

  • 스케줄러는 큐에서 꺼낸 Pod마다 filtering으로 후보 노드를 거르고, scoring으로 순위를 정한 뒤, binding으로 nodeName을 기록한다.
  • spec.nodeName 직접 지정은 이 파이프라인 전체를 우회하므로 filtering이 제공하는 안전장치가 사라진다.
  • Pending Pod의 원인은 kubectl describe pod의 FailedScheduling 이벤트에 원인별 노드 수로 남는다.