- Published on
리눅스 OOM Killer로 프로세스 죽음 원인 추적
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버에서 특정 프로세스가 예고 없이 종료되면 대부분은 “크래시(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 등)로
kernelfacility 수집
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) 실전 트러블슈팅 체크리스트(순서대로)
- 커널 로그에서 OOM 확정:
journalctl -k/dmesg -T - Killed process 식별: PID/comm, RSS 구성(anon/file)
- 시스템 OOM vs cgroup OOM 분류:
memory.events, kubeOOMKilled, 로그의 memcg 힌트 - 왜 선택됐는지 확인:
oom_score,oom_score_adj, 서비스 설정 - 원인 후보 좁히기: RSS/PSS 상위, 워커 수, 힙 상한, 캐시/버퍼
- 재발 방지: 리밋/힙 조정, 스왑/노드 메모리 증설, 관측(스냅샷/메트릭/로그 수집)
10) 마무리: “OOM은 커널이 남기는 가장 중요한 범죄현장 기록”
OOM Killer는 예측 불가능하게 보이지만, 커널 로그에는 “누가, 얼마나 쓰다가, 어떤 조건에서 죽었는지”가 꽤 구체적으로 남습니다. 핵심은 OOM 여부를 먼저 확정하고, 시스템 OOM과 cgroup OOM을 분리한 뒤, **점수(oom_score_adj)와 메모리 구성(anon/file)**을 근거로 재발 방지 조치를 설계하는 것입니다.
쿠버네티스에서 OOM이 CrashLoopBackOff로 이어지는 패턴까지 함께 다루고 싶다면 Kubernetes CrashLoopBackOff 원인별 로그·Probe·리소스 디버깅를 같이 보면 원인-증상-대응이 한 흐름으로 정리됩니다.