쿠버네티스 클러스터를 운영하다 보면 어느 날 갑자기 파드가 OOMKilled 상태로 재시작되거나, CPU 사용률은 낮은데 응답 지연이 급증하는 현상을 마주하게 됩니다. 대부분의 경우 원인은 resources.requests와 resources.limits 설정의 부재 또는 잘못된 값에 있습니다. 이 두 필드는 단순한 리소스 제한을 넘어 스케줄링 결정, QoS 클래스 부여, HPA 동작 기준, 노드 오버커밋 허용 여부 전체에 영향을 미칩니다.
쿠버네티스 리소스 requests/limits 튜닝은 “한 번 설정하고 끝”이 아닙니다. 애플리케이션 트래픽 패턴, 노드 사양, 클러스터 오버커밋 정책에 따라 지속적으로 조정해야 하는 운영 활동입니다. 이 글에서는 requests와 limits의 개념 차이부터 QoS 클래스별 동작, OOMKilled·CPU throttling 원인과 대응, HPA와의 연계, 실전 튜닝 방법까지 체계적으로 정리합니다.
쿠버네티스 requests와 limits의 차이
requests는 스케줄러가 파드를 노드에 배치할 때 사용하는 최소 보장 자원량입니다. 노드의 할당 가능 자원(allocatable)에서 모든 파드의 requests 합계가 초과하면 스케줄러는 해당 노드를 선택하지 않습니다. 반면 limits는 런타임 상한선입니다. CPU limits를 초과하면 컨테이너는 CPU throttling을 받고, 메모리 limits를 초과하면 OOM Killer에 의해 프로세스가 종료(OOMKilled)됩니다.
apiVersion: v1
kind: Pod
metadata:
name: example-app
spec:
containers:
- name: app
image: myapp:latest
resources:
requests:
cpu: "250m" # 0.25 vCPU 보장
memory: "256Mi" # 256 MiB 보장
limits:
cpu: "500m" # 0.5 vCPU 상한
memory: "512Mi" # 512 MiB 상한 초과 시 OOMKilled
CPU는 압축 가능(compressible) 자원으로, limits를 초과해도 프로세스가 죽지 않고 throttling됩니다. 메모리는 비압축(incompressible) 자원으로 limits 초과 시 즉시 프로세스가 종료됩니다. 이 차이를 이해하는 것이 튜닝의 출발점입니다.
QoS 클래스: Guaranteed, Burstable, BestEffort
쿠버네티스는 파드의 requests/limits 설정 조합에 따라 자동으로 세 가지 QoS(Quality of Service) 클래스를 부여합니다. QoS 클래스는 노드 메모리 압박(memory pressure) 상황에서 어떤 파드가 먼저 퇴거(eviction)되는지를 결정합니다.
| QoS 클래스 | 조건 | 노드 압박 시 우선순위 | 적합한 워크로드 |
|---|---|---|---|
| Guaranteed | 모든 컨테이너에 requests = limits (CPU·메모리 모두) | 가장 나중에 퇴거 | 데이터베이스, 핵심 API 서버, 레이턴시 민감 서비스 |
| Burstable | 최소 1개 컨테이너에 requests 또는 limits 설정 | 중간 (BestEffort보다 나중) | 일반 웹 서비스, 배치 작업, 버스트 트래픽 있는 앱 |
| BestEffort | requests·limits 모두 미설정 | 가장 먼저 퇴거 | 개발/테스트 환경, 손실 허용 가능 워크로드 |
프로덕션 핵심 서비스는 Guaranteed 또는 Burstable을 목표로 하고, BestEffort 파드는 프로덕션 네임스페이스에 배포하지 않는 것을 원칙으로 삼아야 합니다. kubectl get pod <name> -o jsonpath='{.status.qosClass}'로 현재 QoS 클래스를 확인할 수 있습니다.
OOMKilled와 CPU Throttling 원인 진단과 대응
OOMKilled 대응
OOMKilled는 컨테이너의 메모리 사용량이 limits.memory를 초과했을 때 커널 OOM Killer가 프로세스를 강제 종료하며 발생합니다. 쿠버네티스는 이를 감지해 파드를 재시작합니다(restartPolicy에 따라 다름).
# OOMKilled 이벤트 확인
kubectl describe pod <pod-name> | grep -A5 "Last State"
# 출력 예:
# Last State: Terminated
# Reason: OOMKilled
# Exit Code: 137
# 메모리 사용량 실시간 확인 (metrics-server 필요)
kubectl top pod <pod-name> --containers
# 시계열 메모리 사용량 (Prometheus 쿼리 예시)
# container_memory_working_set_bytes{pod="my-pod", container="app"}
- 단기 대응:
limits.memory를 현재 peak 사용량 기준 1.5~2배로 늘립니다. - 근본 대응: 실제 메모리 사용 패턴을 Prometheus + Grafana로 7일 이상 수집한 뒤 P95 ~ P99 값을 limits로 설정합니다.
- 메모리 누수 의심 시: 애플리케이션 코드와 힙 덤프를 분석해 누수 원인을 수정합니다. limits 증가만으로는 임시방편에 불과합니다.
- JVM 앱:
-XX:MaxRAMPercentage=75.0으로 JVM 힙을 컨테이너 메모리 limits의 75% 이하로 제한해 non-heap 메모리 여유를 확보합니다.
CPU Throttling 대응
CPU throttling은 컨테이너의 CPU 사용량이 limits.cpu를 초과할 때 Linux CFS(Completely Fair Scheduler) quota 메커니즘에 의해 발생합니다. 응답 지연이 늘어나지만 프로세스는 종료되지 않아 발견이 늦어지는 경우가 많습니다.
# CPU throttling 비율 확인 (Prometheus 쿼리)
rate(container_cpu_cfs_throttled_seconds_total[5m])
/ rate(container_cpu_cfs_periods_total[5m])
# 20% 이상이면 throttling이 성능에 영향을 주고 있다고 볼 수 있음
# kubectl로 CPU 사용량 확인
kubectl top pod <pod-name> --containers
- CPU limits 조정: throttling 비율이 지속적으로 20%를 넘으면 limits.cpu를 현재값의 1.5~2배로 늘립니다.
- CPU limits 제거 고려: 일부 팀은 CPU는 compressible 자원이므로 limits를 아예 설정하지 않고 requests만 두는 정책을 씁니다. 노드 오버커밋이 허용되는 환경에서 적합하지만, 단일 파드가 노드 CPU를 독점할 위험을 고려해야 합니다.
- requests와 limits 비율: CPU limits를 requests의 2~4배 이내로 유지하는 것이 일반적입니다. 너무 높은 비율은 스케줄러 예측을 어렵게 합니다.
Deployment YAML 실전 예시: 웹 API 서버
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: api-server
template:
metadata:
labels:
app: api-server
spec:
containers:
- name: api
image: myapp/api-server:1.5.2
ports:
- containerPort: 8080
resources:
requests:
cpu: "200m"
memory: "256Mi"
limits:
cpu: "800m"
memory: "512Mi"
# 라이브니스/레디니스 프로브도 함께 설정 권장
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 15
periodSeconds: 20
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
---
# HPA: CPU 사용률 기준 자동 스케일링
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: api-server-hpa
namespace: production
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: api-server
minReplicas: 3
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60 # requests 대비 60% 초과 시 스케일 아웃
HPA의 CPU 사용률 계산 기준은 limits가 아닌 requests입니다. requests가 너무 낮으면 실제 부하가 낮은데도 HPA가 과도하게 스케일 아웃합니다. 반대로 너무 높으면 실제 부하 상황에서 스케일 아웃이 늦어집니다. requests 값을 정확히 설정하는 것이 HPA 튜닝의 핵심입니다.
LimitRange와 ResourceQuota로 네임스페이스 정책 강제
개별 파드 설정에만 의존하면 requests/limits를 빠뜨리는 실수가 발생합니다. 네임스페이스 단위로 LimitRange와 ResourceQuota를 설정하면 정책을 강제할 수 있습니다.
# LimitRange: 기본값 및 최대값 정의
apiVersion: v1
kind: LimitRange
metadata:
name: default-limits
namespace: production
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"
---
# ResourceQuota: 네임스페이스 전체 합산 상한
apiVersion: v1
kind: ResourceQuota
metadata:
name: production-quota
namespace: production
spec:
hard:
requests.cpu: "20"
requests.memory: "40Gi"
limits.cpu: "40"
limits.memory: "80Gi"
pods: "50"
실전 튜닝 체크리스트
- 모든 프로덕션 컨테이너에 requests와 limits가 모두 설정되어 있는가?
- 메모리 사용량 P95 값을 기준으로 limits.memory를 설정했는가? (최소 1.2~1.5배 여유)
- CPU throttling 비율을 Prometheus로 모니터링하고 있는가?
- HPA가 있다면 requests 값이 실제 idle CPU와 부합하는가?
- JVM/Python 등 런타임이 컨테이너 메모리 limits를 인식하도록 설정되었는가?
- 네임스페이스에 LimitRange로 기본값이 설정되어 있는가?
- Vertical Pod Autoscaler(VPA) recommendation 모드를 개발 환경에서 실행해 권장값 참고 여부 검토
kubectl describe node로 노드 allocatable 대비 requests 합계를 주기적으로 확인하는가?
마무리 요약
쿠버네티스 리소스 requests/limits 튜닝의 핵심은 실측 데이터 기반 설정입니다. 초기에는 Vertical Pod Autoscaler(VPA) recommendation 모드나 kubectl top으로 baseline을 파악하고, Prometheus로 P95·P99 사용량을 수집해 점진적으로 조정합니다. OOMKilled는 limits.memory를 올리고 근본 원인을 분석하고, CPU throttling은 limits.cpu를 늘리거나 HPA로 수평 확장을 검토합니다. LimitRange와 ResourceQuota로 네임스페이스 정책을 강제하면 설정 누락으로 인한 사고를 예방할 수 있습니다.
자주 묻는 질문
Q. requests와 limits를 같게 설정하면 어떤 장점이 있나요?
A. requests = limits로 설정하면 파드가 Guaranteed QoS 클래스를 받아 노드 자원 압박 시 가장 나중에 퇴거됩니다. 또한 스케줄러가 정확한 자원 예약을 할 수 있어 노드 오버커밋 없이 안정적인 운영이 가능합니다. 단점은 자원 활용률이 낮아질 수 있다는 점입니다. 데이터베이스나 레이턴시 민감 서비스에는 Guaranteed를, 일반 웹 서비스에는 Burstable을 권장합니다.
Q. HPA 사용 시 requests를 너무 낮게 잡으면 어떤 문제가 생기나요?
A. HPA의 CPU 사용률은 실제 CPU 사용량 / requests.cpu로 계산됩니다. requests를 50m으로 잡은 컨테이너가 실제 60m을 쓰면 사용률이 120%로 계산되어 HPA가 즉시 스케일 아웃을 시도합니다. 결과적으로 불필요한 파드 증가, 자원 낭비, 잦은 스케일 이벤트로 인한 불안정이 생깁니다. requests는 실제 idle/low-traffic 상태의 CPU 사용량에 가깝게 설정하는 것이 기준입니다.
Q. Vertical Pod Autoscaler(VPA)와 HPA를 함께 쓸 수 있나요?
A. CPU·메모리 기반 HPA와 VPA를 같은 디플로이먼트에 동시에 적용하면 충돌이 발생할 수 있습니다. VPA가 requests를 변경하면 HPA의 스케일 판단 기준이 달라지기 때문입니다. 일반적으로 권장되는 패턴은 VPA를 updateMode: "Off"(권장값만 조회, 자동 적용 안 함)로 설정해 requests 조정 참고자료로만 활용하고, 실제 스케일링은 HPA에 맡기는 방식입니다. 커스텀 메트릭(RPS, 큐 길이 등) 기반 HPA와 VPA 조합은 공식적으로 지원됩니다.