요즘IT
위시켓
AIDP
콘텐츠프로덕트 밸리
요즘 작가들컬렉션물어봐
놀이터
콘텐츠
프로덕트 밸리
요즘 작가들
컬렉션
물어봐
놀이터
새로 나온
인기
개발
AI
IT서비스
기획
디자인
비즈니스
프로덕트
커리어
트렌드
스타트업
서비스 전체보기
위시켓요즘ITAIDP
고객 문의
02-6925-4867
10:00-18:00주말·공휴일 제외
yozm_help@wishket.com
요즘IT
요즘IT 소개작가 지원
기타 문의
콘텐츠 제안하기광고 상품 보기
요즘IT 슬랙봇크롬 확장 프로그램
이용약관
개인정보 처리방침
청소년보호정책
㈜위시켓
대표이사 : 박우범
서울특별시 강남구 테헤란로 211 3층 ㈜위시켓
사업자등록번호 : 209-81-57303
통신판매업신고 : 제2018-서울강남-02337 호
직업정보제공사업 신고번호 : J1200020180019
제호 : 요즘IT
발행인 : 박우범
편집인 : 노희선
청소년보호책임자 : 박우범
인터넷신문등록번호 : 서울,아54129
등록일 : 2022년 01월 23일
발행일 : 2021년 01월 10일
© 2013 Wishket Corp.
로그인
요즘IT 소개
콘텐츠 제안하기
광고 상품 보기
개발

코드 레벨로 쿠버네티스 스케줄러 동작 뜯어보기

조훈(Hoon Jo)
22분
2시간 전
131
에디터가 직접 고른 실무 인사이트 매주 목요일에 만나요.
newsletter_profile0명 뉴스레터 구독 중

코드로 살펴보는 쿠버네티스 스케줄러의 효율적인 동작법

 

쿠버네티스를 사용하다 보면, 쿠버네티스의 워크로드(Workload)가 어느 워커 노드에서 동작하는지에 자연스럽게 관심을 가지게 됩니다. 그러다 보면 Affinity와 이어지는 다양한 스펙을 접하고, 이를 실제 환경에서 활용합니다. 다만, 그럴 때면 Taint처럼 특정 노드를 사용하지 못하도록 제한하는 스펙을 만나기도 합니다.

 

원래대로라면, 파드(Pod) 입장에서 Affinity는 어디로든 갈 수 있는, 즉 ‘뚫을 수 있는’ 창의 역할을 합니다. 한편 Taint는 그러한 파드가 접근하지 못하도록 막는 방패의 역할을 합니다.

 

4컷 만화로 표현한 Affinity와 Taint의 관계 <출처: 작가>

 

이처럼 서로 다른 특성이 충돌하는 경우, 과연 어떤 것이 우선해 동작할까요? 이는 쿠버네티스 스케줄러와 가장 밀접한 연관이 있습니다.

 

나아가 이처럼 스케줄러가 제공하는 다양한 조건을 기반으로 배포 전략을 세밀하게 설계할 수 있다는 장점은, 쿠버네티스가 SRE/DevOps 환경에서 많은 사랑을 받는 이유이기도 합니다. 그래서 이번 글에서는 쿠버네티스 스케줄러의 구체적인 동작을 코드 레벨에서 살펴보려고 합니다.

 

들어가기에 앞서: 스케줄러 중심으로 보는 쿠버네티스

쿠버네티스 스케줄러에 관한 내용을 살펴보기에 앞서, 먼저 쿠버네티스 구성 요소를 정리하면 아래 그림과 같습니다.

 

<출처: kubernetes.io>

 

이처럼 다양한 쿠버네티스 구성 요소를, 다시 쿠버네티스 스케줄러(scheduler) 중심으로 그리면 이렇습니다.

 

<출처: 작가>

 

가장 먼저 보이는 Stage0은 스케줄러로 진입하기 전, 쿠버네티스 API 서버 수준에서 처리되는 단계입니다.* 반면 Stage4는 쿠버네티스 노드에 워크로드가 선언된 이후의 단계입니다. 이 시점에도 일부 스케줄러가 쓰이긴 하지만, 전적으로 관여하는 것은 아닙니다. 이 두 단계는 스케줄러의 범위를 넘어서는 영역이므로, 여기서는 다루지 않겠습니다.

*관련 내용은 이전 글, CEL만 알면 나도 쿠버네티스 보안 전문가!에서 다룬 바 있습니다.

 

따라서, 글에서는 Stage 1, 2, 3에 집중하여 쿠버네티스 스케줄러가 얼마나 아름답게 동작하는지 살펴볼 예정입니다. 참고로 Stage1과 Stage2는 코드 수준에서는 같은 플러그인 형태를 가지고 있습니다. 다만 동작을 구분해 설명하는 편이 이해에 도움이 되기 때문에, 여기서는 임의로 분리해 설명하겠습니다.


