본문으로 건너뛰기
2.4 스케줄러와 컨트롤러 매니저

2.4 스케줄러와 컨트롤러 매니저

2.1의 hub-and-spoke 그림에서 결정을 담당하는 컴포넌트가 둘 있었다. 새 Pod를 어느 노드에서 실행할지 정하는 kube-scheduler, 그리고 선언된 상태와 현재 상태를 계속 일치시키는 kube-controller-manager다. 둘 다 노드에 직접 지시를 보내지 않는다. apiserver를 감시하다가 자기 몫의 결정을 내리고, 그 결과를 다시 apiserver에 기록할 뿐이다. 이 챕터는 두 프로세스의 동작을 확인하고, Kubernetes 전체를 관통하는 reconciliation loop 패턴과 control plane의 leader election을 함께 정리한다.

kube-scheduler: 비어 있는 nodeName을 채운다

모든 Pod에는 spec.nodeName 필드가 있다. 이 필드가 채워져 있으면 해당 노드의 kubelet이 Pod를 실행하고, 비어 있으면 아무 노드도 그 Pod를 자기 몫으로 여기지 않는다. kube-scheduler의 일은 여기서 정의된다. nodeName이 비어 있는 Pod를 watch로 감시하다가, 적합한 노드 하나를 선택해 그 필드를 채우는 것이다.

노드 선택은 두 단계로 진행된다. 필터링 단계는 그 Pod를 실행할 수 없는 노드를 제외한다. CPU와 메모리 requests를 수용할 자원이 없는 노드, Pod가 toleration을 갖추지 못한 taint가 걸린 노드, nodeSelector나 affinity 조건에 맞지 않는 노드가 걸러진다. 스코어링 단계는 남은 노드들에 점수를 매긴다. 여유 자원이 많은 노드, 컨테이너 이미지를 이미 내려받아 둔 노드, 같은 워크로드의 Pod가 한곳에 몰리지 않는 배치가 높은 점수를 받는다. 최고점 노드가 정해지면 스케줄러는 apiserver에 바인딩을 기록한다. Pod의 nodeName이 채워지는 것이 결정의 전부이고, 컨테이너를 실제로 시작하는 일은 그 노드의 kubelet에게 넘어간다.

필터링을 통과하는 노드가 하나도 없으면 Pod는 Pending 상태로 남는다. 스케줄러는 클러스터 상태가 바뀔 때마다 재시도하므로, 노드가 추가되거나 다른 Pod가 종료되어 자원이 확보되면 그때 배정된다. requests와 limits, affinity, taint 같은 배치 조건의 상세는 Part VIII. 스케줄링과 자원 관리에서 다룬다.

kube-controller-manager 안에 동거하는 컨트롤러들

kube-controller-manager는 수십 개의 컨트롤러를 하나의 바이너리로 묶어 실행하는 프로세스다. 각 컨트롤러는 특정 리소스 하나를 책임지는 독립된 제어 루프이며, 자주 만나는 것만 추리면 다음과 같다.

컨트롤러감시 대상담당
Deployment controllerDeployment배포 전략에 맞게 ReplicaSet 생성과 조정
ReplicaSet controllerReplicaSetreplicas 수만큼 Pod 존재 보장
Node controllerNode노드 heartbeat 감시, 무응답 노드 표시
Job controllerJob완료까지 Pod 실행, 성공 횟수 추적
EndpointSlice controllerService, PodService 백엔드 Pod 목록 갱신

한 바이너리에 묶은 것은 운영 편의 때문이다. 수십 개의 데몬을 각각 배포하고 감시하는 대신 하나만 관리한다. 논리적으로는 완전히 독립이라 Deployment controller는 ReplicaSet controller의 존재를 모른다. Deployment를 만들면 Deployment controller가 ReplicaSet을 만들어 apiserver에 기록하고, 그 기록을 본 ReplicaSet controller가 Pod를 만든다. 같은 프로세스 안에서도 협력은 apiserver에 저장된 오브젝트를 매개로 일어난다. 각 워크로드 리소스의 상세 동작은 Part IV. 워크로드에서 다룬다.

reconciliation loop: 관찰, 비교, 조치

컨트롤러 수십 개의 코드는 제각각이지만 동작의 골격은 하나로 수렴한다. 현재 상태를 관찰하고, 원하는 상태와 비교하고, 차이가 있으면 줄이는 조치를 실행한 뒤 다시 관찰로 돌아간다.

    flowchart TD
  W["관찰: 상태 수신"] --> C{"원하는 상태와 일치?"}
  C -->|"일치"| W
  C -->|"불일치"| A["조치: 생성/수정/삭제"]
  A --> W
  

이 루프는 이벤트가 아니라 상태를 기준으로 동작한다. “Pod가 삭제되었다"는 사건에 반응하는 것처럼 보여도, 실제 판단 기준은 “replicas는 3인데 Pod가 2개다"라는 상태의 차이다. 사건 기반이라면 컨트롤러가 재시작하는 동안 발생한 사건을 놓친 만큼 상태가 어긋난 채 남지만, 상태 기반에서는 다음 비교에서 차이가 다시 드러나므로 놓친 사건이 문제로 남지 않는다. watch는 반응 속도를 높이는 최적화이고, 정확성은 상태 비교의 반복이 담보한다. 이 패턴은 controller-manager 안의 컨트롤러만이 아니라 kubelet, kube-proxy, 사용자가 작성하는 커스텀 컨트롤러까지 Kubernetes의 능동 컴포넌트 전부가 공유한다.

control plane의 leader election

apiserver는 무상태라 여러 대가 동시에 요청을 처리해도 문제가 없지만, 스케줄러와 controller-manager는 사정이 다르다. 두 인스턴스가 동시에 같은 Pod를 배정하거나 같은 ReplicaSet을 조정하면 충돌한다. 그래서 이 둘은 여러 대를 실행하더라도 실제로 일하는 인스턴스는 하나뿐인 active-standby로 동작한다.

리더를 정하는 방법 역시 Kubernetes답게 apiserver의 오브젝트를 이용한다. kube-system 네임스페이스의 Lease 오브젝트가 잠금 역할이다. 각 인스턴스는 기동하면서 Lease 획득을 시도하고, 획득한 리더는 주기적으로 갱신 시각을 기록해 생존을 알린다. 리더가 죽어 갱신이 끊기면 대기하던 인스턴스가 만료된 Lease를 넘겨받아 새 리더가 된다.

kubectl get leases -n kube-system

HOLDER 컬럼에서 kube-scheduler와 kube-controller-manager 각각의 현재 리더 인스턴스를 확인한다. control plane 노드를 3대로 늘려도 스케줄링 처리량이 3배가 되지 않는 이유가 여기에 있다. 늘어나는 것은 리더 상실 시 이어받을 후보의 수, 곧 가용성이다.

정리

스케줄러는 nodeName이 빈 Pod에 노드를 배정해 기록할 뿐이고, 실행은 kubelet의 몫이다. controller-manager는 관찰, 비교, 조치를 반복하는 컨트롤러 수십 개를 하나의 바이너리로 묶은 것이며, 이 reconciliation loop가 Kubernetes 능동 컴포넌트 전체의 공통 형태다. 둘 다 여러 대를 두면 Lease 기반 leader election으로 한 인스턴스만 활동한다.