“Node CPU가 아직 남아 보이는데 왜 Pod가 Pending이지?”는 Kubernetes를 운영하며 자주 만나는 질문이다. 대개 원인은 현재 CPU 사용률이 아니라, 다른 Pod가 이미 요청해 둔 자원의 합에 있다.
resources.requests와 resources.limits는 둘 다 CPU와 메모리 값을 적지만 역할은 다르다.
| 설정 | 주로 관여하는 컴포넌트 | 뜻 |
|---|---|---|
requests | kube-scheduler | 이 Pod를 배치할 때 확보해야 하는 최소 자원 |
limits | kubelet / container runtime | 실행 중 컨테이너가 넘지 못하게 할 상한 |
공식 Resource Management for Pods and Containers 문서도 scheduler는 request로 Node를 고르고, kubelet은 실행 중인 컨테이너에 CPU와 메모리 limit을 적용한다고 설명한다.
request로 Pod가 들어갈 자리를 계산한다
Node마다 Pod에 나눠 줄 수 있는 allocatable CPU와 메모리가 있다. scheduler는 이미 배치된 컨테이너의 request와 새 Pod의 request를 더한 뒤, 그 값이 Node 용량 안에 들어오는지 확인한다.
Node allocatable CPU: 4 cores
기존 Pod들의 CPU request 합: 3.7 cores
새 Pod의 CPU request: 500m
3.7 + 0.5 > 4.0 → 이 Node는 후보에서 제외
이때 실제 CPU 사용률이 20%여도 결과는 바뀌지 않는다. scheduler는 순간 사용량이 아니라 선언된 예산을 기준으로 안정적으로 배치한다. 컨테이너가 request보다 더 쓸 수 있는 상황도 있으므로, 낮은 사용률만 보고 request를 과하게 줄이는 것도 위험하다.
API와 배치 Job이 같은 Node를 쓴다면
thumbnail-api는 평소 CPU 사용량이 낮지만 트래픽이 들어오면 즉시 처리할 여유가 필요하다. 반면 image-converter Job은 몇 분 동안 CPU를 집중적으로 사용해도 된다. 둘을 같은 일반 Node pool에서 돌린다면 최소한의 실행 예산을 명시해야 한다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: thumbnail-api
spec:
replicas: 3
selector:
matchLabels:
app.kubernetes.io/name: thumbnail-api
template:
metadata:
labels:
app.kubernetes.io/name: thumbnail-api
spec:
containers:
- name: api
image: example.registry/thumbnail-api:1.0
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: "1"
memory: 768Mi
- name: metrics-sidecar
image: example.registry/metrics-sidecar:1.0
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 200m
memory: 256Mi
스케줄러가 계산하는 이 Pod의 CPU request는 600m, 메모리 request는 640Mi다. Pod 안에 컨테이너가 여러 개 있으면 각 컨테이너의 request와 limit을 더해 Pod가 필요한 전체 자원을 구한다.
Job도 같은 방식이다.
apiVersion: batch/v1
kind: Job
metadata:
name: image-converter
spec:
template:
spec:
restartPolicy: Never
containers:
- name: converter
image: example.registry/image-converter:1.0
resources:
requests:
cpu: "1"
memory: 1Gi
limits:
cpu: "2"
memory: 2Gi
이 Job Pod는 Node에서 CPU 1코어와 메모리 1GiB를 확보할 수 있을 때만 배치된다. CPU limit인 2코어는 스케줄러가 예약하는 값이 아니라 실행 중 허용하는 상한이다.
CPU limit과 메모리 limit 비교하기
CPU limit을 넘기려는 컨테이너는 사용 속도가 제한되어 느려질 수 있지만, 일반적으로 바로 종료되지는 않는다. 반면 메모리는 필요한 양을 확보하지 못하면 컨테이너가 OOM(메모리 부족)으로 종료될 수 있다.
그래서 운영에서는 보통 다음을 분리해 생각한다.
| 관찰한 현상 | 먼저 의심할 것 |
|---|---|
Pod가 Pending, Insufficient cpu | request가 Node의 남은 예약 용량보다 큰지 |
| 응답 시간이 주기적으로 튐 | CPU limit으로 throttling이 생기는지 |
컨테이너가 OOMKilled | 메모리 limit이 최대 사용량보다 낮은지, 메모리 누수가 있는지 |
| Node는 비어 보이는데 새 Pod가 안 올라감 | 다른 Pod들의 request 합, Node allocatable, taint/affinity |
kubectl top은 실제 사용량을 볼 때 유용하지만 scheduler의 결정을 그대로 설명해 주지는 않는다. 스케줄링 문제는 kubectl describe pod <pod-name> 이벤트와 kubectl describe node <node-name>의 Allocated resources를 함께 봐야 한다.
request를 너무 작게 잡으면 어떻게 될까?
request를 실제 필요량보다 작게 잡으면 스케줄러는 많은 Pod를 한 Node에 넣을 수 있다. 처음에는 Node 활용률이 좋아 보인다. 하지만 트래픽이 함께 늘면 여러 Pod가 동시에 CPU와 메모리를 더 사용하면서 응답이 느려지거나 Node의 메모리가 부족해질 수 있다.
반대로 request를 무조건 크게 잡아도 문제다. 실제로는 잘 쓰지 않는 자리를 예약하게 되어 Pod가 Pending에 머물 수 있고, 자동 확장 기능이 불필요하게 Node를 늘릴 수도 있다.
좋은 출발점은 아래와 같다.
- 안정적으로 필요한 사용량과 피크 사용량을 모니터링으로 나눈다.
- request는 서비스가 평소 안정적으로 동작하는 데 필요한 수준으로 둔다.
- limit은 과도한 사용을 막는 안전장치로 두되, 최대 메모리 사용량보다 낮게 잡지 않는다.
- 배치 Job은 병렬도와 request를 함께 조정한다. Job 하나는 작아도 수십 개가 동시에 실행되면 합계가 커진다.
QoS 등급은 설정에 따라 정해진다
request와 limit 설정에 따라 Pod에는 Guaranteed, Burstable, BestEffort QoS 등급이 붙는다. 이 등급은 Node 자원이 부족해 Pod를 내보낼 때 우선순위에 영향을 줄 수 있다. 하지만 모든 Pod를 Guaranteed로 만드는 것이 목표는 아니다. 서비스에 필요한 request와 limit을 먼저 정하면 QoS 등급은 그 결과로 정해진다.
QoS의 정확한 조건은 Pod Quality of Service Classes 공식 문서에서 확인할 수 있다.
핵심 정리
자원 설정은 단순한 성능 튜닝이 아니라 스케줄링 정책이다.
request는 scheduler에게 필요한 자리 크기를 알려 준다.limit은 실행 중 한 컨테이너가 쓸 수 있는 상한을 정한다.- Pod의 request는 컨테이너 request의 합으로 생각한다.
Pending은 현재 사용률보다 request와allocatable의 조합으로 먼저 설명해야 한다.
다음 글에서는 자원이 정말 부족할 때 어떤 Pod를 먼저 대기열에 올리고, 언제 실행 중인 Pod를 비울 수 있는지 PriorityClass와 preemption으로 이어서 살펴본다.