실습 환경 구성 

실습 환경을 위해 필요한 베이그런트, 버추얼박스 그리고 타비(Optional) 구성은 마찬가지 이전 글, 새로 산 맥북으로 쿠버네티스 클러스터 구축하기를 참고하면 좋습니다. 이러한 기본 환경이 구축되어 있다는 가정 아래 실험을 진행합니다.

 

1. 베이그런트 파일 외에 소스들이 있는 talk 깃허브 저장소를 클론 합니다.

 

$ git clone https://github.com/sysnet4admin/talks.git
Cloning into 'talks'...
remote: Enumerating objects: 1004, done.
remote: Counting objects: 100% (10/10), done.
remote: Compressing objects: 100% (8/8), done.
remote: Total 1004 (delta 2), reused 7 (delta 2), pack-reused 994 (from 1)
Receiving objects: 100% (1004/1004), 12.03 MiB | 9.06 MiB/s, done.
Resolving deltas: 100% (481/481), done.

 

2. talks/KubeCon/2025-NA/DEMO/k8s-cluster-builder 디렉터리로 이동해 ls -1명령어로 파일 목록을 확인합니다.

 

$ cd talks/KubeCon/2025-NA/DEMO/k8s-cluster-builder
$ ls -1
controlplane_node.sh
extra_k8s_pkgs.sh
k8s_env_build.sh
k8s_pkg_cfg.sh
README.md
Vagrantfile
worker_nodes.sh

 

3. vagrant up을 실행해 1개의 컨트롤 플레인 노드와 6개의 워커 노드를 구성합니다.

 

$ vagrant up
Bringing machine 'cp-k8s-1.34.1' up with 'virtualbox' provider...
Bringing machine 'w1-k8s-1.34.1' up with 'virtualbox' provider...
Bringing machine 'w2-k8s-1.34.1' up with 'virtualbox' provider...
Bringing machine 'w3-k8s-1.34.1' up with 'virtualbox' provider...
Bringing machine 'w4-k8s-1.34.1' up with 'virtualbox' provider...
Bringing machine 'w5-k8s-1.34.1' up with 'virtualbox' provider...
Bringing machine 'w6-k8s-1.34.1' up with 'virtualbox' provider...
==> cp-k8s-1.34.1: Importing base box 'sysnet4admin/Ubuntu-k8s'...
==> cp-k8s-1.34.1: Matching MAC address for NAT networking...
==> cp-k8s-1.34.1: Checking if box 'sysnet4admin/Ubuntu-k8s' version '0.8.6' is up to date...
==> cp-k8s-1.34.1: Setting the name of the VM: cp-k8s-1.34.1(github_SysNet4Admin)

[ 생략 ]

 

4. 터미널 도구에서 아래 정보를 입력합니다. 저는 터미널 도구로 타비(Tabby)를 주로 쓰지만, 기본 터미널 프로그램에서 진행해도 크게 문제 없습니다.

 

  • IP: 127.0.0.1
  • Port: 60010
  • ID: root
  • Password: vagrant

 

접속을 마쳤다면, 새로 구성된 가상머신인 cp-k8s에 접속한 게 맞는지 hostname으로 확인합니다. 이어 k get nodes명령어로 쿠버네티스 클러스터도 잘 구성되어 있는지 확인합니다.

 

$ ssh root@127.0.0.1 -p 60010
root@127.0.0.1's password: < vagrant >
root@cp-k8s:~# id
uid=0(root) gid=0(root) groups=0(root)
root@cp-k8s:~# hostname
cp-k8s
root@cp-k8s:~# k get nodes 
NAME     STATUS   ROLES           AGE     VERSION
cp-k8s   Ready    control-plane   6m30s   v1.34.1
w1-k8s   Ready    <none>          5m23s   v1.34.1
w2-k8s   Ready    <none>          4m22s   v1.34.1
w3-k8s   Ready    <none>          3m32s   v1.34.1
w4-k8s   Ready    <none>          2m41s   v1.34.1
w5-k8s   Ready    <none>          101s    v1.34.1
w6-k8s   Ready    <none>          46s     v1.34.1

 

5. 편의를 위해 사전에 구성한 스크립트 kg-nodes-labels와 kg-nodes-taints 명령어를 입력해 아래와 같이 나오는지 확인합니다. 각 노드에 사전에 입력된 레이블과 테인트 정보를 확인한 것으로, 원활한 실습을 위한 구성입니다.

 

