스케줄링과 리소스 — 파드는 어느 노드로 가나
스케줄러는 딱 한 가지 결정만 한다 — 이 파드를 어느 노드에 둘 것인가. 그런데 그 한 결정에
Pending의 거의 모든 원인이 들어 있다.
1. 두 단계로 정한다
-
① 필터링(Filtering) 둘 수 있는 노드를 추린다
-
② 스코어링(Scoring) 남은 것 중 가장 좋은 곳을 고른다
- 자원이 고르게 퍼지도록
- 이미지가 이미 있는 노드를 선호
- 어피니티 가중치 반영
하나도 안 남으면 파드는 Pending 으로 멈춘다 그리고 그 이유가 이벤트에 그대로 적힌다
kubectl describe pod web-xxx | tail -20
# Events:
# Warning FailedScheduling 0/5 nodes are available:
# 3 Insufficient cpu, 2 node(s) had untolerated taint {node-role...}
Pending을 만나면 반드시describe를 읽는다. 몇 개 노드가 왜 탈락했는지가 문장으로 나온다. 추측할 필요가 없다.
2. requests 와 limits — 역할이 다르다
resources:
requests:
cpu: "250m"
memory: "256Mi"
limits:
cpu: "1"
memory: "512Mi"
-
- 이만큼 확보 가능한 노드를 찾는다
- 실제 사용량과 무관하다 — 예약이지 소비가 아니다
-
limits — '실행 중 상한' 이다
가장 흔한 오해 — "노드에 메모리가 남는데 왜 Pending 인가"
- 스케줄러는 '실제 사용량' 이 아니라 'requests 의 합' 을 본다
- requests 를 크게 잡은 파드들이 실제로는 놀고 있어도
- 장부상으로는 노드가 꽉 찬 것이다
kubectl describe node <name> 의 Allocated resources 가 그 장부다
CPU 단위
- 1 = 1 코어(vCPU)
- 500m — = 0.5 코어 (m = milli)
- 250m — = 0.25 코어
메모리
Mi/Gi 2진 (1Mi = 1,048,576 바이트) ← 보통 이것을 쓴다
M/G 10진 (1M = 1,000,000 바이트)
512M 과 512Mi 는 다르다. 섞어 쓰면 계산이 어긋난다
3. QoS 클래스 — 자원이 모자랄 때 누가 먼저 죽나
-
Guaranteed — 모든 컨테이너에 requests == limits (cpu·memory 둘 다)
- 가장 나중에 퇴출된다
-
Burstable — requests 는 있는데 limits 와 다르거나 일부만 있다
- 중간
-
BestEffort — requests·limits 가 아예 없다
- 가장 먼저 퇴출된다
kubectl get pod web -o jsonpath='{.status.qosClass}'
노드 메모리가 부족해지면 kubelet 이 파드를 퇴출(evict)한다 순서 — BestEffort → Burstable(requests 초과분이 큰 순) → Guaranteed
중요한 워크로드일수록 requests 를 명시해야 하는 이유다 아무것도 안 적으면 BestEffort 가 되어 제일 먼저 쫓겨난다
4. 어디에 둘지 지정하기
# ① 가장 단순 — 라벨이 맞는 노드에만
nodeSelector:
disktype: ssd
# ② 어피니티 — 조건을 더 표현할 수 있다
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution: # 필수
nodeSelectorTerms:
- matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values: ["ap-northeast-2a", "ap-northeast-2c"]
preferredDuringSchedulingIgnoredDuringExecution: # 선호
- weight: 100
preference:
matchExpressions:
- key: node-type
operator: In
values: ["compute"]
-
required — 만족하는 노드가 없으면 Pending
-
preferred — 없으면 그냥 다른 곳에 둔다 (점수만 준다)
-
IgnoredDuringExecution 이 이름에 붙은 이유
- 이미 떠 있는 파드는 조건이 깨져도 쫓겨나지 않는다
- 배치 시점에만 본다
파드끼리의 관계
# 같은 노드에 모으기 (affinity) — 캐시와 앱을 붙인다
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels: { app: cache }
topologyKey: kubernetes.io/hostname
# 흩뜨리기 (anti-affinity) — 같은 노드에 몰리지 않게
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels: { app: web }
topologyKey: kubernetes.io/hostname
topologyKey 가 '무엇을 같은 곳으로 볼 것인가' 를 정한다
kubernetes.io/hostname 같은 노드
topology.kubernetes.io/zone 같은 가용 영역
anti-affinity 를 required 로 걸고 replicas 를 노드 수보다 크게 잡으면 남는 파드가 영원히 Pending 이 된다 — 매우 흔한 실수다
더 나은 도구 — topologySpreadConstraints
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels: { app: web }
anti-affinity 는 '같이 두지 마라' 만 말할 수 있다 spread 는 '얼마나 고르게' 를 수치로 말한다
maxSkew: 1 영역 간 파드 수 차이가 1을 넘지 않게
whenUnsatisfiable
DoNotSchedule 못 지키면 Pending
ScheduleAnyway 못 지켜도 배치 (점수만 반영) ← 대개 이쪽이 안전하다
5. 테인트와 톨러레이션 — 노드가 거부한다
kubectl taint nodes node1 gpu=true:NoSchedule
tolerations:
- key: "gpu"
operator: "Equal"
value: "true"
effect: "NoSchedule"
방향이 반대다
어피니티 파드가 "나는 이런 노드로 가고 싶다"
테인트 노드가 "나는 아무나 안 받는다"
톨러레이션 파드가 "나는 그 거부를 견딜 수 있다"
effect
NoSchedule 새로 배치되지 않는다 (기존 파드는 유지)
PreferNoSchedule 가급적 피한다
NoExecute 기존 파드도 쫓아낸다
쓰임
- GPU 노드를 전용으로 — GPU 파드만 톨러레이션을 갖는다
- 컨트롤 플레인 노드에 일반 워크로드가 안 뜨게 (기본 테인트)
- 문제 있는 노드를 격리 (cordon/drain 과 함께)
주의 — 톨러레이션이 있다고 '그 노드로 간다' 는 뜻이 아니다
- '갈 수 있다' 는 뜻일 뿐이다. 유도하려면 nodeAffinity 를 함께 쓴다
6. 노드를 비우기 — cordon 과 drain
kubectl cordon node1 # 새 파드를 안 받는다 (기존은 그대로)
kubectl drain node1 --ignore-daemonsets --delete-emptydir-data
kubectl uncordon node1 # 다시 받는다
drain 은 노드의 파드를 다른 곳으로 옮긴다 (퇴출 API 사용)
--ignore-daemonsets DaemonSet 파드는 어차피 노드마다 떠야 하므로 제외
--delete-emptydir-data emptyDir 데이터가 사라짐을 인정한다
노드 업그레이드·교체의 표준 절차다
- cordon → drain → 작업 → uncordon
PodDisruptionBudget — 한꺼번에 빠지지 않게
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: web-pdb
spec:
minAvailable: 2 # 또는 maxUnavailable: 1
selector:
matchLabels: { app: web }
drain 이나 클러스터 오토스케일러가 파드를 뺄 때 이 조건을 지킨다
- 세 개 중 두 개는 항상 살아 있어야 한다면 minAvailable: 2
주의
- PDB 는 '자발적 중단' 만 막는다 (drain · 오토스케일러)
- 노드가 갑자기 죽는 것(비자발적)은 못 막는다
- minAvailable 을 replicas 와 같게 두면 drain 이 영원히 멈춘다
- 노드 업그레이드가 진행되지 않는다. 흔한 사고다
7. 오토스케일링 세 가지
- HPA (Horizontal Pod Autoscaler) 파드 '개수' 를 늘린다
- VPA (Vertical Pod Autoscaler) — 파드의 requests/limits 를 조정한다
- Cluster Autoscaler — '노드' 를 늘린다
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
spec:
scaleTargetRef: { apiVersion: apps/v1, kind: Deployment, name: web }
minReplicas: 3
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target: { type: Utilization, averageUtilization: 70 }
중요 — HPA 의 CPU 사용률은 'requests 대비' 비율이다
- requests 가 없으면 계산이 불가능해 HPA 가 동작하지 않는다
- HPA 를 쓰려면 requests 는 필수다
그리고 metrics-server 가 설치돼 있어야 한다 없으면 kubectl top 도 안 되고 HPA 도 unknown 으로 멈춘다
세 오토스케일러의 상호작용 HPA 와 VPA 를 같은 지표(CPU)로 함께 쓰면 서로 싸운다
-
VPA 는 requests 를 올리고, HPA 는 그 requests 기준 비율을 본다
-
권장되지 않는 조합이다 HPA + Cluster Autoscaler 는 잘 맞는다
-
파드가 Pending 이 되면 노드를 늘린다
In-Place 리사이즈 (1.35 GA)
예전에는 requests/limits 를 바꾸면 파드가 재생성됐다 1.35 부터 재시작 없이 조정할 수 있다 (In-Place Pod Resize) 1.36 에서는 파드 수준 리소스도 조정 가능해졌다 (베타)
상태 있는 워크로드에서 특히 의미가 크다 — 스케일 조정이 곧 중단이 아니게 된다
8. LimitRange 와 ResourceQuota
# 네임스페이스 안 파드의 기본값·상한을 정한다
apiVersion: v1
kind: LimitRange
spec:
limits:
- type: Container
default: { cpu: "500m", memory: "512Mi" } # limits 기본값
defaultRequest: { cpu: "100m", memory: "128Mi" } # requests 기본값
max: { cpu: "2", memory: "2Gi" }
# 네임스페이스 전체 총량을 제한한다
apiVersion: v1
kind: ResourceQuota
spec:
hard:
requests.cpu: "20"
requests.memory: 40Gi
pods: "50"
LimitRange 가 있으면 requests 를 안 쓴 파드에도 기본값이 붙는다
- BestEffort 파드가 생기는 것을 구조적으로 막는다
ResourceQuota 가 걸린 네임스페이스에서는
- requests/limits 를 명시하지 않은 파드가 '생성 거부' 된다
- "파드가 아예 안 만들어진다" 의 원인. 이벤트에 이유가 나온다
한눈에 정리
- 스케줄링 — 필터링 → 스코어링. 실패 이유는 describe 의 Events 에 문장으로
- requests — 스케줄링 기준(예약). 실제 사용량이 아니라 장부다
- limits — 실행 중 상한. CPU 는 스로틀, 메모리는 OOMKilled
- QoS — Guaranteed > Burstable > BestEffort 순으로 늦게 퇴출된다
- nodeSelector/어피니티 — required 는 없으면 Pending, preferred 는 점수만
- anti-affinity required + replicas > 노드 수 = 영구 Pending
- spread — maxSkew 로 고르게. ScheduleAnyway 가 대개 안전하다
- 테인트 — 노드가 거부 · 톨러레이션은 '견딜 수 있다' 일 뿐 유도가 아니다
- cordon/drain — 노드 교체 표준 절차. PDB 로 한꺼번에 빠지는 것을 막는다
- minAvailable == replicas 면 drain 이 영원히 멈춘다
- HPA — requests 대비 비율이다 → requests 없으면 동작하지 않는다
- metrics-server 필수
출처 — Kubernetes Docs: "Kubernetes Scheduler" · "Assigning Pods to Nodes" · "Taints and Tolerations" · "Pod Topology Spread Constraints" · "Resource Management for Pods and Containers" · "Pod Quality of Service Classes" · "Horizontal Pod Autoscaling" · "Disruptions" · CKA Curriculum v1.35