Kubernetes는 자원을 어떻게 나누고 지키는가
cgroups 구현부터 QoS 클래스, HPA·VPA·클러스터 오토스케일러까지, Kubernetes 자원 관리 철학의 층위를 추적한다.
- 01 쿠버네티스는 왜 모든 것을 API Server를 통해서만 통신하는가
- 02 쿠버네티스 파드는 어떻게 태어나고 사라지는가
- 03 쿠버네티스 네트워킹은 어떻게 동작하는가
- 04 쿠버네티스 스토리지는 왜 이렇게 복잡한가
- 05 Kubernetes는 자원을 어떻게 나누고 지키는가
- 06 Kubernetes 배포는 왜 이렇게 설계됐는가
- 07 쿠버네티스는 어떻게 스스로를 운영하는가
Kubernetes의 자원 관리는 단순한 숫자 설정이 아니다. Request와 Limit이라는 두 값이 스케줄러의 배치 결정, 리눅스 커널의 프로세스 제어, OOM 발생 시의 종료 순서, 그리고 노드 증감까지 연쇄적으로 결정한다. “노드 CPU가 20%인데 왜 파드가 느린가”라는 역설을 이해하려면 이 연쇄를 처음부터 따라가야 한다.
두 값의 서로 다른 역할
Request는 스케줄러의 언어다. 스케줄러는 실제 CPU 사용량을 보지 않는다. 노드에 배치된 파드들의 Request 합계만 본다. 노드 CPU가 10%밖에 사용되지 않더라도, Request 합계가 Allocatable을 채웠다면 새 파드는 Pending이 된다.
Limit은 커널의 언어다. CPU Limit은 cgroups CFS 쿼터로 변환된다.
# 200m CPU Limit → cgroups 파라미터
cpu.cfs_period_us = 100000 # 100ms 주기 (고정)
cpu.cfs_quota_us = 20000 # 주기 중 20ms만 사용 가능
100ms마다 20ms를 소진하면 나머지 80ms는 강제로 대기한다. 노드 전체에 CPU가 남아도 해당 파드는 멈춘다. 이것이 “CPU는 여유로운데 응답이 느린” 역설의 정체다.
Memory Limit은 다르게 작동한다. memory.limit_in_bytes를 초과하는 순간 OOM Killer가 컨테이너를 종료한다. CPU는 Throttling으로 살아남지만, 메모리는 초과하면 죽는다.
누가 먼저 죽는가 — QoS 클래스
메모리가 부족해질 때 어떤 파드가 먼저 종료되는지는 Request/Limit 설정 패턴으로 결정된 QoS 클래스에 달려 있다.
| QoS 클래스 | 조건 | oom_score_adj | 종료 순서 |
|---|---|---|---|
| Guaranteed | Request == Limit (전체) | -997 | 마지막 |
| Burstable | Request < Limit, 또는 일부만 설정 | 2~999 | 중간 |
| BestEffort | 아무것도 미설정 | 1000 | 가장 먼저 |
Linux OOM Killer는 oom_score가 높은 프로세스부터 종료한다. kubelet은 QoS 클래스에 따라 이 값을 미리 설정해둔다. “배치 작업은 살아있고 API 서버가 죽었다”는 사고의 원인은 종종 여기 있다. API 서버가 Burstable이고 배치 작업이 Guaranteed라면, OOM 발생 시 API 서버가 먼저 종료된다.
kubelet은 OOM Killer보다 한 발 앞서 행동한다. memory.available < 100Mi에 도달하면 BestEffort 파드부터 graceful Eviction을 시작한다. OOM Kill과 달리 SIGTERM을 먼저 보내 정상 종료 기회를 준다.
핵심 서비스는 Request == Limit으로 Guaranteed를 확보하라. 배치 작업은 BestEffort로 두면 메모리 압박 시 가장 먼저 정리된다. QoS는 명시적으로 설정하는 게 아니라 Request/Limit 패턴에서 자동으로 결정된다.
파드 수와 노드 수의 자동 조정
자원 할당이 잘 설계됐다면, 그 다음은 트래픽에 맞게 파드 수와 노드 수를 조정하는 일이다.
HPA는 파드 수를 조정한다. 핵심 공식은 하나다.
현재 파드 2개가 평균 CPU 80%를 쓰고 목표가 50%라면, ceil(2 × 1.6) = 4개로 늘린다. CPU Utilization은 Request 대비 비율이므로 CPU Request가 반드시 설정되어 있어야 한다. HPA가 동작하지 않을 때 kubectl get hpa에서 TARGETS가 <unknown>이면 metrics-server가 없거나 CPU Request가 빠진 것이다.
스케일다운에는 기본 5분 안정화 윈도우가 있다. 메트릭이 순간적으로 떨어졌다 다시 올라오는 flapping을 막기 위해, 최근 5분간의 최댓값 기준으로 스케일다운 여부를 결정한다.
VPA는 파드 수가 아니라 Request/Limit 값 자체를 조정한다. Recommender가 과거 사용량의 95th percentile로 CPU Request를, 90th percentile로 Memory Request를 권고한다. 처음 Request를 설정할 때는 updateMode: Off로 2주 정도 권고값을 수집한 뒤 수동으로 반영하는 것이 가장 안전한 접근이다.
CPU 기반 HPA와 VPA Auto 모드를 같은 Deployment에 동시 사용하면 파드가 불안정해진다. HPA가 파드 수를 늘리면 VPA Updater가 권고값으로 파드를 재생성하고, 이 사이클이 반복된다. VPA를 쓰면서 수평 스케일링도 필요하다면 HPA는 RPS 같은 커스텀 메트릭 기반으로 구성하고 VPA는 Off/Initial 모드로 제한하라.
**클러스터 오토스케일러(CA)**는 노드 수를 조정한다. Pending 파드(Insufficient CPU/Memory)를 감지하면 노드 그룹 시뮬레이션을 거쳐 클라우드 API로 VM을 추가한다. 총 소요 시간은 2~5분이다. HPA가 파드를 먼저 늘리고, 그 파드들이 Pending 상태가 되면 CA가 노드를 추가하는 순서로 동작한다.
스케일다운은 까다롭다. 노드 자원 사용률이 10분 이상 50% 미만을 유지해야 하고, 그 노드의 파드를 전부 다른 노드로 이동할 수 있어야 한다. 로컬 PV를 사용하는 파드, safe-to-evict: false 어노테이션이 붙은 파드, PDB를 위반하는 파드가 하나라도 있으면 그 노드는 스케일다운 대상에서 제외된다.
# 스케일다운 안 되는 노드 진단
kubectl get pods -A --field-selector spec.nodeName=<node-name>
kubectl describe node <node-name> | grep -i scale-down
트레이드오프
CPU Limit은 역설적이다. 설정하면 노드에 여유가 있어도 Throttling이 걸린다. 설정 안 하면 파드 하나가 노드 CPU를 독식할 수 있다. 실무에서 권장되는 절충점은 CPU Limit을 넉넉하게 설정하거나 아예 생략하되, ResourceQuota로 네임스페이스 전체 CPU 상한을 두는 방식이다. Memory Limit은 반드시 설정해야 한다 — 설정하지 않으면 메모리 누수 파드가 노드 전체를 고갈시킨다.
정리
- Request는 Scheduler의 배치 기준이자 QoS 클래스 결정 요소다. 실제 사용량이 아닌 이 값의 합계로 노드의 “남은 공간”이 계산된다.
- CPU Limit은 cgroups CFS 쿼터로, Memory Limit은 OOM Killer의 하드 경계로 구현된다. 초과의 결과가 다르다 — CPU는 멈추고, 메모리는 종료된다.
- QoS 클래스는 메모리 압박 시 종료 순서를 결정한다. 핵심 서비스는 Guaranteed, 배치 작업은 BestEffort가 원칙이다.
- HPA는 파드 수, VPA는 Request/Limit 값, CA는 노드 수를 각각 조정한다. 세 계층이 순서대로 반응하도록 설계해야 한다.
다음 글에서는 Deployment 롤링 업데이트가 파드 교체 과정에서 이 자원 설정과 어떻게 상호작용하는지, 그리고 maxSurge와 maxUnavailable이 실제로 무엇을 제어하는지 추적한다.