root@cp-k8s:~# kg-nodes-labels
NAME     STATUS   ROLES    AGE     VERSION   ZONE     DISKTYPE
cp-k8s   Ready    <none>   9m45s   v1.34.1
w1-k8s   Ready    <none>   8m38s   v1.34.1   zone-a   ssd
w2-k8s   Ready    <none>   7m37s   v1.34.1   zone-a   hdd
w3-k8s   Ready    <none>   6m47s   v1.34.1   zone-b   ssd
w4-k8s   Ready    <none>   5m56s   v1.34.1   zone-b   hdd
w5-k8s   Ready    <none>   4m56s   v1.34.1   zone-c   ssd
w6-k8s   Ready    <none>   4m1s    v1.34.1   zone-c   hdd
root@cp-k8s:~# kg-nodes-taints
NAME     TAINTS
cp-k8s   [map[effect:NoSchedule key:node-role.kubernetes.io/control-plane]]
w1-k8s   <none>
w2-k8s   <none>
w3-k8s   <none>
w4-k8s   <none>
w5-k8s   [map[effect:NoSchedule key:gpu value:nvidia]]
w6-k8s   [map[effect:PreferNoSchedule key:maintenance value:true]]

 

6. 마지막으로 실습에서 사용할 fzf를 설치합니다. 없어도 동작은 하지만, 좀 더 편리하게 사용할 수 있습니다.

 

$ root@cp-k8s:~# apt-get install fzf
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
The following NEW packages will be installed:
  fzf
0 upgraded, 1 newly installed, 0 to remove and 89 not upgraded.

 [ 생략 ]

 

현재 구성을 정리한 표입니다. 이렇게 사전 입력한 정보 기반으로 단계별 실습을 진행할 예정입니다.

 

 

 

Stage1: 노드네임(nodeName)

 

처음으로 살펴볼 Stage 1, 노드네임을 처리하는 단계의 주요 코드입니다.

 

<코드 출처: kubernetes GitHub>

 

복잡하고 어려워 보이지만, 실제로는 매우 간단합니다. 우선 주석 부분부터 확인해 보면 되는데요.

 

// Filter invoked at the filter extension point.
// 필터 확장 지점에서 호출된 필터

 

이 주석의 의미는 실제로 nodeName이라는 기능을 쓰기 위해, Filter를 사용했다는 뜻입니다. 이어지는 코드는 사전 체크를 담당합니다.

 

 

풀어 말하면, Pod를 배포하기 위해 노드 정보에 맞는(Fits) 노드가 있나요? 만약(if 조건) 없다면, “UnschedulableAndUnresolvable“ 상태로 반환(return)해 주세요. 라는 단순한 내용을 담고 있습니다.

 

이어지는 다음 코드로 시스템은 Fits한 노드가 있다면, 해당 노드의 정보를 반환(return)해 줍니다.

 

 

실습으로 확인하기

실습 환경으로 돌아와 01-pass-nodename-direct.yaml 파일의 노드네임이 어떻게 해석이 되는지 확인해 보겠습니다. 앞서 코드로 살펴본 내용이 실제 환경에서는 어떻게 동작하는지 직접 검증해 보는 과정입니다.

 

 

이 예제를 보면 w5-k8s라는 nodeName이 어떻게 처리되는지 조금 더 쉽게 이해할 수 있습니다. 핵심은 nodeName이 지정되면, 스케줄러를 거치지 않고 해당 노드로 바로 배치된다는 점입니다.

 

 

이제 nodeName이 포함된 실제 코드를 적용해, taint가 존재하는 w5-k8s에도 파드가 어사인되는지 확인해 보겠습니다. 즉, “taint가 있어도 정말 배치될까?”를 검증하는 단계입니다.

 

1. ~/talks/KubeCon/2025-NA/DEMO/k8s-scheduler/로 간 다음, stage1로 이동합니다. 곧이어 ls -1을 입력해 다음과 같은지 확인합니다. 우리는 01-pass-nodename-direct.yaml파일만 동작시켜 볼 예정입니다.

 

root@cp-k8s:~/talks/KubeCon/2025-NA/DEMO/k8s-scheduler# cd stage1
root@cp-k8s:~/talks/KubeCon/2025-NA/DEMO/k8s-scheduler/stage1# ls -1
01-pass-nodename-direct.yaml
02-pass-no-nodename.yaml
03-fail-nodename-notfound.yaml
04-pass-nodename-bypass-taint.yaml
05-fail-no-nodename-taint.yaml

 

2. 01-pass-nodename-direct.yaml을 apply하기 전에, kg-nodes-taints 명령어로 설정된 taint 정보를 다시 확인합니다.

 

