앞 글에서 ConfigMap과 Secret으로 설정값 주입을 다뤘습니다.
이제 Pod에 어떤 값을 주입할지는 알겠는데, 이 Pod가 노드 자원을 얼마나 쓸 수 있는지는 아직 명시하지 않았습니다.
리소스를 지정하지 않으면, 하나의 Pod가 노드 자원을 독점해서 다른 Pod를 밀어낼 수 있습니다. 반대로 너무 작게 잡으면 OOMKilled나 CPU 스로틀링이 발생하고요.
이번 글에서는 Pod의 자원 관리에 필요한 것들 - requests, limits, QoS 클래스, Namespace 수준의 정책(LimitRange, ResourceQuota), 그리고 최근 GA된 In-Place Pod Resize까지 정리합니다.
requests와 limits
워크로드편에서 Pod를 만들 때 resources.requests와 limits를 잠깐 설정했었는데, 이게 왜 중요한지는 간략하게만 짚고 넘어갔습니다. 여기서 좀 더 자세히 다룹니다.
resources:
requests:
cpu: "500m" # 0.5 코어 보장
memory: "256Mi" # 256 MiB 보장
limits:
cpu: "1" # 최대 1 코어
memory: "512Mi" # 최대 512 MiB
requests vs limits
구분 requests limits
| 구분 | requests | limits |
| 역할 | 스케줄러가 노드 선택에 사용 | 런타임이 사용량 제한에 사용 |
| CPU 초과 시 | — | 스로틀링 (느려지지만 죽지 않음) |
| 메모리 초과 시 | — | OOMKilled (컨테이너 종료) |
CPU와 메모리의 초과 동작이 다르다는 게 중요합니다. CPU는 스로틀링만 걸리지만 메모리는 컨테이너가 죽습니다.
그래서 메모리 limits는 보수적으로, CPU limits는 여유 있게 잡는 게 일반적입니다.
CPU 단위와 메모리 단위
처음에 500m이 뭔가 헷갈렸습니다. CPU 단위는 밀리코어(millicore) 로, 1000m이 1코어입니다.
500m은 0.5코어, 100m은 0.1코어 식입니다. 소수점(0.5)으로 적어도 되지만, 500m이 더 명확하고 실수가 적습니다.
메모리는 이진 접두사를 씁니다. Mi(메비바이트)는 1024² 바이트, Gi는 1024³ 바이트입니다. M(메가바이트, 1000²)이나 G도 쓸 수 있지만, 컨테이너 메모리는 보통 Mi/Gi를 씁니다.
⚠️ 주의: 500M과 500Mi는 다릅니다(전자는 500,000,000 바이트, 후자는 524,288,000 바이트). 정확한 의도가 아니면 항상 Mi/Gi를 쓰는 게 안전합니다.
Init Container의 리소스 계산
Init Container를 쓸 때 자원 계산이 일반 컨테이너와 다릅니다. 일반 컨테이너는 모든 컨테이너의 합(sum) 으로 계산되는데, Init Container는 가장 큰 값(max) 만 잡힙니다. 어차피 순차로 실행되니 동시에 점유하지는 않으니까요.
예를 들어 Init A가 200m, Init B가 500m을 요청하고 메인 컨테이너 두 개가 각각 300m을 요청하면, Pod 전체의 effective request는 max(500m, 300m + 300m) = 600m입니다. Init 단계에서 일시적으로 큰 자원이 필요한 경우 이 점을 기억해두면 됩니다.
QoS 클래스
Kubernetes는 requests/limits 설정에 따라 Pod에 QoS(Quality of Service) 클래스를 부여합니다.
노드 리소스가 부족할 때 QoS가 낮은 Pod부터 퇴거시킵니다.
| QoS 클래스 | 조건 | 퇴거 우선순위 |
| Guaranteed | 모든 컨테이너의 requests = limits | 가장 나중 (보호) |
| Burstable | requests < limits이거나 일부만 설정 | 중간 |
| BestEffort | requests, limits 모두 미설정 | 가장 먼저 (취약) |
프로덕션 워크로드는 최소한 Burstable, 중요한 건 Guaranteed로 설정하는 게 좋습니다. BestEffort는 노드 압박 시 가장 먼저 쫓겨납니다.
# Pod의 QoS 클래스 확인
kubectl get pod <pod> -o jsonpath='{.status.qosClass}'
⚠️ 주의: requests 없이 limits만 설정하면 Kubernetes가 requests를 limits와 같은 값으로 자동 설정합니다. 의도치 않게 Guaranteed가 되어 리소스를 많이 "예약"하게 될 수 있습니다.
PriorityClass
QoS와 헷갈리기 쉬운 게 PriorityClass입니다. QoS는 노드 리소스 압박 시 퇴거 우선순위를 결정하고, PriorityClass는 스케줄링 시 우선순위와 선점(preemption) 을 결정합니다.
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: high-priority
value: 1000000
globalDefault: false
description: "중요한 워크로드용"
---
apiVersion: v1
kind: Pod
metadata:
name: critical-app
spec:
priorityClassName: high-priority
containers:
- name: app
image: myapp:v1
높은 우선순위 Pod가 스케줄될 노드가 없으면, 낮은 우선순위 Pod를 강제 퇴거시키고 자리를 차지할 수 있습니다.
시스템 컴포넌트는 미리 정의된 system-cluster-critical, system-node-critical PriorityClass를 씁니다.
LimitRange와 ResourceQuota
개별 Pod의 리소스는 requests/limits로 관리하지만, Namespace 수준에서 정책을 강제하려면 LimitRange와 ResourceQuota가 필요합니다. 둘은 역할이 다릅니다.
LimitRange — 기본값과 범위 강제
컨테이너/Pod의 기본값과 최소·최대를 설정합니다.
apiVersion: v1
kind: LimitRange
metadata:
name: defaults
namespace: dev
spec:
limits:
- type: Container
default: # limits 기본값
cpu: "500m"
memory: "256Mi"
defaultRequest: # requests 기본값
cpu: "200m"
memory: "128Mi"
max:
cpu: "2"
memory: "1Gi"
ResourceQuota — 네임스페이스 총량 제한
Namespace 전체의 총 리소스 사용량을 제한합니다.
apiVersion: v1
kind: ResourceQuota
metadata:
name: dev-quota
namespace: dev
spec:
hard:
requests.cpu: "10"
requests.memory: "20Gi"
limits.cpu: "20"
limits.memory: "40Gi"
pods: "50"
persistentvolumeclaims: "10"
리소스 외에 오브젝트 개수도 제한할 수 있어서, Pod·PVC·Service의 개수를 막거나 Secret 개수를 제한하는 식으로 활용합니다.
두 개를 조합하는 이유
ResourceQuota가 있으면 모든 Pod에 requests/limits가 필수인데, LimitRange의 기본값이 이걸 자동으로 채워주기 때문에, 둘을 조합하여 사용하는 것이 효과적입니다.
개발자가 resources를 깜빡해도 LimitRange가 기본값을 넣어주고, 전체 사용량은 ResourceQuota가 관리하는 구조입니다.
# Quota 사용량 확인
kubectl describe resourcequota dev-quota -n dev
# 노드별 리소스 사용량
kubectl top nodes
# Pod별 리소스 사용량
kubectl top pods -n dev
⚠️ 주의: kubectl top 명령은 metrics-server가 설치돼 있어야 동작합니다. 매니지드 쿠버네티스(EKS, GKE 등)는 기본 설치되어 있지만, 직접 구축한 클러스터에서는 별도 설치가 필요합니다.
In-Place Pod Resize
전통적으로 Pod의 리소스를 변경하려면 Pod를 삭제하고 다시 만들어야 했습니다. Deployment의 template을 수정하면 새 ReplicaSet이 만들어지고 롤링 업데이트가 일어나는 식이었죠. 이게 stateful 워크로드나 시작 시간이 긴 앱에는 큰 부담이었습니다.
v1.27 alpha → v1.33 beta → v1.35에서 GA된 In-Place Pod Resize는 이 문제를 해결합니다. 실행 중인 Pod의 CPU와 메모리 requests/limits를 컨테이너 재시작 없이 변경할 수 있습니다.
# kubectl로 직접 리사이즈 (resize subresource 사용)
kubectl patch pod my-app --subresource resize --patch \
'{"spec":{"containers":[{"name":"app","resources":{"limits":{"cpu":"2","memory":"1Gi"}}}]}}'
# resizePolicy로 리소스별로 재시작 필요 여부를 명시
spec:
containers:
- name: app
image: myapp:v1
resizePolicy:
- resourceName: cpu
restartPolicy: NotRequired # CPU 변경은 재시작 없이
- resourceName: memory
restartPolicy: RestartContainer # 메모리는 컨테이너 재시작
CPU는 cgroup만 조정하면 되니 거의 모든 워크로드가 재시작 없이 가능하고, 메모리는 앱에 따라 다릅니다.
JVM처럼 시작 시점에 힙 크기를 결정하는 앱은 메모리 변경 시 재시작이 필요합니다.
⚠️ 주의: In-Place Resize는 cgroup v2 환경과 비교적 최신 커널을 요구합니다. 그리고 노드에 여유 자원이 없으면 resize가 Deferred 상태로 보류됩니다.
VPA(Vertical Pod Autoscaler)도 In-Place Resize와 연동되어, 권장값에 따라 자동으로 비파괴적 리사이즈를 수행하는 방향으로 발전하고 있습니다.
어떻게 사용해야할까?
리소스 설정을 처음 잡을 때 어디서부터 시작해야 할지 막막한데, 일반적으로 이렇게 접근합니다.
- 개발 환경에서 측정 — kubectl top pods로 실제 사용량을 확인합니다.
- requests는 평상시 사용량의 1.2~1.5배 — 약간 여유를 두고 잡습니다.
- limits는 피크 사용량의 1.5~2배 — 너무 빡빡하면 OOMKilled가 자주 발생합니다.
- 메모리 limits는 보수적으로 — 죽이는 것보다 느려지는 게 낫습니다(CPU와 다름).
처음부터 정확한 값을 잡기 어려우니, 운영하면서 메트릭을 보고 조정해가는 게 현실적입니다. VPA(Vertical Pod Autoscaler) 같은 도구가 자동으로 권장값을 제시해주기도 하는데, 이건 오토스케일링 글에서 다시 다루겠습니다.
트러블슈팅
리소스 관련해서 가장 자주 만나는 두 가지 문제입니다.
OOMKilled 디버깅
# Pod 상태 확인
kubectl get pod <pod> -o jsonpath='{.status.containerStatuses[*].lastState}'
# {"terminated":{"exitCode":137,"reason":"OOMKilled",...}}
# 이전 컨테이너 로그 (재시작 직전 로그)
kubectl logs <pod> --previous
# 노드 레벨 메모리 사용량
kubectl top pod <pod> --containers
exitCode: 137은 SIGKILL을 받았다는 의미이고, reason: OOMKilled면 메모리 limits 초과입니다. 해결은 두 가지 방향입니다.
- limits를 올린다 (피크 사용량 확인 후)
- 앱의 메모리 사용을 줄인다 (메모리 누수 확인)
CPU Throttling 디버signing
CPU는 죽지 않고 느려지기만 해서 발견이 더 어렵습니다. 응답 시간이 갑자기 늘었다면 throttling을 의심해봐야 합니다.
# 컨테이너 내부에서 throttling 통계 확인
kubectl exec <pod> -- cat /sys/fs/cgroup/cpu.stat
# nr_throttled: 12345 (throttle 당한 횟수)
# throttled_usec: 67890 (총 throttle 시간)
nr_throttled이 계속 증가하면 CPU limits를 올리거나, 아예 CPU limits를 빼는 것도 검토합니다. CPU 제한 없이 requests만 설정하면 노드에 여유가 있을 때 자유롭게 burst할 수 있어요. 다만 다른 Pod와의 공정성 문제가 있으니 클러스터 정책에 따라 판단합니다.
정리
requests는 스케줄러가, limits는 런타임이 사용합니다. CPU 초과는 스로틀링, 메모리 초과는 OOMKilled라는 차이가 핵심입니다.
- QoS 클래스는 노드 압박 시 퇴거 우선순위를 결정하고,
- Namespace 수준에서는 LimitRange로 기본값을,
- ResourceQuota로 총량을 관리합니다.
- v1.35에서 GA된 In-Place Pod Resize 덕분에 이제 재시작 없이 리소스를 조정할 수도 있고요.
다음 글에서는 컨테이너가 죽어도 데이터가 사라지지 않도록 하는 영속 스토리지인 Volume, PV, PVC, StorageClass를 다루겠습니다.
'DevOps > kubernetes' 카테고리의 다른 글
| 6. ConfigMap과 Secret - 설정을 이미지에서 분리 (0) | 2026.06.13 |
|---|---|
| 5. Service와 Ingress - 네트워킹 정리 (0) | 2026.06.06 |
| 4. StatefulSet 부터 CronJob 까지 (0) | 2026.05.30 |
| 3. Pod에서 Deployment 까지 (0) | 2026.05.16 |
| 2. Kubernetes 클러스터 아키텍처 (0) | 2026.05.09 |