신규 배치 파드 하나가 노드 메모리를 모두 사용하면서 같은 노드의 API 파드까지 느려졌습니다. requests와 limits를 설정하지 않으면 스케줄러도 용량을 예측할 수 없고 kubelet도 보호선을 그을 수 없습니다. 리소스 제한은 멀티테넌트 클러스터의 기본 안전장치입니다.
프로덕션 클러스터에서 새벽 2시 알람이 울렸습니다. 특정 노드의 메모리 사용률이 98%를 넘어서며 여러 파드가 동시에 죽기 시작한 겁니다. 원인을 파보니 며칠 전 배포한 데이터 처리 파드에 메모리 limits가 설정되어 있지 않았고, 배치 작업 중 메모리 누수가 쌓이면서 노드 전체를 잠식했습니다. 이 패턴은 Kubernetes 초기 운영 팀이 가장 자주 겪는 인시던트 유형입니다. requests와 limits를 이해하면 이런 상황을 사전에 차단할 수 있습니다.
Kubernetes의 리소스 관리 모델은 단순합니다. requests는 "이 파드를 스케줄링할 노드에 최소한 이만큼은 남아있어야 한다"는 예약량이고, limits는 "이 이상 사용하면 강제 종료한다"는 상한선입니다. 이 두 값의 비율이 파드의 QoS(Quality of Service) 클래스를 결정하고, 노드가 리소스 부족에 빠졌을 때 어떤 파드가 먼저 종료되는지를 결정합니다. 실무에서 이를 제대로 설정하지 않은 클러스터는 언제 터질지 모르는 시한폭탄과 같습니다.
- 1CPU requests vs limits 동작 원리 (throttling vs OOM)
- 2메모리 requests vs limits 차이점
- 3QoS 클래스 3종: Guaranteed, Burstable, BestEffort
- 4LimitRange로 네임스페이스 기본값 강제
- 5ResourceQuota로 팀별 리소스 할당
- 6실무 limits 설정 가이드라인
kubectl get nodes -o custom-columns='NAME:.metadata.name,CPU:.status.capacity.cpu,MEM:.status.capacity.memory'kubectl create namespace resource-demokubectl top nodes 2>&1 | head -3kubectl top pods -A 2>/dev/null | head -10requests와 limits: 스케줄러와 kubelet의 역할 분담
배치 작업 파드 하나가 노드 메모리를 모두 소진하면서 같은 노드에 있던 API 서버 파드들이 연이어 OOMKilled됐습니다. limits가 없었기 때문입니다. 반대로 requests를 너무 높게 설정하면 실제로는 여유가 있는 노드인데 파드가 Pending 상태로 스케줄이 안 됩니다. requests와 limits는 Kubernetes가 파드를 배치하고 실행 중에 제어하는 두 단계의 안전장치입니다. 이 두 값의 관계를 이해해야 클러스터 자원을 낭비하지 않으면서도 이웃 파드를 보호하는 올바른 설정을 할 수 있습니다.
requests와 limits는 각각 다른 Kubernetes 컴포넌트가 사용합니다. requests는 스케줄러(kube-scheduler)가 파드를 어느 노드에 배치할지 결정할 때 사용하고, limits는 kubelet이 실행 중인 컨테이너를 제어할 때 사용합니다.