root@cp-k8s:~/talks/KubeCon/2025-NA/DEMO/k8s-scheduler/stage1# kg-nodes-taints
NAME     TAINTS
cp-k8s   [map[effect:NoSchedule key:node-role.kubernetes.io/control-plane]]
w1-k8s   <none>
w2-k8s   <none>
w3-k8s   <none>
w4-k8s   <none>
w5-k8s   [map[effect:NoSchedule key:gpu value:nvidia]]
w6-k8s   [map[effect:PreferNoSchedule key:maintenance value:true]]

 

3. 이제 apply를 진행한 다음, 파드가 배치된 노드를 kgpw(alias: k get pods -o wide) 명령으로 확인합니다.

taint가 설정되어 있음에도 w5-k8s에 파드가 어사인된 것을 확인할 수 있습니다. nodeName이 스케줄러의 taint 검사를 우회했기 때문입니다.

 

root@cp-k8s:~/talks/KubeCon/2025-NA/DEMO/k8s-scheduler/stage1# k apply -f 01-pass-nodename-direct.yaml
pod/stage1-nodename-direct created
root@cp-k8s:~/talks/KubeCon/2025-NA/DEMO/k8s-scheduler/stage1# kgpw
NAME                     READY   STATUS    RESTARTS   AGE   IP            NODE     NOMINATED NODE   READINESS GATES
stage1-nodename-direct   1/1     Running   0          9s    172.16.5.70   w5-k8s   <none>           <none>

 

4. 다음 실습을 위해 kd .을 입력해 배포된 모든 파드를 지웁니다. 이어지는 모든 실습에서도 한 차례 단계를 거치면 반드시 같은 명령으로 파드를 지우기를 권장합니다.

*kd: kubectl delete -f 의 alias, ka: kubectl apply -f의 alias

 

root@cp-k8s:~/talks/KubeCon/2025-NA/DEMO/k8s-scheduler/stage1# kd .
pod "stage1-nodename-direct" deleted from default namespace
Error from server (NotFound): error when deleting "02-pass-no-nodename.yaml": pods "stage1-no-nodename" not found
Error from server (NotFound): error when deleting "03-fail-nodename-notfound.yaml": pods "stage1-nodename-notfound" not found
Error from server (NotFound): error when deleting "04-pass-nodename-bypass-taint.yaml": pods "stage1-nodename-bypass-taint" not found
Error from server (NotFound): error when deleting "05-fail-no-nodename-taint.yaml": pods "stage1-no-nodename-taint-fail" not found

 

 

Stage2: 필터(Filter)

 

두 번째로 살펴볼 Stage 2, 필터를 처리하는 주요 코드 부분은 다음과 같습니다.

 

<출처: Kubernetes GitHub>

 

우리가 이미 살펴본 nodeName의 경우도 필터의 일종이긴 합니다. 다만 nodeName은 스케줄러를 거치지 않는 구조를 가지고 있어, 다른 필터와는 동작 방식이 꽤 다르게 보입니다. 그래서 해당 부분은 앞에서 따로 분리해 설명했습니다.

 

그 외 다른 종류의 내장 필터는 다음과 같이 구성됩니다. 이러한 필터 조건에 따라 Stage3인 스코어(Score)를 거치지 않거나, 적합한 노드가 생성될 때까지 기다리는 특성을 갖습니다.

 

  • NodeSelector
  • Resource Requests
  • Required NodeAffinity
  • Required PodAffinity
  • Taints / Tolerations (NoSchedule, NoExecute)
  • TopologySpreadConstraints (DoNotSchedule)

 

이제 본격적으로 다시 코드를 살펴보겠습니다. 최상단 영역의 코드입니다.

 

 

여기서 유심히 살펴봐야 할 부분은 feasibleNodes입니다. 문구 그대로 실행할 수 있는(feasible) 노드들이라는 뜻을 가집니다. 이러한 feasible 노드는 NodeSelector 등 내장 필터를 거치며 구성되는데, 0개, 1개, 또는 그 외의 수까지 총 3가지로 구분하여 동작합니다.

 

 

우선 0개라면( len(feasibleNodes) == 0 ), 파드를 어사인할 수 있는 노드가 없다는 상태로 에러가 발생하게 됩니다. 한편 1개라면 ( len(feasibleNodes) == 1 ), 더이상 고민할 것이 없이 바로 어사인이 이뤄집니다. 그 1개 외에는 파드를 보낼 수 있는 충족 조건이 없기 때문입니다.

 

 

마지막으로 0개도 아니고 1개도 아닌, 즉 2개 이상인 상태에서는 feasibleNodes를 스코어(Score) 계산으로 넘겨 다음 조건까지 함께 비교합니다.

 

실습으로 확인하기

이제 실습으로 돌아가겠습니다. 98.filter-leaves-one-node-score-bypassed.yaml 파일은 nodeSelector, tolerations, affinity 조합을 통해 w5-k8s만 feasibleNodes로 남도록 구성한 예제입니다.

 

