infra
Platform

모듈 맵

[Kubernetes] requests와 limits 적정 값 계산과 CPU 스로틀링 대처

0 / 29 완료

펼치기
0 / 29 완료0%

Kubernetes · 11 / 29

[Kubernetes] requests와 limits 적정 값 계산과 CPU 스로틀링 대처

CPU·메모리 requests/limits와 QoS 클래스를 이해하고 OOMKilled를 예방합니다

🚨INCIDENT ALERT
HIGH

신규 배치 파드 하나가 노드 메모리를 모두 사용하면서 같은 노드의 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-demo
metrics-server 설치 여부 확인
kubectl top nodes 2>&1 | head -3
현재 파드 리소스 사용량 확인
kubectl top pods -A 2>/dev/null | head -10
💡개념

requests와 limits: 스케줄러와 kubelet의 역할 분담

배치 작업 파드 하나가 노드 메모리를 모두 소진하면서 같은 노드에 있던 API 서버 파드들이 연이어 OOMKilled됐습니다. limits가 없었기 때문입니다. 반대로 requests를 너무 높게 설정하면 실제로는 여유가 있는 노드인데 파드가 Pending 상태로 스케줄이 안 됩니다. requests와 limits는 Kubernetes가 파드를 배치하고 실행 중에 제어하는 두 단계의 안전장치입니다. 이 두 값의 관계를 이해해야 클러스터 자원을 낭비하지 않으면서도 이웃 파드를 보호하는 올바른 설정을 할 수 있습니다.

requests와 limits는 각각 다른 Kubernetes 컴포넌트가 사용합니다. requests는 스케줄러(kube-scheduler)가 파드를 어느 노드에 배치할지 결정할 때 사용하고, limits는 kubelet이 실행 중인 컨테이너를 제어할 때 사용합니다.

requests와 limits: 스케줄러와 kubelet의 역할 분담

CPU와 메모리는 동작 방식이 다릅니다.

  • CPU limits 초과: 컨테이너는 죽지 않습니다. CPU throttling이 걸려 속도가 느려집니다.
  • 메모리 limits 초과: 컨테이너가 즉시 OOMKilled(Out of Memory Killed, 종료 코드 137)됩니다.
YAML
# 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%를 의미합니다.

Kubernetes
# 파드 생성 및 리소스 설정 확인
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/READYRunning, Ready, Available처럼 정상 상태를 나타내는 필드가 있는지 봅니다.
  • RESTARTS/EVENTS재시작 횟수나 Warning 이벤트가 증가하지 않는지 확인합니다.
💡개념

QoS 클래스: 메모리 부족 시 누가 먼저 죽는가

Kubernetes는 노드 메모리가 부족해질 때 파드를 evict(강제 퇴거)합니다. 이때 QoS 클래스에 따라 우선순위가 결정됩니다. BestEffort가 가장 먼저, Guaranteed가 가장 마지막으로 종료됩니다.

QoS 클래스: 메모리 부족 시 누가 먼저 죽는가

YAML
# 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: {}
Kubernetes
# 파드의 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 값이 주입됩니다.

YAML
# 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"
Kubernetes
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 숫자가 비정상적으로 높습니다.

Kubernetes
# 증상 확인
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 우선순위
Guaranteedrequests == limits (전 컨테이너)마지막
Burstablerequests < limits (일부라도 설정)중간
BestEffort설정 없음가장 먼저

지식 확인

퀴즈 — 3문제

Q1

파드에 requests만 설정하고 limits를 설정하지 않으면 어떤 일이 발생할 수 있는가?

Q2

QoS 클래스가 Guaranteed인 파드의 조건으로 올바른 것은?

Q3

컨테이너가 OOMKilled 상태로 반복 재시작될 때 올바른 초기 진단 명령어는?

0 / 3 답변

🧪 실습으로 확인하기

K8s 기초 — Pod/Deployment/Service 생성

초급

kubectl로 nginx Pod를 생성하고 Deployment와 Service를 차례로 만들어 클러스터 외부에서 접근 가능한 상태까지 구성한다. K8s 3대 리소스의 역할과 관계를 직접 손으로 익힌다.

40📋 5단계💻 직접 환경
실습 시작하기 →

이것도 배워보세요

kubernetes중급 · 55
[Kubernetes] Liveness, Readiness, Startup Probe 헬스 체크 설정
Kubernetes 트랙 계속
docker입문 · 30
[Docker] 백엔드 개발자에게 Docker와 컨테이너 가상화가 필수인 이유
Docker 트랙 시작점