Published on

리눅스 OOM Killer로 프로세스 죽음 원인 추적

Authors

서버에서 특정 프로세스가 예고 없이 종료되면 대부분은 “크래시(SEGFAULT)인가?”, “배포가 덮어썼나?”, “누가 kill 했나?”부터 의심합니다. 그런데 리눅스에서는 메모리가 고갈되는 순간 커널이 시스템 전체를 살리기 위해 OOM(Out-Of-Memory) Killer를 실행하고, 그 결과로 프로세스가 강제 종료될 수 있습니다. 문제는 애플리케이션 로그에는 아무 흔적이 없고, Killed 한 줄만 남거나 아예 조용히 사라지는 경우가 많다는 점입니다.

이 글은 “OOM Killer가 실제로 프로세스를 죽였는지”를 빠르게 확인하고, 어떤 메모리 압박(시스템/컨테이너/cgroup) 때문에, 왜 그 프로세스가 선택되었는지, 어떤 지표/로그를 남겨야 재발 방지 가능한지를 단계별로 정리합니다. 쿠버네티스 환경이라면 함께 읽을 만한 글로 Kubernetes CrashLoopBackOff 원인별 로그·Probe·리소스 디버깅도 참고하면 연결이 잘 됩니다.

1) OOM Killer의 종류: “시스템 OOM” vs “cgroup OOM”

OOM은 한 가지가 아닙니다. 원인 추적을 위해 먼저 분류해야 합니다.

  • 시스템 OOM(Global OOM): 노드 전체 메모리가 부족해 커널이 프로세스를 골라 죽입니다. 보통 MemAvailable이 바닥나고, 페이지 회수(reclaim)가 실패하면서 발생합니다.
  • cgroup OOM(컨테이너/서비스 단위 OOM): cgroup 메모리 제한(memory.max/memory.limit_in_bytes)을 넘어서 해당 cgroup 내부 프로세스가 죽습니다. 쿠버네티스의 OOMKilled가 대표적입니다.

둘 다 “프로세스가 죽었다”는 현상은 같지만, 로그 위치/형태와 재발 방지 전략이 달라집니다.

2) 1분 컷: OOM 여부를 커널 로그에서 확정하기

가장 먼저 확인할 것은 커널 메시지입니다. OOM이면 흔적이 거의 항상 남습니다.

dmesg로 확인

sudo dmesg -T | egrep -i 'out of memory|oom-kill|killed process|oom_reaper|invoked oom-killer' | tail -n 200

대표적으로 다음과 같은 라인이 보입니다.

  • invoked oom-killer: (OOM Killer가 발동됨)
  • Out of memory: Killed process 12345 (myapp) total-vm:... anon-rss:... (누가 죽었는지)
  • oom_reaper: reaped process ... (메모리 회수)

systemd journal에서 커널 메시지 조회

서버가 systemd 기반이면 journal이 더 편합니다.

sudo journalctl -k -S "-24h" | egrep -i 'oom|out of memory|killed process' | tail -n 200

“Killed”만 보일 때의 해석

쉘에서 실행한 프로세스가 OOM으로 죽으면 stdout/stderr에 아래처럼만 찍히는 경우가 많습니다.

  • Killed

이건 애플리케이션이 스스로 종료한 게 아니라, **커널이 SIGKILL(9)**로 날렸을 가능성이 큽니다. 이때는 반드시 커널 로그로 확정해야 합니다.

3) 커널 로그 한 줄에서 뽑아내야 할 핵심 정보

OOM 로그에서 원인 추적에 필요한 필드는 다음입니다.

  • Killed process PID/comm: 어떤 PID/프로세스명이 죽었는지
  • anon-rss / file-rss / shmem-rss: 실제 물리 메모리 점유(RSS) 구성
  • total-vm: 가상메모리 크기(과대할 수 있음)
  • oom_score_adj: 점수 보정치(선택 우선순위에 영향)
  • task_memcg / constraint: cgroup OOM인지 힌트

예시(형태만 이해하면 됩니다):

Out of memory: Killed process 24817 (python) total-vm:4023456kB, anon-rss:1854320kB, file-rss:1200kB, shmem-rss:0kB, UID:1000 pgtables:4096kB oom_score_adj:0