이에 따라 그 다음 score에 해당하는 w2-k8s 노드는 무시하는 내용을 담고 있습니다.

 

 

위 코드를 실제 스케줄러 흐름과 연결해 보면, 코드를 더 쉽게 이해할 수 있습니다.

 

 

코드의 실제 동작을 확인하기 위해, 앞서 설명한 것과 동일하게 총 3가지 시나리오(0개, 1개, 그 외)를 진행합니다.

 

1. stage2폴더로 이동합니다. 실습은 97, 98, 99 총 3개 파일로 진행할 것입니다.

 

2. 97.filter-leaves-zero-nodes-unschedulable.yaml은 nodeSelector와 affinity로 w5-k8s에 파드를 어사인하려고 하지만, taint를 통과하지 못하는 구조입니다. 그 결과 feasibleNodes가 0개가 되고, 파드는 노드에 어사인되지 못한 채 Pending 상태로 남습니다.

 

root@cp-k8s:~/talks/KubeCon/2025-NA/DEMO/k8s-scheduler/stage2# ka 97.filter-leaves-zero-nodes-unschedulable.yaml
pod/filter-leaves-zero-nodes-unschedulable created
root@cp-k8s:~/talks/KubeCon/2025-NA/DEMO/k8s-scheduler/stage2# kgpw
NAME                                     READY   STATUS    RESTARTS   AGE   IP       NODE     NOMINATED NODE   READINESS GATES
filter-leaves-zero-nodes-unschedulable   0/1     Pending   0          3s    <none>   <none>   <none>           <none>

 

3. kd로 해당 파드를 지우고, feasibleNodes가 1개 있는 상태인 98번을 적용해 보겠습니다. 이 경우에는 taint에 해당하는 toleration이 존재함으로 w5-k8s에 정상적으로 파드를 어사인할 수 있습니다.

 

root@cp-k8s:~/talks/KubeCon/2025-NA/DEMO/k8s-scheduler/stage2# kd 97.filter-leaves-zero-nodes-unschedulable.yaml
pod "filter-leaves-zero-nodes-unschedulable" deleted from default namespace
root@cp-k8s:~/talks/KubeCon/2025-NA/DEMO/k8s-scheduler/stage2# ka 98.filter-leaves-one-node-score-bypassed.yaml
pod/filter-leaves-one-node-score-bypassed created
root@cp-k8s:~/talks/KubeCon/2025-NA/DEMO/k8s-scheduler/stage2# kgpw
NAME                                    READY   STATUS    RESTARTS   AGE   IP            NODE     NOMINATED NODE   READINESS GATES
filter-leaves-one-node-score-bypassed   1/1     Running   0          3s    172.16.5.17   w5-k8s   <none>           <none>

 

4. 마지막으로 99번은 feasibleNodes가 여러 개인 경우입니다. 여기서는 필터 조건에 따라 w3-k8s와 w5-k8s가 feasibleNodes로 남습니다. Score 결과는 w1-k8s가 더 높게 나오지만, 스케줄러는 필터 조건을 더 우선합니다. 따라서 w1-k8s는 무시되고 w3-k8s 또는 w5-k8s 중 하나에 어사인됩니다.

 

root@cp-k8s:~/talks/KubeCon/2025-NA/DEMO/k8s-scheduler/stage2# kd 98.filter-leaves-one-node-score-bypassed.yaml
pod "filter-leaves-one-node-score-bypassed" deleted from default namespace
root@cp-k8s:~/talks/KubeCon/2025-NA/DEMO/k8s-scheduler/stage2# ka 99.filter-leaves-two-nodes-score-zero.yaml
pod/filter-leaves-two-nodes-score-zero created
root@cp-k8s:~/talks/KubeCon/2025-NA/DEMO/k8s-scheduler/stage2# kgpw
NAME                                 READY   STATUS    RESTARTS   AGE   IP            NODE     NOMINATED NODE   READINESS GATES
filter-leaves-two-nodes-score-zero   1/1     Running   0          20s   172.16.3.16   w3-k8s   <none>           <none>

 

 

Stage3: 스코어 (Score)

 

세 번째, Stage3로 스코어를 처리하는 주요 코드 부분은 다음과 같습니다. 스코어의 경우에는 처리 과정이 꽤 복잡하기 때문에, 모든 부분을 포함하지 않고 핵심 일부만 가져왔습니다.

 

<출처: Kubernetes GitHub>

 

여기서는 스코어(ScorePlugins) 계산 자체에 집중해 설명하겠습니다. 앞 단계인 Filter가 가능한 후보를 고르는 과정이라면, Score는 그 후보에 점수를 매기는 과정입니다.

 

