메모리 제한을 넘겨 커널이 프로세스를 죽인 상태.
$ docker inspect web --format '{{.State.OOMKilled}} {{.State.ExitCode}}'
true 137
컨테이너 안 프로세스가 limit 을 넘겨 쓰려 한다
→ 커널이 그 cgroup 안에서 프로세스를 하나 죽인다
→ 보통 가장 많이 쓰는 프로세스, 즉 우리 애플리케이션
→ 상태 OOMKilled · 종료 코드 137
로그가 안 남는 이유
SIGKILL 은 잡을 수 없다
→ 종료 훅 · 셧다운 로그 · 스택 덤프가 전혀 돌지 않는다
→ 애플리케이션 로그의 마지막 줄이 평범한 처리 로그다
확인은 밖에서 한다
dmesg | grep -i "killed process"
docker events --filter event=oom
kubectl describe pod (Last State: Terminated, Reason: OOMKilled)
런타임이 제한을 인식해야 한다
JVM 은 Java 10+ 에서 컨테이너 제한을 인식한다 (UseContainerSupport, 기본 켜짐) -XX:MaxRAMPercentage=75 limit 대비 비율로 힙을 잡는다 -Xmx 를 직접 주면 limit 이 바뀌어도 안 따라간다
주의 — 힙 밖에도 메모리가 든다 메타스페이스 · 코드 캐시 · 스레드 스택 · 다이렉트 버퍼 · GC 자체 구조 힙을 limit 의 100% 로 잡으면 반드시 OOMKilled 된다 실무 감각은 limit 의 60~75%
컨테이너 limit 2GB 예산 예시
힙 1,200MB
메타스페이스 128MB
코드 캐시 64MB
스레드 200개 200MB
다이렉트 버퍼 128MB
JVM 내부·GC ~200MB
─────────────────────
합계 ~1,920MB
스왑을 열어 두면 더 나쁠 수 있다
--memory-swap 을 --memory 보다 크게 두면
- 메모리를 넘겨도 스왑으로 버틴다
- 안 죽는 대신 극도로 느려진다
- 살아 있는데 응답이 없는, 진단하기 더 어려운 상태
대개 --memory-swap 을 --memory 와 같게 잠그는 편이 낫다 "빨리 죽고 빨리 재시작" 이 더 나은 경우가 많다
진짜 누수인지 가른다
- 제한 부족 — 사용량이 limit 근처에서 평평하다가 튄다
- 부하가 오를 때만 발생한다
- 누수 — 사용량이 시간에 따라 계속 우상향한다
- 부하와 무관하게 며칠 뒤 발생한다
메모리 사용량 그래프를 며칠 단위로 보면 둘이 바로 갈린다 제한만 올리면 누수는 시점만 미뤄진다
노드 전체가 압박받는 경우와 구분한다
- 컨테이너 OOM — 그 cgroup 의 limit 초과 → 그 컨테이너만 죽는다
- 노드 OOM — 호스트 전체 메모리 부족 → 커널이 아무 프로세스나 고른다
- limit 을 안 걸어 둔 컨테이너가 원인인 경우가 많다
노드 OOM 이면 무관한 컨테이너가 죽는다 — 원인 찾기가 훨씬 어렵다 그래서 '모든 컨테이너에 limit 을 건다' 가 기본 규칙이다
메모리를 실제로 재는 법
docker stats --no-stream
cat /sys/fs/cgroup/memory.current # 컨테이너 안에서
cat /sys/fs/cgroup/memory.peak # 최고치 (v2)
memory.current 에는 페이지 캐시도 포함된다
→ 파일을 많이 읽는 워크로드는 사용량이 높게 보인다
→ 캐시는 압박이 오면 회수되므로 그것만으로 OOM 이 나지는 않는다
→ memory.stat 의 anon(익명 메모리)을 함께 본다. 이쪽이 진짜 사용량에 가깝다
면접 함정
- ❌ "OOM이면 애플리케이션 로그에 남는다" → SIGKILL이라 아무것도 안 남는다.
- ❌ "힙을 limit만큼 주면 된다" → 힙 밖 메모리 때문에 반드시 초과한다.