홈/인프라/OOMKilled exit code 137 해결: kubectl 30초 진단·5분 복구 런북
인프라OOMKilledexit code 137

OOMKilled exit code 137 해결: kubectl 30초 진단·5분 복구 런북

Pod가 OOMKilled exit code 137로 죽었나요? kubectl describe·jsonpath·dmesg로 컨테이너 limit 초과인지 노드 메모리 압박인지 30초에 가르고, requests/limits 재산정·JVM 힙 설정·Guaranteed QoS·알람까지 복붙 명령으로 5분 복구합니다.

OOMKilled exit code 137 해결: kubectl 30초 진단·5분 복구 런북
목차
  1. 30초 진단 분기표: 내 137은 어느 갈래인가
  2. 복붙 진단 명령 모음
  3. 원인별 복구 + 재발 방지
  4. 측정해서 정하기: working set, requests·limit 산정, 누수 판별
  5. 결론: 형제 에러로 오진하지 마세요
  6. 참고: 공식 문서
  7. 자주 묻는 질문 (FAQ)

OOMKilled exit code 137 해결: kubectl 30초 진단·5분 복구 런북

kubectl describe pod 출력에서 Last State: Terminated, Reason: OOMKilled, Exit Code: 137을 보고 들어오셨다면 잘 찾아오셨습니다. 개념 설명은 뒤로 미루고, 지금 죽은 Pod부터 살립니다.

exit code 137은 128 + 9, 즉 SIGKILL을 받고 강제 종료됐다는 뜻입니다. 누가 죽였느냐가 핵심인데, 범인은 보통 셋 중 하나입니다. 컨테이너가 자기 limit을 넘겼거나(OOMKilled), 노드 전체가 메모리 부족이라 쫓겨났거나(Evicted), 사람이 직접 죽였거나(SIGKILL). 30초 안에 해야 할 단 한 가지는, 내 137이 이 셋 중 어느 분기인지 가르는 것입니다. 분기를 잘못 잡으면 limit만 올리다 노드 압박을 못 보거나, 반대로 멀쩡한 limit을 건드립니다.

30초 진단 분기표: 내 137은 어느 갈래인가

아래 표에서 본인 증상을 찾고, 바로 다음 섹션의 해당 명령으로 가세요.

증상 (describe / events에서 보이는 문구)진단다음 행동
Exit Code: 137 + Reason: OOMKilled컨테이너 메모리 limit 초과진단 명령 A·B·C → 복구 1·2
Reason: Evicted, message: The node was low on resource: memory노드 전체 메모리 압박진단 명령 D·E → 복구 3
Exit Code: 137인데 OOMKilled 표시 없음수동 kubectl delete --grace-period=0 또는 외부 SIGKILLevents·audit log 확인, 배포/스크립트 점검

핵심 구분점: OOMKilled는 컨테이너 단위, Evicted는 Pod 단위(노드 스케줄러가 쫓아냄) 입니다. kubectl get pod에서 STATUS가 OOMKilled로 안 뜨고 정상 Running처럼 보여도, 재시작 카운트가 올라간다면 lastState를 꼭 확인하세요.

복붙 진단 명령 모음

위에서 분기를 잡았으면 그대로 복사해 실행하세요. <pod>만 본인 것으로 바꾸면 됩니다.

Bash
# A) Last State 한눈에 보기 — Reason과 Exit Code가 여기 나옵니다
kubectl describe pod <pod> | grep -A5 "Last State"

# B) 스크립트/자동화용: reason + exitCode만 정확히 추출
kubectl get pod <pod> -o jsonpath='{.status.containerStatuses[0].lastState.terminated.reason}{"\n"}{.status.containerStatuses[0].lastState.terminated.exitCode}{"\n"}'
# 출력 예: OOMKilled / 137

# C) 죽기 직전 실제 사용량 추세 (metrics-server 필요)
kubectl top pod <pod> --containers

# D) OOMKilling 이벤트를 클러스터 전역에서 수집
kubectl get events --field-selector reason=OOMKilling -A