스코어 계산에 사용되는 내장 필터(정확히는 Score 플러그인)는 다음과 같습니다. 대부분 접두어로 Preferred가 붙기에, 필수 조건이 아닌 선호 조건(preferred)으로 동작한다는 것을 알 수 있습니다.

 

  • Preferred NodeAffinity
  • Preferred PodAffinity
  • Taints / Tolerations (PreferNoSchedule)
  • TopologySpreadConstraints (ScheduleAnyway)

 

실습으로 확인하기

실습으로 돌아가 보겠습니다. 다음의 99.score-picks-winner.yaml 파일은 현재 가장 높은 스코어를 가지는 곳에 파드를 어사인하기 위한 테스트 목적으로 작성되었습니다. 따라서 taint-node.sh를 필터 조건으로 스코어 계산을 회피하도록 합니다.

 

 

각 노드의 스코어 계산은 다음과 같이 이루어집니다. 계산 결과를 보면 w1-k8s가 가장 높은 점수를 가지며, 파드는 기본적으로 w1-k8s에 어사인됨을 알 수 있습니다.

 

 

하지만 taint와 같은 필터 조건이 추가되면 이야기가 달라집니다. 필터는 스코어보다 먼저 적용되며, 더 강한 제약으로 동작합니다. 따라서 taint가 없는 노드 중에서 가장 높은 스코어를 가진 노드가 선택됩니다.

 

이 부분은 실습으로 확인하는 것이 더 직관적이므로, 몇 가지 명령어를 실행해 보겠습니다.

 

1. stage3으로 이동합니다. 실습은 위에서 말한대로 99번으로 진행하겠지만, taint로 필터 조건을 걸어 어사인되는 노드를 계속 변경해 볼 것 입니다.

 

2. 99.score-picks-winner.yaml를 배포할 때, 각 노드의 스코어 값은 다음과 같습니다. 참고로, w5-k8s와 w6-k8s는 taint 조건에 의해 스코어 산정에서 제외되었습니다.

 

  • w1-k8s: zone-a (100) + SSD (80) = 180 points
  • w2-k8s: zone-a (100) + HDD (30) = 130 points
  • w3-k8s: zone-b (0) + SSD (80) = 80 points
  • w4-k8s: zone-b (0) + HDD (30) = 30 points

 

이 계산 결과를 염두에 두고 99번을 apply한 뒤, kgpw 명령어로 현재 파드가 배포된 노드가 w1-k8s인지 확인합니다.

 

root@cp-k8s:~/talks/KubeCon/2025-NA/DEMO/k8s-scheduler/stage3# ka 99.score-picks-winner.yaml
pod/score-picks-winner created
root@cp-k8s:~/talks/KubeCon/2025-NA/DEMO/k8s-scheduler/stage3# kgpw
NAME                 READY   STATUS    RESTARTS   AGE   IP            NODE     NOMINATED NODE   READINESS GATES
score-picks-winner   1/1     Running   0          9s    172.16.1.97   w1-k8s   <none>           <none>

 

3. 이제 taint 필터가 스코어 기반 선택에 어떠한 영향을 끼치는지 보겠습니다. 배포된 파드를 삭제한 뒤, ./taint-node.sh를 실행하고 w1-k8s를 선택합니다. (fzf가 설치되어 있으면 목록에서 선택할 수 있고, 설치되어 있지 않으면 직접 w1-k8s를 입력하도록 되어 있습니다.)

 

root@cp-k8s:~/talks/KubeCon/2025-NA/DEMO/k8s-scheduler/stage3# kd 99.score-picks-winner.yaml
pod "score-picks-winner" deleted from default namespace

 

4. 보기 중에 1) NoSchedule을선택해 taint를 적용합니다.

Applying NoSchedule taint to w1-k8s...
node/w1-k8s modified


✓ NoSchedule taint applied


Effect:
- w1-k8s will be FILTERED OUT in Stage 2 (hard constraint)
- comprehensive-stage3-winner Pod cannot be scheduled to w1-k8s
- Scheduler will pick the next highest scoring node from remaining candidates


To test:
  kubectl delete pod comprehensive-stage3-winner 2>/dev/null || true
  kubectl apply -f 99.comprehensive-stage3-winner.yaml
  kubectl get pod comprehensive-stage3-winner -o wide


Current taints on w1-k8s after change:
[
  {
    "effect": "NoSchedule",
    "key": "demo",
    "value": "test"
  }
]


============================================================
Taint operation completed
============================================================

 

5. 다시 99번을 배포하고, kgpw 명령어로 두 번째 높은 스코어를 가진 w2-k8s에 배포되었는지 확인합니다.

 

