본문 바로가기
Infra2026년 8월 23일7분 읽기

쿠버네티스 requests·limits — OOMKilled와 CPU 스로틀링 튜닝

YS
김영삼
조회 4
쿠버네티스 requests·limits — OOMKilled와 CPU 스로틀링 튜닝

쿠버네티스에서 requests는 스케줄러가 파드를 배치할 때 예약하는 최소 자원이고, limits는 컨테이너가 넘을 수 없는 상한이다. 이 둘을 잘못 잡으면 두 가지 병이 생긴다 — 메모리가 limit을 넘으면 OOMKilled로 컨테이너가 강제 종료되고, CPU가 limit에 닿으면 눈에 안 보이게 스로틀링(throttling)돼 응답이 느려진다. 오늘은 이 두 증상을 어떻게 진단하고 튜닝하는지 다룬다.

이게 왜 어렵냐면, CPU와 메모리가 완전히 다르게 동작하기 때문이다. 그걸 모르면 "limit을 넉넉히 주면 안전하겠지" 같은 잘못된 직관으로 오히려 문제를 만든다. 나도 그랬다.

CPU와 메모리는 규칙이 다르다

가장 먼저 머리에 박아야 할 것. 메모리는 압축 불가능(incompressible), CPU는 압축 가능(compressible) 자원이다.

메모리는 한번 쓰면 뺏을 수 없다. 그래서 limit을 넘으면 커널이 그 컨테이너를 죽인다. 이게 OOMKilled다. 반면 CPU는 순간적으로 뺏어도 된다 — 잠깐 느려질 뿐 죽지 않는다. 그래서 CPU가 limit에 닿으면 죽이는 대신 스로틀링, 즉 "이번 주기엔 여기까지만" 하고 강제로 대기시킨다.

이 차이에서 실전 원칙 하나가 나온다. 메모리는 requests와 limits를 같게 두고, CPU는 requests만 두고 limit은 웬만하면 걸지 마라.

OOMKilled 진단과 대응

파드가 자꾸 죽는데 앱 로그엔 아무것도 없다면 십중팔구 OOMKilled다. 확인은 이렇게 한다.

kubectl describe pod mypod | grep -A5 "Last State"
# Last State:     Terminated
#   Reason:       OOMKilled
#   Exit Code:    137

Exit Code 137은 128 + 9(SIGKILL)로, 커널의 OOM killer가 개입했다는 신호다. 여기서 흔한 실수는 limit을 무작정 2배로 올리는 것이다. 진짜 원인이 메모리 누수라면, limit을 올려봤자 죽는 시점만 미룰 뿐이다. 먼저 실제 사용량 추이를 봐야 한다.

kubectl top pod mypod --containers
# 시계열이 필요하면 Prometheus에서:
# container_memory_working_set_bytes

working set이 우상향으로 계속 오르면 누수, 특정 요청에서 스파이크가 튀면 그 요청이 범인이다. 나는 이미지 리사이징 API에서 큰 파일이 들어올 때만 순간적으로 메모리가 3배 튀는 걸 이 그래프로 잡았다. limit을 올리는 대신 스트리밍 처리로 바꿔서 근본을 해결했다.

CPU limit이 오히려 독이 되는 이유

이건 반직관적이라 자주 놀란다. CPU limit을 걸면 안전할 것 같지만, 실제로는 throttling이라는 숨은 지연을 만든다. 리눅스 CFS는 100ms 단위 주기로 CPU 할당량을 계산하는데, limit이 낮으면 주기 초반에 할당량을 다 써버리고 남은 시간 내내 강제로 멈춘다. p99 레이턴시가 이유 없이 튀는 서비스, 열에 아홉은 여기가 원인이다.

# 스로틀링 발생 여부 확인 (Prometheus)
rate(container_cpu_cfs_throttled_periods_total[5m])
  / rate(container_cpu_cfs_periods_total[5m])
# 이 비율이 높으면 CPU limit 때문에 손해 보는 중

그래서 요즘 나는 CPU는 requests만 정확히 잡고 limit은 아예 안 거는 경우가 많다. requests가 스케줄링과 최소 보장을 담당하니, 노드에 여유가 있으면 파드는 requests 이상으로 CPU를 자유롭게 쓴다. 여유가 없으면 requests 비율대로 공정하게 나눠 갖는다. 이게 대부분의 워크로드에서 더 좋은 성능을 낸다.

QoS 클래스도 같이 결정된다

requests와 limits를 어떻게 잡느냐에 따라 파드의 QoS 등급이 정해지고, 이건 노드가 자원 압박을 받을 때 누가 먼저 쫓겨나는지를 결정한다.

QoS 클래스조건축출 우선순위
Guaranteedrequests=limits (CPU·메모리 둘 다)가장 나중에 쫓겨남
Burstablerequests < limits (일부만 설정)중간
BestEffortrequests·limits 둘 다 없음가장 먼저 쫓겨남

DB나 결제처럼 절대 죽으면 안 되는 워크로드는 메모리 requests와 limits를 같게 줘서 Guaranteed로 만든다. 반대로 배치 작업은 Burstable로 두고 여유 자원을 유연하게 쓰게 한다.

실전 권장 조합

resources:
  requests:
    cpu: "250m"       # 스케줄링·최소 보장의 기준
    memory: "256Mi"   # 메모리는 requests=limits 권장
  limits:
    memory: "256Mi"   # OOM 예방 위해 실측 기반으로
    # cpu limit은 의도적으로 생략 (스로틀링 회피)

숫자는 반드시 실측에서 뽑는다. 며칠간 container_memory_working_set_bytes의 최댓값에 20~30% 여유를 얹어 메모리를 정하고, CPU requests는 평상시 사용량의 중앙값 근처로 잡는다. 감으로 찍은 값은 거의 항상 틀린다. VPA(Vertical Pod Autoscaler)를 recommend 모드로 돌려 추천값을 참고하는 것도 좋은 출발점이다.

자주 묻는 질문

CPU limit을 정말 안 걸어도 괜찮나요?

대부분의 온라인 서비스에서는 안 거는 편이 레이턴시에 유리합니다. 다만 멀티테넌트 클러스터에서 특정 파드가 노드 CPU를 독점하는 걸 막아야 하거나, 비용 예측이 엄격하게 필요한 환경이라면 limit을 겁니다. 이 경우 스로틀링 지표를 반드시 모니터링하세요.

메모리 limit을 requests보다 높게 주면 안 되나요?

가능하지만 위험합니다. requests보다 많이 쓰는 상태가 되면 노드 메모리 압박 시 이 파드가 축출 대상이 됩니다. 안정성이 중요한 워크로드는 메모리 requests와 limits를 같게 두어 Guaranteed QoS로 만드는 걸 권장합니다.

OOMKilled인데 노드 전체 메모리는 여유가 있어요.

컨테이너 자체의 limit을 넘겨서 죽은 경우입니다. 노드 전체가 아니라 해당 컨테이너의 cgroup 한도가 기준입니다. limit을 올리기 전에 working set 추이로 누수인지 스파이크인지부터 판단하세요.

requests와 limits 중 스케줄링에 쓰이는 건 무엇인가요?

requests입니다. 스케줄러는 노드의 남은 requests 총량만 보고 파드를 배치하며, limits는 배치 결정에 관여하지 않습니다. 그래서 requests를 실제보다 너무 낮게 잡으면 노드에 파드가 과밀하게 몰려 자원 경합이 생깁니다.

댓글 0

아직 댓글이 없습니다.
Ctrl+Enter로 등록