# E) 노드로 들어가 커널 OOM killer 로그 직접 확인 (가장 확실한 증거)
#   nsenter/디버그 컨테이너로 노드 진입 후:
dmesg -T | grep -i "Out of memory: Killed process"
journalctl -k | grep -i oom

E의 dmesg에 Killed process ... (java) 같은 줄이 보이면 커널 OOM killer가 그 프로세스를 직접 골라 죽였다는 빼도 박도 못할 증거입니다. cgroup v2가 기본인 2026년 다수 배포판에서는 OOM이 cgroup 단위로 더 정확히 격리되어, 형제 컨테이너 영향 없이 해당 컨테이너만 죽는 경우가 많습니다.

원인별 복구 + 재발 방지

복구 1: limits 재산정

kubectl top으로 본 실사용량 + 여유 30%를 limit으로 잡습니다. requests는 평소 사용량, limit은 피크 기준.

YAML
# Before — limit이 실사용보다 빠듯해 피크 때 즉사
resources:
  requests: { memory: "256Mi" }
  limits:   { memory: "512Mi" }

# After — top 측정 피크 700Mi 기준 여유 확보
resources:
  requests: { memory: "768Mi" }
  limits:   { memory: "1Gi" }

복구 2: 런타임 힙 vs 컨테이너 limit 불일치 (가장 흔한 함정)

JVM·Node.js에서 런타임 힙 설정이 컨테이너 limit과 같거나 크면 시작하자마자, 혹은 GC 직전에 OOMKilled가 납니다. 힙 외 메타스페이스/스택/네이티브 버퍼도 메모리를 먹기 때문입니다.

YAML
# JVM — 절대값 -Xmx 박지 말고 limit 비율로 (limit 인식)
env:
  - name: JAVA_TOOL_OPTIONS
    value: "-XX:MaxRAMPercentage=75.0"   # limit의 75%만 힙에, 나머지는 네이티브 여유
resources:
  limits: { memory: "1Gi" }              # 힙 ~768Mi + 여유 256Mi

# Node.js — old space를 limit보다 작게
env:
  - name: NODE_OPTIONS
    value: "--max-old-space-size=768"    # limit 1Gi 대비 안전

-Xmx1g에 limits.memory: 1Gi처럼 같게 잡는 게 대표적 즉사 패턴입니다. 반드시 힙 < limit으로 두세요.

복구 3: limit 미설정으로 인한 eviction

limit이 없으면 Pod는 BestEffort 또는 Burstable QoS가 되고, 노드 메모리가 부족해지면 이런 Pod가 가장 먼저 쫓겨납니다(Evicted). Karpenter 같은 노드 오토스케일 환경에서는 빈 노드를 줄이려다 압박이 생겨 eviction이 더 자주 보입니다. requests/limits를 명시해 우선순위를 끌어올리세요.

재발 방지: Guaranteed QoS + 모니터링

가장 중요한 워크로드는 requests.memory == limits.memory로 맞춰 Guaranteed QoS를 확보하면 eviction 순위에서 가장 뒤로 빠집니다.

YAML
resources:
  requests: { cpu: "500m", memory: "1Gi" }
  limits:   { cpu: "500m", memory: "1Gi" }   # 메모리 동일 → Guaranteed

추천값은 VPA의 권장 모드(updateMode: "Off")로 먼저 받아 보세요. 이 모드에서 VPA는 권장값만 계산하고 Pod는 바꾸지 않습니다(VPA). 재시작 없이 컨테이너 리소스를 바꾸는 in-place Pod resize는 1.27 알파, 1.33 베타(기본 활성)를 거쳐 1.35에서 GA가 됐습니다(리소스 in-place 조정 문서).

YAML
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata: { name: app-vpa }
spec:
  targetRef: { apiVersion: apps/v1, kind: Deployment, name: app }
  updatePolicy: { updateMode: "Off" }   # 우선 추천값만 받아보기

HPA는 메모리 기반 스케일이 가능하지만, 메모리는 줄어들지 않는 특성(누수·캐시) 때문에 CPU 기준만큼 안정적이지 않습니다. 메모리는 VPA로 적정값을 잡고, 트래픽 스케일은 HPA(CPU/커스텀 메트릭)로 나누는 편을 권합니다.