root@cp-k8s:~/talks/KubeCon/2025-NA/DEMO/k8s-scheduler/stage3# ka 99.score-picks-winner.yaml
pod/score-picks-winner created
root@cp-k8s:~/talks/KubeCon/2025-NA/DEMO/k8s-scheduler/stage3# kgpw
NAME                 READY   STATUS    RESTARTS   AGE   IP             NODE     NOMINATED NODE   READINESS GATES
score-picks-winner   1/1     Running   0          43s   172.16.2.116   w2-k8s   <none>           <none>

 

7. 한 차례 확인만으로는 불확실합니다. 다시 w2-k8s에 taint를 걸고 확인해 보겠습니다. 배포된 파드를 우선 지우고, ./taint-node.sh 를 실행한 후에 w2-k8s를 선택합니다. 그리고 1) NoSchedule을선택해 taint를 적용합니다.

 

곧이어 kg-nodes-taints를 입력해 w2-k8s에 적용된 taint 설정을 확인합니다. 아래 결과가 나오면 완료입니다.

 

Applying NoSchedule taint to w2-k8s...
node/w2-k8s modified


✓ NoSchedule taint applied


Effect:
- w2-k8s will be FILTERED OUT in Stage 2 (hard constraint)
- comprehensive-stage3-winner Pod cannot be scheduled to w2-k8s
- Scheduler will pick the next highest scoring node from remaining candidates


To test:
  kubectl delete pod comprehensive-stage3-winner 2>/dev/null || true
  kubectl apply -f 99.comprehensive-stage3-winner.yaml
  kubectl get pod comprehensive-stage3-winner -o wide


Current taints on w2-k8s after change:
[
  {
    "effect": "NoSchedule",
    "key": "demo",
    "value": "test"
  }
]


============================================================
Taint operation completed
============================================================
root@cp-k8s:~/talks/KubeCon/2025-NA/DEMO/k8s-scheduler/stage3# kg-nodes-taints
NAME     TAINTS
cp-k8s   [map[effect:NoSchedule key:node-role.kubernetes.io/control-plane]]
w1-k8s   [map[effect:NoSchedule key:demo value:test]]
w2-k8s   [map[effect:NoSchedule key:demo value:test]]
w3-k8s   <none>
w4-k8s   <none>
w5-k8s   [map[effect:NoSchedule key:gpu value:nvidia]]
w6-k8s   [map[effect:PreferNoSchedule key:maintenance value:true]]

 

8. 다시 99.score-picks-winner.yaml를 배포하면, 파드가 어사인된 노드가 스코어 3순위인 w3-k8s가 되었는지 체크해 볼 수 있습니다.

 

root@cp-k8s:~/talks/KubeCon/2025-NA/DEMO/k8s-scheduler/stage3# ka 99.score-picks-winner.yaml
pod/score-picks-winner created
root@cp-k8s:~/talks/KubeCon/2025-NA/DEMO/k8s-scheduler/stage3# kgpw
NAME                 READY   STATUS    RESTARTS   AGE   IP             NODE     NOMINATED NODE   READINESS GATES
score-picks-winner   1/1     Running   0          1s    172.16.3.132   w3-k8s   <none>           <none>

 

 

마치며: ‘쿠버네티스 코드를 본다’는 것의 의미는?

지금까지 쿠버네티스 스케줄러의 동작 방식을 확인했습니다. nodeName의 특수한 동작부터, Filter와 Score의 우선순위에 따라 파드 배정이 어떻게 달라지는지 살펴봤습니다.

 

쿠버네티스를 코드로 본다는 것은 쿠버네티스가 동작하는 방식을 더 면밀하게 살펴본다는 뜻입니다. 물론 과거에는 이러한 대형 프로젝트의 코드를 직접 살펴보는 것은 매우 시간이 많이 걸리고 어려운 일이었습니다. 그러나 요즘은 AI의 도움을 받아 쉽게 시작 지점을 찾고 연관된 부분을 찾아 해석할 수 있습니다.

 

예를 들어 쿠버네티스 GitHub 소스를 모두 내려받은 다음, 클로드 코드에 다음과 같이 오늘 실습 내용을 질의하면 정말 많은 내용을 분석하고 이해할 수 있습니다.

 

[클로드 코드 질의 예시]

> 현재 쿠버네티스 폴더를 읽어서 nodeName이 동작하는 코드 부분을 알려줘


⏺ Explore(Find nodeName code in K8s)
  ⎿  Done (36 tool uses · 82.0k tokens · 2m 2s)