CPU와 메모리는 동작 방식이 다릅니다.
- CPU limits 초과: 컨테이너는 죽지 않습니다. CPU throttling이 걸려 속도가 느려집니다.
- 메모리 limits 초과: 컨테이너가 즉시 OOMKilled(Out of Memory Killed, 종료 코드 137)됩니다.
# resource-demo.yaml
apiVersion: v1
kind: Pod
metadata:
name: resource-demo
namespace: resource-demo
spec:
containers:
- name: app
image: nginx:1.25
resources:
requests:
memory: "128Mi" # 스케줄러: 이 노드에 128MiB 여유가 있어야 함
cpu: "250m" # 스케줄러: 0.25 코어(250 밀리코어) 여유 필요
limits:
memory: "256Mi" # kubelet: 초과 시 OOMKilled
cpu: "500m" # kubelet: 초과 시 CPU throttling (죽이지 않음)
CPU 단위 m은 밀리코어입니다. 1000m = 1 CPU 코어. 250m은 1코어의 25%를 의미합니다.
# 파드 생성 및 리소스 설정 확인
kubectl apply -f resource-demo.yaml
kubectl describe pod resource-demo -n resource-demo | grep -A 10 "Limits\|Requests"
# Limits:
# cpu: 500m
# memory: 256Mi
# Requests:
# cpu: 250m
# memory: 128Mi
# 실제 사용량 모니터링 (metrics-server 필요)
kubectl top pod resource-demo -n resource-demo
# NAME CPU(cores) MEMORY(bytes)
# resource-demo 3m 8Mi
- NAME—조회 대상 리소스 이름이 예상한 대상과 일치하는지 확인합니다.
- STATUS/READY—Running, Ready, Available처럼 정상 상태를 나타내는 필드가 있는지 봅니다.
- RESTARTS/EVENTS—재시작 횟수나 Warning 이벤트가 증가하지 않는지 확인합니다.
QoS 클래스: 메모리 부족 시 누가 먼저 죽는가
Kubernetes는 노드 메모리가 부족해질 때 파드를 evict(강제 퇴거)합니다. 이때 QoS 클래스에 따라 우선순위가 결정됩니다. BestEffort가 가장 먼저, Guaranteed가 가장 마지막으로 종료됩니다.