마지막으로 죽기 전에 알림을 받으세요. OpenTelemetry로 메모리 메트릭이 표준화되는 추세지만, 룰 자체는 동일합니다.

YAML
# Prometheus alert: limit의 90% 도달 시 사전 경고
- alert: PodMemoryNearLimit
  expr: container_memory_working_set_bytes / container_spec_memory_limit_bytes > 0.9
  for: 5m
  labels: { severity: warning }
  annotations:
    summary: "{{ $labels.pod }} 메모리 limit 90% 초과 — OOMKilled 임박"

점검 순서 팁: limit을 무작정 올리기 전에 런타임 힙 설정(MaxRAMPercentage, --max-old-space-size)이 limit보다 충분히 작은지부터 확인하세요. 힙을 limit과 같게 고정한 설정은 limit을 늘리지 않고도 바로잡을 수 있는 대표적인 원인입니다.

측정해서 정하기: working set, requests·limit 산정, 누수 판별

kubectl top과 축출 계산이 보는 값: working set

kubectl top과 kubelet의 메모리 축출 판단은 cgroup 메모리 사용량에서 비활성 파일 캐시를 뺀 working set을 씁니다. 노드의 memory.available도 capacity - workingSet으로 계산됩니다(노드 압박 축출 문서). 그래서 limit을 정할 때는 RSS나 전체 사용량(container_memory_usage_bytes)보다 container_memory_working_set_bytes를 기준으로 보는 편이 실제 동작과 맞습니다. 컨테이너 OOMKilled 자체는 cgroup 한도(cgroup v2에서는 memory.max)에 닿았는데 회수할 메모리가 없을 때 커널이 일으킵니다.

requests·limit은 관측값에서 출발

임의의 숫자 대신 관측 기간의 분포를 보고 정합니다. Prometheus가 있다면 최근 7일 working set의 p95를 requests의 출발점으로, 같은 기간 최대값에 여유를 더한 값을 limit의 출발점으로 삼을 수 있습니다. 여유를 얼마나 둘지는 배치·캐시 워밍 같은 피크 패턴을 보고 정하고, 바꾼 뒤 다시 관측합니다.

PROMQL
# 최근 7일 working set p95 (컨테이너별)
quantile_over_time(0.95,
  container_memory_working_set_bytes{namespace="<ns>", pod=~"<deploy>-.*", container="<c>"}[7d])

# 같은 기간 최대값
max_over_time(
  container_memory_working_set_bytes{namespace="<ns>", pod=~"<deploy>-.*", container="<c>"}[7d])

requests와 limit을 같게 두면 Guaranteed QoS가 되어 노드 압박 시 축출 순위가 뒤로 밀리지만(Pod QoS 문서), 메모리는 압축할 수 없는 자원이라 순간 피크가 limit에 닿으면 그대로 OOMKilled가 납니다. 중요 워크로드에 Guaranteed를 쓸 때는 관측 최대값을 기준으로 값을 잡으세요.

JVM은 힙 밖의 메모리까지 계산

JVM 프로세스 메모리는 힙(-Xmx 또는 MaxRAMPercentage) 외에 메타스페이스, 스레드 스택, 코드 캐시, Direct Buffer 같은 비힙 영역을 더한 값입니다. 힙을 limit과 같게 잡으면 비힙 영역만큼 limit을 넘습니다. 위 예시처럼 MaxRAMPercentage로 힙을 limit의 일부로 두고, 비힙 사용량이 큰 애플리케이션(Direct Buffer를 많이 쓰거나 스레드가 많은 서버 등)은 비율을 더 낮춥니다. 비힙 사용량은 -XX:NativeMemoryTracking=summary로 띄운 뒤 jcmd <pid> VM.native_memory summary로 확인할 수 있습니다(Oracle NMT 문서).

limit 상향인가, 누수인가

working set 그래프 모양으로 가릅니다.

CODE
working set 그래프
  ├─ 오르내림 반복(GC·캐시 해제 후 내려감), 피크만 limit에 닿음 → limit이 빠듯함 → limit 조정
  └─ GC 뒤에도 내려가지 않고 계속 우상향 → 누수 의심 → 힙 덤프·프로파일링