⏺ Kubernetes nodeName 동작 코드 분석


  1. nodeName 정의 위치


  파일: staging/src/k8s.io/api/core/v1/types.go:4173-4179


  // NodeName indicates in which node this pod is scheduled.
  // If empty, this pod is a candidate for scheduling by the scheduler defined in schedulerName.
  // Once this field is set, the kubelet for this node becomes responsible for the lifecycle of this pod.
  NodeName string `json:"nodeName,omitempty" protobuf:"bytes,10,opt,name=nodeName"`


  ---
  2. 스케줄러 필터 플러그인 (nodeName으로 스케줄러 우회)


  파일: pkg/scheduler/framework/plugins/nodename/node_name.go:70-82


  func (pl *NodeName) Filter(ctx context.Context, _ fwk.CycleState, pod *v1.Pod, nodeInfo fwk.NodeInfo) *fwk.Status {
      if !Fits(pod, nodeInfo) {
          return fwk.NewStatus(fwk.UnschedulableAndUnresolvable, ErrReason)
      }
      return nil
  }


  func Fits(pod *v1.Pod, nodeInfo fwk.NodeInfo) bool {
      return len(pod.Spec.NodeName) == 0 || pod.Spec.NodeName == nodeInfo.Node().Name
  }


  동작 원리:
  - nodeName이 비어있으면 → 모든 노드에서 필터 통과 (일반 스케줄링)
  - nodeName이 설정되면 → 해당 노드만 통과, 나머지는 UnschedulableAndUnresolvable 반환


[ 생략 ]

 

코드 뜯어보기를 시작하려는 것은, 그 마음을 먹는 일만으로 다소 부담스러울 수 있습니다. 하지만 공식 문서를 넘어, 그 안에 담긴 의미를 찾으려면 결국 코드를 직접 보는 과정이 필요합니다.

 

따라서 작은 부분이라도 코드를 보려는 시도가 여러분의 성장에 큰 도움이 될 것이라 믿습니다. 코드로 읽어보고 그것으로 기술을 이해하는 작업을 추천하고 싶습니다.

 

지금까지 살펴본 내용은 KubeCon + CloudNativeCon 북미 2025에서 제가 한 Anatomy of a Kubernetes Scheduler: Narrate Workloads Priority in Sequence 발표를 글로 옮긴 것입니다. 영상으로 보면 또 다른 재미가 있지 않을까 합니다.


작가

조훈(CNCF 앰버서더)

메가존에서 쿠버네티스와 컨테이너 인프라 Tech Evangelist, CoE(Center of Excellence) 역할을 맡고 있다. 클라우드 네이티브 컴퓨팅 재단(CNCF)의 글로벌 앰버서더, ‘IT 인프라 엔지니어 그룹’의 운영진, 오픈소스 컨트리뷰터로도 활동하고 있다. 인프런/유데미에서 앤서블 및 쿠버네티스에 관한 강의를 하고, 『컨테이너 인프라 환경 구축을 위한 쿠버네티스/도커』, 『우아하게 앤서블』, 『시스템/네트워크 관리자를 위한 파이썬 실무 프로그래밍』을 집필하였으며, 요즘IT와 같은 온라인 플랫폼에 글을 기고한다.

 

심근우

LG유플러스 CTO부문에서 대고객 비즈니스 시스템의 DevOps를 담당하는 UcubeDAX팀의 팀장으로 일하고 있다. 퍼블릭 클라우드와 프라이빗 클라우드에 걸친 쿠버네티스 클러스터를 안정적으로 운영하기 위해 노력하고 있으며, 특히 주니어 DevOps 엔지니어들의 육성에 큰 관심을 가지고 있다.

 

문성주 

글로벌 소셜 플랫폼 기업에서 Site Reliability Engineer로 재직하며, 쿠버네티스 멀티 클러스터 관리와 데이터베이스 플랫폼 운영을 주도하고 있다. 또한 ISMS-P, GDPR, CCPA 등 글로벌 보안 규제에 부합하는 데이터 라이프사이클 파이프라인을 설계·운영한 실무 경험을 가지고 있으며, 쿠버네티스 오픈소스 프로젝트에도 기여하고 있다. 더불어 국내 주요 기업과 국가 기관의 클라우드 전환, 데이터 거버넌스 컨설팅, 보안 컴플라이언스 대응을 지원하고 있다.

 

이성민

미국 넷플릭스(Netflix) 사의 Data Platform Infrastructure 팀에서 사내 플랫폼 팀들과 데이터 사용자들을 어우르기 위한 가상화 및 도구들을 개발하는 일들을 하고 있다. 과거 컨테이너와 쿠버네티스에 큰 관심을 두고 ingress-nginx를 비롯한 오픈 소스에 참여했으며, 현재는 데이터 분야에 일하게 되면서 stateful 한 서비스들이 컨테이너화에서 겪는 어려움을 보다 근본적으로 해결하기 위한 많은 노력을 하고 있다.

 

©️요즘IT의 모든 콘텐츠는 저작권법의 보호를 받는 바, 무단 전재와 복사, 배포 등을 금합니다.