kubectl describe pod web | grep -A5 'Last State'
# Last State: Terminated
# Reason: OOMKilled
# Exit Code: 137
메모리 limit 을 넘겨 커널이 프로세스를 죽인 상태 137 = 128 + 9 (SIGKILL)
로그가 안 남는다
-
SIGKILL 은 잡을 수 없다
-
종료 훅 · 셧다운 로그 · 스택 덤프가 전혀 돌지 않는다
-
애플리케이션 로그의 마지막 줄이 평범한 처리 로그다
-
확인은 밖에서 한다
- kubectl describe pod Last State 의 Reason
- kubectl get events -A | grep -i oom
- 노드에서: dmesg | grep -i "killed process"
런타임이 limit 을 인식해야 한다
cgroup 은 커널 수준 제한이라, 런타임이 모르면 호스트 전체 메모리를 기준으로 판단한다
JVM 은 Java 10+ 에서 컨테이너 제한을 인식한다 (UseContainerSupport) -XX:MaxRAMPercentage=75 limit 대비 비율로 힙을 잡는다 -Xmx 를 직접 주면 limit 이 바뀌어도 안 따라간다
주의 — 힙 밖에도 메모리가 든다 메타스페이스 · 코드 캐시 · 스레드 스택 · 다이렉트 버퍼 · GC 구조 힙을 limit 의 100% 로 잡으면 반드시 OOMKilled 된다 실무 감각은 limit 의 60~75%
컨테이너 OOM 과 노드 OOM 을 구분한다
-
컨테이너 OOM — 그 cgroup 의 limit 초과 → 그 컨테이너만 죽는다
- Reason: OOMKilled 로 명확히 표시된다
-
노드 압박 — 노드 전체 메모리 부족 → kubelet 이 파드를 '퇴출' 한다
- Reason: Evicted, 메시지에 MemoryPressure
- BestEffort → Burstable → Guaranteed 순으로 쫓겨난다
- limit 을 안 건 파드가 원인인 경우가 많다
-
둘은 조치가 다르다
- 전자 — 그 파드의 limit 이나 사용량을 본다
- 후자 — 노드 전체 배치와 limit 미설정 파드를 본다
제한 부족인지 누수인지
- 제한 부족 — 사용량이 limit 근처에서 평평하다가 튄다
- 부하가 오를 때만 발생한다
- 누수 — 사용량이 시간에 따라 계속 우상향한다
- 부하와 무관하게 며칠 뒤 발생한다
메모리 그래프를 며칠 단위로 보면 둘이 바로 갈린다 제한만 올리면 누수는 시점만 미뤄진다
메모리 지표를 정확히 읽는다
- container_memory_working_set_bytes OOM 판정에 쓰이는 값
- container_memory_usage_bytes — 페이지 캐시를 포함해 더 크게 보인다
- container_memory_rss — 익명 메모리
kubectl top 은 working set 을 보여 준다
usage 만 보고 "limit 에 가깝다" 고 판단하면 오진한다 캐시는 압박이 오면 회수되므로 그것만으로 OOM 이 나지는 않는다
재현이 안 될 때
OOM 은 피크에만 발생해 평소 그래프에서 안 보인다
- 스크레이프 주기(보통 15~30초)보다 짧은 스파이크는 안 잡힌다
- 그래서 "메모리가 여유로운데 OOM 이 난다" 로 보인다
대응
- memory.peak(cgroup v2) 또는 컨테이너 런타임의 최대치 지표를 본다
- 힙 덤프를 남기게 한다
- -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/dump
- (단 컨테이너 OOM 은 JVM OOM 이 아니라 덤프가 안 남을 수 있다)
- limit 을 일시적으로 크게 올려 실제 피크를 관찰한다
면접 함정
- ❌ "OOM이면 애플리케이션 로그에 남는다" → SIGKILL이라 아무것도 안 남는다.
- ❌ "limit만 올리면 해결" → 누수면 시점만 미뤄진다. 그래프로 먼저 구분한다.