# QoS: Guaranteed — requests == limits (모든 컨테이너, CPU+메모리 전부)
resources:
requests:
memory: "256Mi"
cpu: "500m"
limits:
memory: "256Mi" # requests와 동일
cpu: "500m" # requests와 동일
---
# QoS: Burstable — requests < limits, 또는 일부 컨테이너만 설정
resources:
requests:
memory: "128Mi"
cpu: "250m"
limits:
memory: "512Mi" # requests와 다름
cpu: "1000m"
---
# QoS: BestEffort — requests, limits 모두 미설정
# (설정하지 않으면 자동으로 BestEffort)
resources: {}
# 파드의 QoS 클래스 확인
kubectl get pod resource-demo -n resource-demo \
-o jsonpath='{.status.qosClass}'
# Burstable
# 모든 파드의 QoS 클래스 일괄 확인
kubectl get pods -A \
-o custom-columns='NAMESPACE:.metadata.namespace,NAME:.metadata.name,QOS:.status.qosClass'
실무 가이드:
- DB, 메시지 큐, 결제 서비스 → Guaranteed (안정성 최우선)
- API 서버, 백엔드 서비스 → Burstable (평소에는 낮게, 트래픽 몰릴 때 버스트)
- 배치 작업, 개발 환경 → BestEffort (OK, 단 limits는 꼭 설정)
LimitRange: 네임스페이스 기본값 강제
팀원이 resources를 빠뜨리고 배포할 때를 대비해 LimitRange로 기본값과 허용 범위를 강제할 수 있습니다. resources를 설정하지 않은 파드에는 자동으로 default 값이 주입됩니다.
# limitrange.yaml
apiVersion: v1
kind: LimitRange
metadata:
name: default-limits
namespace: resource-demo
spec:
limits:
- type: Container
default: # limits 미설정 시 자동 주입
cpu: "500m"
memory: "256Mi"
defaultRequest: # requests 미설정 시 자동 주입
cpu: "100m"
memory: "128Mi"
max: # 이 값 이상은 허용 안 함
cpu: "2"
memory: "2Gi"
min: # 이 값 미만은 허용 안 함
cpu: "50m"
memory: "64Mi"
kubectl apply -f limitrange.yaml
# 검증: resources 없이 파드 생성
kubectl run test-default --image=nginx:1.25 -n resource-demo
# 자동으로 default 값이 주입됐는지 확인
kubectl describe pod test-default -n resource-demo | grep -A 8 "Limits\|Requests"
# Limits:
# cpu: 500m ← LimitRange에서 주입됨
# memory: 256Mi ← LimitRange에서 주입됨
kubectl delete pod test-default -n resource-demo
실습: 메모리 limits 효과 직접 확인
실제로 메모리 limits를 초과하면 어떤 일이 발생하는지 확인합니다.
# 메모리를 급격히 소모하는 파드 생성 (limits: 64Mi)
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: memory-stress
namespace: resource-demo
spec:
containers:
- name: stress
image: polinux/stress
args: ["--vm", "1", "--vm-bytes", "128M", "--vm-hang", "1"]
resources:
requests:
memory: "64Mi"
limits:
memory: "64Mi" # 128MB 요청하지만 64MB만 허용
EOF
# 파드 상태 관찰 (OOMKilled 대기)
kubectl get pod memory-stress -n resource-demo -w
# NAME READY STATUS RESTARTS AGE
# memory-stress 0/1 OOMKilled 0 5s
# memory-stress 0/1 CrashLoopBackOff 1 10s
# 종료 이유 확인
kubectl describe pod memory-stress -n resource-demo | grep -A 5 "Last State"
# Last State: Terminated
# Reason: OOMKilled
# Exit Code: 137
# 정리
kubectl delete pod memory-stress -n resource-demo
실습: ResourceQuota로 네임스페이스 총량 제한
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: ResourceQuota
metadata:
name: team-quota
namespace: resource-demo
spec:
hard:
requests.cpu: "2"
requests.memory: "2Gi"
limits.cpu: "4"
limits.memory: "4Gi"
pods: "10"
EOF
# 현재 사용량 vs 할당량 확인
kubectl describe resourcequota team-quota -n resource-demo
# Name: team-quota
# Namespace: resource-demo
# Resource Used Hard
# -------- ---- ----
# limits.cpu 500m 4
# limits.memory 256Mi 4Gi
# pods 1 10
# requests.cpu 100m 2
# requests.memory 128Mi 2Gi
신규 데이터 집계 서비스를 배포한 후 처음 며칠은 정상 동작했습니다. 그런데 시간이 지나면서 파드가 주기적으로 재시작되고, 재시작 간격이 점점 짧아지더니 결국 노드 전체가 느려지기 시작했습니다. kubectl get pods를 보면 RESTARTS 숫자가 비정상적으로 높습니다.
# 증상 확인
kubectl get pods -n production
# NAME READY STATUS RESTARTS AGE
# data-aggregator 0/1 CrashLoopBackOff 47 6h
# 1단계: 종료 이유 파악
kubectl describe pod data-aggregator -n production | grep -A 10 "Last State\|Reason\|Exit Code"
# Last State: Terminated
# Reason: OOMKilled ← 핵심 단서
# Exit Code: 137
# Started: Fri, 16 May 2026 03:14:22
# Finished: Fri, 16 May 2026 03:19:57
# 2단계: 현재 리소스 설정 확인
kubectl get pod data-aggregator -n production \
-o jsonpath='{.spec.containers[0].resources}' | python3 -m json.tool
# {} ← resources가 아예 없음!
# 3단계: 파드가 실제로 얼마나 쓰고 있었는지 (이전 로그)
kubectl logs data-aggregator -n production --previous 2>&1 | tail -20
# 4단계: 동종 파드가 있다면 현재 메모리 사용량 측정
kubectl top pod -l app=data-aggregator -n production
# NAME CPU(cores) MEMORY(bytes)
# data-aggregator-xyz-abc 15m 487Mi
# 5단계: 안전한 limits 계산 (측정값 × 1.5 여유)
# 측정값 487Mi → limits: 768Mi, requests: 256Mi 로 설정
# 6단계: Deployment 수정
kubectl set resources deployment/data-aggregator \
-n production \
--requests=cpu=100m,memory=256Mi \
--limits=cpu=500m,memory=768Mi
# 7단계: 롤아웃 확인
kubectl rollout status deployment/data-aggregator -n production
# 8단계: 이후 안정성 모니터링
kubectl top pod -l app=data-aggregator -n production --watch
근본 원인: resources 블록 자체가 없어 QoS 클래스가 BestEffort였습니다. 메모리 누수가 있는 상황에서 limits가 없으니 프로세스는 계속 메모리를 늘렸고, 결국 노드의 OOM killer가 컨테이너를 강제 종료했습니다.
예방: 모든 Deployment에 resources 블록을 필수로 작성하도록 팀 컨벤션을 정하고, LimitRange로 미설정 시 기본값이 주입되도록 네임스페이스를 설정해두세요. CI 파이프라인에서 kubectl apply --dry-run=server로 LimitRange 위반 여부를 사전 검사할 수도 있습니다.
시나리오: 신규 API 서비스의 리소스 설정값 결정
스타트업의 첫 번째 Kubernetes 마이그레이션을 맡았습니다. VM에서 잘 돌던 Node.js API 서버를 K8s로 옮기는데, resources를 어떻게 설정해야 할지 모르겠습니다.
# 1단계: VM에서 현재 사용량 측정 (이전 환경 참고)
# VM에서: top, htop, free -m 등으로 1주일치 평균 측정
# 측정 결과: 평균 CPU 0.15코어, 평균 메모리 180MB, 피크 메모리 320MB
# 2단계: 초기 값 보수적으로 설정하고 배포
cat <<EOF > api-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server
namespace: production
spec:
replicas: 2
selector:
matchLabels:
app: api-server
template:
metadata:
labels:
app: api-server
spec:
containers:
- name: api
image: myapp/api:v1.0.0
ports:
- containerPort: 3000
resources:
requests:
cpu: "200m" # 평균 0.15코어 + 여유
memory: "256Mi" # 평균 180MB 올림
limits:
cpu: "800m" # 버스트 허용 (Burstable QoS)
memory: "512Mi" # 피크 320MB + 안전 여유
EOF
kubectl apply -f api-deployment.yaml
# 3단계: 1주일 후 실제 사용량 기반으로 튜닝
kubectl top pods -l app=api-server -n production
# NAME CPU(cores) MEMORY(bytes)
# api-server-xxx-aaa 45m 198Mi
# api-server-xxx-bbb 52m 205Mi
# CPU가 요청의 25%밖에 안 쓰므로 requests 하향 조정
kubectl set resources deployment/api-server \
-n production \
--requests=cpu=100m,memory=256Mi \
--limits=cpu=500m,memory=512Mi
# 4단계: HPA 설정을 위한 기준선 완성
# (다음 모듈에서 HPA 추가 예정)
실무 포인트: requests는 측정값 기반으로, limits는 "이게 넘어가면 뭔가 잘못된 것"이라는 안전망 개념으로 설정합니다. 처음부터 완벽한 값을 찾으려 하지 말고, 배포 후 kubectl top으로 1-2주 모니터링해 실측값 기반으로 조정하는 것이 현실적입니다.
핵심 요약
| 개념 | 역할 | 초과 시 동작 |
|---|---|---|
| CPU requests | 스케줄링 기준 | — |
| CPU limits | 실행 중 제어 | throttling (느려짐, 종료 없음) |
| Memory requests | 스케줄링 기준 | — |
| Memory limits | 실행 중 제어 | OOMKilled (종료 코드 137) |
| LimitRange | 네임스페이스 기본값/범위 | 초과 시 파드 거부 |
| ResourceQuota | 네임스페이스 총량 제한 | 초과 시 파드 거부 |
| QoS 클래스 | 조건 | Eviction 우선순위 |
|---|---|---|
| Guaranteed | requests == limits (전 컨테이너) | 마지막 |
| Burstable | requests < limits (일부라도 설정) | 중간 |
| BestEffort | 설정 없음 | 가장 먼저 |