여기서 anon-rss가 크면(익명 메모리) 파이썬/자바 힙, 네이티브 버퍼, 캐시되지 않는 데이터 구조가 원인일 수 있습니다. file-rss가 크면 mmap 파일, 페이지 캐시 성격이 강합니다.

4) “왜 하필 이 프로세스가 죽었나?”: OOM 점수 이해

커널은 후보 프로세스에 점수를 매겨 “죽이기 좋은” 프로세스를 선택합니다. 대략적으로:

  • 메모리를 많이 쓰는 프로세스일수록
  • oom_score_adj가 높을수록(=죽이기 쉬움)

점수가 올라갑니다.

현재 프로세스의 OOM 점수 확인

PID=24817
cat /proc/$PID/oom_score
cat /proc/$PID/oom_score_adj
  • oom_score: 커널이 계산한 현재 점수
  • oom_score_adj: -1000(절대 죽이지 않음에 가깝게) ~ +1000(먼저 죽임)

systemd 서비스라면 OOMScoreAdjust 확인

서비스 단위로 조정했을 수 있습니다.

systemctl show myapp.service -p OOMScoreAdjust

OOMScoreAdjust를 무작정 낮추면(살리면) 다른 중요한 프로세스가 대신 죽을 수 있어, 원인 제거/리밋 조정이 우선입니다.

5) 시스템 OOM인지 확인: 메모리 고갈의 흔적 찾기

시스템 OOM이라면 “노드 전체”가 압박을 받았습니다. 다음을 확인합니다.

OOM 시점의 메모리 상태(로그에 종종 포함)

커널 로그에는 Mem-Info: 섹션이 붙는 경우가 많습니다. 없으면 사후라도 다음을 봅니다.

free -h
cat /proc/meminfo | egrep 'MemTotal|MemFree|MemAvailable|Buffers|Cached|SwapTotal|SwapFree'
  • MemAvailable이 매우 낮고
  • Swap이 없거나(SwapTotal: 0) 이미 바닥이면
  • OOM이 더 쉽게 발생합니다.

vmstat로 압박 징후 확인

vmstat 1 10
  • si/so(swap in/out)가 튀면 스왑 압박
  • r이 높고 wa가 높으면 I/O 대기와 동반된 리클레임 문제 가능

“메모리 누수” vs “순간 스파이크” 구분

  • 누수: RSS가 장시간 우상향, GC/캐시 정책 미비
  • 스파이크: 특정 배치/요청에서 급증(대용량 JSON 파싱, 이미지 처리, 대규모 쿼리 결과 적재 등)

원인별로 재현/완화 방법이 달라서, OOM 시각 전후의 메트릭(프로세스 RSS, 컨테이너 working set)을 반드시 남겨야 합니다.

6) cgroup OOM(컨테이너/서비스 제한 초과) 추적

컨테이너 환경에서는 “노드는 멀쩡한데 특정 Pod/컨테이너만 죽는” 케이스가 흔합니다.

cgroup v2에서 메모리 이벤트 확인

# 프로세스가 속한 cgroup 경로 확인
PID=24817
cat /proc/$PID/cgroup

# 예: /sys/fs/cgroup/<path> 아래에서 확인
CG=/sys/fs/cgroup/<path>
cat $CG/memory.current
cat $CG/memory.max
cat $CG/memory.events

memory.events에 다음 값이 유용합니다.

  • oom: OOM 발생 횟수
  • oom_kill: 실제 kill 발생 횟수
  • max: limit 도달 횟수

Kubernetes라면 이벤트/상태에서 빠르게 확인

kubectl describe pod <pod> | egrep -i 'OOMKilled|Reason|Last State|Events' -n
kubectl get pod <pod> -o jsonpath='{.status.containerStatuses[*].lastState.terminated.reason}'

쿠버네티스에서 OOMKilled는 대개 cgroup limit 초과입니다. 이 경우 커널의 global OOM 로그보다, 컨테이너 리밋과 워킹셋(working set) 추적이 핵심입니다. CrashLoopBackOff와 함께 나타날 때의 묶음 진단은 Kubernetes CrashLoopBackOff 원인별 로그·Probe·리소스 디버깅에서 더 확장해 볼 수 있습니다.

