CKA — 쿠버네티스 관리자 · 워크로드와 스케줄링 · 이론
누가 파드를 만들고, 누가 노드를 고르는가
한 줄 요약
Deployment 는 파드를 만들지 않습니다. ReplicaSet 을 만들고, ReplicaSet 이 파드를 만들고, 스케줄러가 노드를 고르고, kubelet 이 실행합니다. 각 층이 자기 아래 한 층만 봅니다. 이 사슬을 알면 롤아웃과 스케줄링 문제를 층별로 끊어서 진단할 수 있습니다.
왜 이게 필요했나
왜 Deployment 가 파드를 직접 만들지 않을까요. 롤링 업데이트 때문입니다.
이미지를 바꾸면 Deployment 는 새 ReplicaSet 을 하나 더 만듭니다. 그리고 새 것을 늘리면서 옛 것을 줄입니다. 이때 얼마나 빨리 늘리고 줄일지가 maxSurge 와 maxUnavailable 입니다.
| 파라미터 | 뜻 | replicas=4 일 때 |
| --- | --- | --- |
| maxSurge: 2 | 목표치보다 몇 개까지 더 띄울 수 있나 | 롤링 중 최대 6개 |
| maxUnavailable: 0 | 목표치보다 몇 개까지 부족해도 되나 | 항상 4개는 Ready |
둘 다 0 으로 두면 아무것도 못 움직입니다. maxUnavailable: 0 은 무중단을 뜻하지만 여유 자원이 필요하고, maxSurge: 0 은 자원을 아끼는 대신 잠깐 용량이 줄어듭니다.
되돌리기가 가능한 이유도 여기 있습니다. 옛 ReplicaSet 은 지워지지 않고 레플리카 0 으로 남아 있습니다. revisionHistoryLimit 개수만큼입니다. rollout undo 는 그 옛 ReplicaSet 을 다시 키우는 것뿐입니다. 이 값을 0 으로 두면 되돌릴 수 없습니다.
어떻게 동작하나
스케줄러에게 배치를 지시하는 손잡이는 크게 일곱입니다.
- nodeSelector — 라벨 완전 일치. 가장 단순하고 가장 자주 쓰입니다.
- nodeAffinity required — 표현식(In, NotIn, Exists, Gt, Lt)으로 노드를 고릅니다. 못 맞추면 Pending.
- nodeAffinity preferred — weight 를 주면 score 단계에서 가산점만 줍니다. 못 맞춰도 배치됩니다.
- podAntiAffinity — 같은 topologyKey 안에 같은 라벨의 파드가 있으면 피합니다. 고가용성 배치의 기본.
- taint / toleration — 노드가 거는 거부, 파드가 내미는 통행증.
NoSchedule은 새 파드만 막고,NoExecute는 이미 뜬 파드도 쫓아냅니다. - PriorityClass — 자원이 모자랄 때 누가 먼저인지. 높은 우선순위 파드가 낮은 것을 선점할 수 있습니다.
- topologySpreadConstraints — 존/노드별 파드 수 편차를
maxSkew이하로 유지합니다.
여기서 자주 틀리는 부분: topologySpread 의 후보 도메인 집합은 파드의 nodeAffinity/nodeSelector 를 통과하는 노드들입니다. 반면 테인트는 기본적으로 무시하고 셉니다. 그래서 cordon 되거나 테인트된 노드가 파드 0개짜리 도메인으로 남아 편차를 키우고, DoNotSchedule 이면 나머지가 Pending 이 되는 일이 생깁니다.
현장에서 만나는 모습
사례 1 — Pending 은 오류가 아닐 수 있다. 홈랩을 kubeadm 1.34 + Cilium 으로 재구축한 직후, 단일 노드 상태에서 hubble-relay 와 hubble-ui 가 Pending 에 머물렀습니다.
Warning FailedScheduling 0/1 nodes are available: 1 node(s) had untolerated taint(s).컨트롤 플레인 노드에는 node-role.kubernetes.io/control-plane:NoSchedule 테인트가 걸려 있고, hubble 구성요소는 DaemonSet 이 아니라 Deployment 라 이를 톨러레이트하지 않습니다. 반면 CoreDNS 는 기본 톨러레이션을 갖고 있어 멀쩡히 떴습니다. 같은 클러스터, 같은 노드, 다른 결과 — 차이는 톨러레이션 하나였습니다. 워커가 조인하자 바로 해소됐습니다. 이건 버그가 아니라 정상 동작입니다.
사례 2 — GPU 1개는 GPU 1개가 아니다. 같은 홈랩에 GPU 4장이 붙어 있습니다. RTX 3090 24GB, RTX 5090 32GB, RTX 4070 Laptop 8GB 두 장. 파드가 nvidia.com/gpu: 1 만 요청하면 32GB 가 필요한 학습이 8GB 노트북 GPU 에 얹힐 수 있습니다. 스케줄러에게 확장 자원은 개수일 뿐이고 둘 다 똑같이 GPU 1개이기 때문입니다.
GPU Feature Discovery 가 붙여 주는 gpu.memory 라벨은 문자열이라 24GB 이상 같은 비교 셀렉터가 안 됩니다. 그래서 의미 기반 라벨을 직접 얹었습니다.
gpu.homelab/tier=xlarge gpu.homelab/vram=32g # 5090gpu.homelab/tier=large gpu.homelab/vram=24g # 3090gpu.homelab/tier=small gpu.homelab/vram=8g # 4070 Laptop x2이제 워크로드가 nodeSelector: {gpu.homelab/tier: xlarge} 로 자기 체급을 고릅니다. 스케줄링은 결국 자원의 이름을 얼마나 정확히 지었느냐의 문제입니다.
덧붙여 배치 대상 선택의 예: 같은 클러스터에서 GPU Operator 를 올렸더니 NFD worker 는 노드 5대 전부에, 나머지 GPU 구성요소는 GPU 노드 4대에만 떴습니다. NFD 가 붙인 라벨을 다른 DaemonSet 들이 nodeSelector 로 보고 있기 때문입니다.
다음 실습에서 할 것
첫 실습에서 Deployment 를 만들고 스케일하고 롤링 파라미터를 조정한 뒤 실제로 롤아웃하고 되돌립니다. DaemonSet 과 StatefulSet 도 만들어 봅니다. 두 번째 실습에서는 nodeSelector 부터 topologySpreadConstraints 까지 일곱 손잡이를 차례로 써 봅니다. 마지막 문제는 앞에서 cordon 하고 테인트한 노드 상태를 기억해야만 풀립니다.