우상향형은 limit을 올려도 OOMKilled 시점이 늦춰질 뿐입니다. limit을 올렸더니 재시작 간격만 길어졌다면 누수를 먼저 의심하세요. JVM이라면 jcmd <pid> GC.heap_dump /tmp/heap.hprof나 JFR로 원인을 찾습니다.

진단 매트릭스

증상추정 원인다음 행동
137 + OOMKilled, 그래프가 오르내리며 피크만 limit 근처limit이 빠듯함관측 최대값 기준으로 limit 재산정
137 + OOMKilled, 그래프가 계속 우상향메모리 누수힙 덤프·프로파일링, 코드 수정
JVM에서 137인데 힙은 여유비힙 영역 초과MaxRAMPercentage 낮추기, NMT로 비힙 확인
Evicted, node was low on resource: memory노드 메모리 압박requests 재산정, 노드 용량·배치 점검(복구 3)
137인데 OOMKilled 표시 없음외부 SIGKILL(강제 삭제, 종료 유예 초과 등)events·audit log 확인

결론: 형제 에러로 오진하지 마세요

137을 다른 종료 코드와 헷갈리면 엉뚱한 곳을 파게 됩니다.

코드/상태의미구분 키워드 → 가야 할 글
137SIGKILL (OOM 또는 강제종료)OOMKilled / dmesg OOM → 이 글
143SIGTERM (graceful 종료 실패)preStop/종료 타임아웃 → graceful shutdown 편
Evicted노드 메모리·디스크 압박node was low on resource → 본문 복구 3
CrashLoopBackOff재시작 루프 (OOM 반복 시 전이)Back-off restarting → 이전 편 CrashLoopBackOff 글

OOM이 반복되면 결국 CrashLoopBackOff로 전이되므로, 반복 재시작이 보이면 lastState reason을 먼저 확인해 진짜 원인이 OOM인지 가르는 게 핵심입니다.

다음 편(K8s_Troubleshooting_Guide 20편)에서는 exit code 143과 SIGTERM, preStop 훅으로 graceful shutdown을 보장하는 런북을 다룹니다.

참고: 공식 문서

이 글에서 다루는 동작·설정·에러의 1차 출처는 다음 공식 문서입니다. 버전별 옵션과 정확한 동작은 여기서 확인하세요.

자주 묻는 질문 (FAQ)

Q. limit을 올렸는데도 또 OOMKilled가 납니다. 왜죠? A. 메모리 누수이거나 JVM/Node 힙 설정이 limit을 따라 올라가지 않은 경우입니다. kubectl top pod --containers로 사용량이 계속 우상향하면 누수, 시작 직후 즉사하면 힙 설정(MaxRAMPercentage, --max-old-space-size)을 점검하세요.

Q. OOMKilled인데 kubectl get pod엔 Running으로 보입니다. A. 컨테이너가 죽고 재시작되어 현재는 Running일 수 있습니다. kubectl describe pod의 Last State와 RESTARTS 카운트, jsonpath로 lastState.terminated.reason을 확인하면 과거 OOM 이력이 드러납니다.

Q. exit code 137인데 OOMKilled 표시가 전혀 없습니다. A. 커널 OOM이 아니라 외부 SIGKILL일 가능성이 큽니다. kubectl delete --grace-period=0, 배포 도구, 노드 종료/드레인, 또는 라이브니스 프로브 실패 후 강제 종료를 의심하고 events와 audit log를 확인하세요.

확인 정보
✦ ✦ ✦
출처·검증 안내

글마다 출처와 확인 범위가 다릅니다. 아래에 확인일·확인 범위·출처가 기록된 글만 그 범위에서 확인된 것입니다. 기록이 없는 글은 적용 전 사용 중인 버전의 공식 문서로 직접 확인하고, 오류는 이메일로 알려주세요. 편집 정책 · 이메일로 제보

이 글에는 사람이 확인한 날짜·범위 기록이 아직 없습니다.

발행 · ·업데이트 ·
주제 관련 공식 문서(자동 연결 · 문장별 근거 아님)Kubernetes 공식 문서 ↗

댓글

첫 번째 댓글을 남겨보세요.