7) “누가 메모리를 먹었나?”: 범인 프로세스/스레드 찾기

OOM 직후에는 이미 죽어서 늦을 수 있으니, 평소에 관측 가능하게 만들어야 합니다.

top/ps로 RSS 상위 프로세스 확인(사후/실시간)

ps -eo pid,ppid,comm,rss,vsz,%mem --sort=-rss | head -n 20

smem으로 PSS(실질 점유) 기준 분석

RSS는 공유 메모리 때문에 과대평가될 수 있습니다. 가능하면 PSS를 봅니다.

sudo apt-get install -y smem  # Debian/Ubuntu
smem -tk | head -n 20

Java/Node/Python 등 런타임별 체크 포인트

  • Java: 힙(-Xmx)과 네이티브(DirectBuffer, Metaspace) 분리 관찰
  • Node.js: --max-old-space-size 미설정 시 컨테이너에서 limit 대비 과도 사용 가능
  • Python: 대형 객체/캐시/멀티프로세싱(워커 수)로 RSS 급증 흔함

8) 재발 방지: 로그/메트릭/코어 덤프 전략

OOM은 “결과”이고, 재발 방지는 관측과 제한 설정에서 시작합니다.

1) 커널 OOM 로그를 영구 보관

  • journal retention 정책 점검(SystemMaxUse, SystemMaxFileSize 등)
  • 클라우드라면 로그 수집기(Fluent Bit 등)로 kernel facility 수집

2) 메모리 상한을 명시하고, 스파이크를 흡수할 여지를 둠

  • 시스템: swap(적절한 크기) + vm.swappiness 튜닝(워크로드에 따라)
  • 컨테이너: request/limit 합리화, JVM/Node 힙 상한을 limit에 맞춤

3) OOM 직전 스냅샷 자동 수집(실무 팁)

systemd 서비스라면 ExecStopPost로 종료 시점에 상태를 남길 수 있습니다(단, OOM SIGKILL은 훅이 실행되지 않을 수 있어 “주기적 스냅샷”도 병행).

예: 주기적으로 RSS 상위와 meminfo를 남기는 스크립트

#!/usr/bin/env bash
TS=$(date -Is)
{
  echo "=== $TS ==="
  free -h
  ps -eo pid,comm,rss,%mem --sort=-rss | head -n 30
  echo "--- meminfo ---"
  cat /proc/meminfo | egrep 'MemAvailable|SwapFree|Cached|AnonPages|Slab'
} >> /var/log/mem-snapshot.log

cron 또는 systemd timer로 10~30초/1분 간격 수집하면, OOM 발생 시점에 가까운 증거를 확보할 확률이 크게 올라갑니다.

9) 실전 트러블슈팅 체크리스트(순서대로)

  1. 커널 로그에서 OOM 확정: journalctl -k / dmesg -T
  2. Killed process 식별: PID/comm, RSS 구성(anon/file)
  3. 시스템 OOM vs cgroup OOM 분류: memory.events, kube OOMKilled, 로그의 memcg 힌트
  4. 왜 선택됐는지 확인: oom_score, oom_score_adj, 서비스 설정
  5. 원인 후보 좁히기: RSS/PSS 상위, 워커 수, 힙 상한, 캐시/버퍼
  6. 재발 방지: 리밋/힙 조정, 스왑/노드 메모리 증설, 관측(스냅샷/메트릭/로그 수집)

10) 마무리: “OOM은 커널이 남기는 가장 중요한 범죄현장 기록”

OOM Killer는 예측 불가능하게 보이지만, 커널 로그에는 “누가, 얼마나 쓰다가, 어떤 조건에서 죽었는지”가 꽤 구체적으로 남습니다. 핵심은 OOM 여부를 먼저 확정하고, 시스템 OOM과 cgroup OOM을 분리한 뒤, **점수(oom_score_adj)와 메모리 구성(anon/file)**을 근거로 재발 방지 조치를 설계하는 것입니다.

쿠버네티스에서 OOM이 CrashLoopBackOff로 이어지는 패턴까지 함께 다루고 싶다면 Kubernetes CrashLoopBackOff 원인별 로그·Probe·리소스 디버깅를 같이 보면 원인-증상-대응이 한 흐름으로 정리됩니다.