- Published on
Linux OOM Killer 원인추적 - dmesg·cgroup·로그
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버가 갑자기 특정 프로세스를 죽이고(혹은 컨테이너가 재시작되고) 아무 경고 없이 서비스가 흔들렸다면, 높은 확률로 Linux OOM(Out-Of-Memory) Killer가 개입했을 수 있습니다. 문제는 “메모리가 부족했다”는 사실만으로는 원인 분석이 끝나지 않는다는 점입니다.
OOM은 크게 두 부류로 나뉩니다.
- 호스트 OOM(전역 메모리 고갈): 노드 전체에서 메모리가 바닥나 커널이 희생양을 고릅니다.
- cgroup OOM(컨테이너/서비스 단위 제한 초과): 노드는 여유가 있어도, 특정 cgroup의
memory.max/limit을 넘어서 해당 그룹 안에서 OOM이 발생합니다.
이 글에서는 dmesg(커널 로그) → cgroup 이벤트/설정 → systemd/journald 및 애플리케이션 로그를 한 줄로 이어, “누가/언제/어떤 이유로 죽었는지”를 증거 기반으로 추적하는 방법을 다룹니다. (쿠버네티스 환경이라면, 사전 점검으로 EKS Pod가 Pending(Insufficient memory)일 때 점검법도 함께 참고하면 좋습니다.)
1) OOM Killer의 시그널: 증상부터 분류하기
1-1. 프로세스가 갑자기 종료(SIGKILL)되었는가?
애플리케이션에서 다음과 같은 현상이 보이면 OOM 가능성이 큽니다.
- 로그가 중간에서 끊기고, 종료 훅/예외 스택트레이스 없이 프로세스가 사라짐
- systemd 서비스가
code=killed, status=9/KILL로 종료 - 컨테이너가
OOMKilled로 재시작
systemd 환경에서는 우선 다음을 확인합니다.
# 서비스가 OOM으로 죽었는지(직접적 단서)
systemctl status my.service
journalctl -u my.service -S "-2h" --no-pager
# 종료 코드/시그널 확인
systemctl show my.service -p ExecMainStatus -p ExecMainCode -p ExecMainPID
status=9/KILL이면 커널/관리자가 SIGKILL을 보낸 것입니다.- 이때 **커널 로그(dmesg/journalctl -k)**에서 OOM 메시지가 같이 찍히는지 이어서 확인해야 합니다.
1-2. 호스트 OOM vs cgroup OOM의 빠른 구분
- 호스트 OOM:
dmesg에Out of memory: Kill process ...와 함께 시스템 전체 메모리 통계가 길게 출력되는 경우가 많습니다. - cgroup OOM:
Memory cgroup out of memory: Kill process ...또는oom-kill:문구에 cgroup 경로가 함께 등장합니다.
이 구분이 중요한 이유는, 해결책이 완전히 달라지기 때문입니다.
- 호스트 OOM: 노드 전체 메모리 압박(누수, 과도한 캐시, 동시성 폭증, swap 설정 등)
- cgroup OOM: 컨테이너/서비스에 설정된 limit이 너무 낮거나, 워킹셋이 증가했거나, 페이지 캐시/버퍼까지 포함해 제한을 초과
2) dmesg/journalctl로 “누가 죽었는지” 확정하기
가장 확실한 1차 증거는 커널 메시지입니다.
2-1. 커널 로그에서 OOM 구간만 뽑기
# 최근 부팅 이후 커널 로그에서 OOM 관련 라인만
journalctl -k -b --no-pager | egrep -i "out of memory|oom-kill|killed process|memory cgroup"
# dmesg를 쓰는 경우(권한 필요)
dmesg -T | egrep -i "out of memory|oom-kill|killed process|memory cgroup"
보통 아래와 같은 정보가 핵심입니다.
- Killed process: 죽은 PID/프로세스명
- oom_score_adj: 희생양 선택에 영향을 준 조정값
- cgroup 경로(있다면): 어떤 제한에서 터졌는지
- 메모리 통계:
MemAvailable,anon-rss,file-rss,shmem-rss,pgtables등
2-2. 로그에서 반드시 읽어야 하는 필드
대표적인 커널 출력 패턴(환경에 따라 다름):
Out of memory: Kill process 12345 (java) score 987 or sacrifice childKilled process 12345 (java) total-vm:..., anon-rss:..., file-rss:...Memory cgroup out of memory: Kill process 12345 (node) ...
여기서 중요한 포인트:
total-vm은 가상메모리 크기라 실제 사용량과 다를 수 있습니다.- 실제 압박을 만든 주범은 대개
anon-rss(힙/스택 등 익명 메모리) 또는file-rss/shmem-rss(파일/공유메모리)입니다. score가 높을수록 희생될 확률이 높고,oom_score_adj로 인위적으로 올리거나 내릴 수 있습니다.
3) cgroup 관점: “왜 그 프로세스가 그 한도에서 죽었는지”
컨테이너/서비스 단위로 메모리를 제한하는 환경(Kubernetes, systemd slice, docker 등)에서는 cgroup OOM이 훨씬 흔합니다.
3-1. cgroup v2인지 v1인지 확인
stat -fc %T /sys/fs/cgroup
# cgroup2fs -> v2
이 글은 v2 기준으로 설명하되, v1도 함께 힌트를 제공합니다.
3-2. 특정 PID가 속한 cgroup 경로 찾기
PID=12345
cat /proc/$PID/cgroup
cgroup v2에서는 보통 0::/some/path 형태로 나오며, 그 경로가 곧 메모리 제한/사용량을 확인할 위치입니다.
예: /sys/fs/cgroup/some/path/
3-3. 메모리 제한/현재 사용량/최댓값 확인(v2)
CG=/sys/fs/cgroup/some/path
# 제한(없으면 max)
cat $CG/memory.max
# 현재 사용량
cat $CG/memory.current
# 피크(커널 버전에 따라 제공)
cat $CG/memory.peak 2>/dev/null || true
# OOM/압박 이벤트 카운트
cat $CG/memory.events
memory.events는 원인추적에 매우 유용합니다. 예시로 다음 항목을 봅니다.
oom: OOM 발생 횟수oom_kill: 실제 kill 발생 횟수high:memory.high에 의해 스로틀링된 횟수
만약 oom_kill이 증가했다면, “호스트 메모리는 남아도는데 컨테이너 제한으로 죽었다”가 거의 확정입니다.
3-4. systemd 서비스라면 slice 단위로 확인
systemd는 서비스마다 cgroup을 만들어 관리합니다.
systemctl show my.service -p ControlGroup
# 예: /system.slice/my.service
CG=/sys/fs/cgroup/system.slice/my.service
cat $CG/memory.max
cat $CG/memory.current
cat $CG/memory.events
또한 systemd 설정에 MemoryMax=가 걸려 있으면 그 자체가 원인일 수 있습니다.
systemctl cat my.service
systemctl show my.service -p MemoryMax -p MemoryHigh -p OOMScoreAdjust
3-5. Kubernetes(컨테이너)라면 limit과 OOMKilled를 함께
쿠버네티스에서는 커널 OOM 로그 + kubelet 이벤트 + 컨테이너 상태를 함께 봐야 합니다.
# 컨테이너 종료 이유가 OOMKilled인지
kubectl get pod <pod> -o jsonpath='{.status.containerStatuses[*].lastState.terminated.reason}'
# 이벤트에서 OOMKill 흔적
kubectl describe pod <pod>
메모리 limit이 너무 타이트하면, 노드에 여유가 있어도 OOMKilled가 발생합니다. 노드 레벨의 “메모리 부족” 문제와는 결이 다르므로, 스케일 아웃/노드 증설만으로 해결되지 않을 수 있습니다.
4) 로그 타임라인 엮기: 커널 → 서비스 → 애플리케이션
OOM 원인추적의 핵심은 시간축 상관관계입니다.
- 커널이 kill한 시각(T0)
- systemd/kubelet이 재시작한 시각(T0+α)
- 애플리케이션이 메모리를 급증시킨 시각(T0-β)
4-1. 커널 OOM 시각을 기준으로 journalctl 범위 조회
# 예: OOM이 13:05:12에 발생했으면 전후 10분
journalctl -k --since "2026-02-23 12:55:00" --until "2026-02-23 13:15:00" --no-pager
# 같은 구간에서 서비스 로그 확인
journalctl -u my.service --since "2026-02-23 12:55:00" --until "2026-02-23 13:15:00" --no-pager
이 구간에서 흔히 찾는 단서:
- 트래픽 급증, 배치/크론 시작
- 특정 요청 패턴(대용량 파일 처리, 압축/이미지 변환, 대규모 쿼리)
- GC 폭주/힙 확장 로그(자바), 워커 수 증가(노드/파이썬), 캐시 워밍
DB 쿼리 폭증이나 락/데드락으로 인해 작업이 쌓이며 메모리가 증가하는 경우도 있어, 필요하면 Spring Boot HikariCP 풀 고갈·DB 타임아웃 10분 진단처럼 “대기열이 메모리를 먹는” 전형적인 병목도 함께 점검하는 것이 좋습니다.
5) “왜 하필 그 프로세스?”: OOM 점수와 희생양 선택
커널은 모든 프로세스를 죽이지 않고, 가장 적절한 희생양을 고릅니다. 이때 중요한 값이 oom_score와 oom_score_adj입니다.
5-1. 죽은 PID의 점수 확인(사후엔 어려울 수 있음)
프로세스가 이미 죽었다면 /proc/<pid>는 사라집니다. 하지만 유사 상황 재현/관찰 시 다음을 확인합니다.
PID=12345
cat /proc/$PID/oom_score
cat /proc/$PID/oom_score_adj
oom_score_adj는 -1000(거의 안 죽음) ~ +1000(잘 죽음)- systemd/Kubernetes는 특정 프로세스(예: kubelet, sshd)를 보호하기 위해 조정할 수 있습니다.
5-2. 무작정 보호(oom_score_adj 낮추기)의 함정
중요 프로세스를 보호하려고 oom_score_adj=-1000을 남발하면, OOM 시 다른 프로세스가 대신 죽고 시스템이 더 불안정해질 수 있습니다.
권장 접근:
- 원인 메모리 폭증을 줄이거나
- cgroup limit을 현실화하고
- 메모리 사용량을 관측 가능하게 만든 뒤
- 마지막 수단으로만 보호 정책을 조정
6) 실전 디버깅 레시피: “OOM 한 번”을 재현 가능한 이슈로 만들기
6-1. 10분 내 1차 결론 체크리스트
아래 질문에 답을 채우면, 원인 범위가 급격히 좁혀집니다.
- 커널 로그에 OOM이 찍혔나? (
journalctl -k) - 호스트 OOM인가, cgroup OOM인가? (로그 문구 +
memory.events) - 죽은 프로세스(PID/커맨드라인)는 무엇인가?
- 죽기 직전 메모리 사용(anon/file/shmem)은 어떤 성격인가?
- 제한값(
memory.max/K8s limit/systemd MemoryMax)은 얼마였나? - 같은 시각에 무엇이 시작/증가했나? (배치, 트래픽, 대용량 요청, DB 지연)
6-2. 원인별로 다음 액션이 달라진다
(A) cgroup limit이 너무 낮은 경우
- 컨테이너/서비스의 메모리 limit 상향(그리고 request/limit 균형 조정)
- 워킹셋 증가 요인 제거(캐시 크기 제한, 동시성 제한)
memory.high를 활용해 “죽이기 전에” 스로틀링으로 완화(v2)
(B) 메모리 누수/비정상 증가가 의심되는 경우
- RSS 추이를 시계열로 수집(프로메테우스/노드 익스포터/cAdvisor)
- 힙 덤프/프로파일링(자바: heap dump, node: heap snapshot)
- 특정 요청/데이터에서만 증가하는지 샘플링
(C) 파일 캐시/버퍼가 원인처럼 보이는 경우
- 실제로는 정상적인 페이지 캐시일 수 있음(리눅스는 남는 메모리를 캐시로 사용)
- 하지만 cgroup 제한에서는 페이지 캐시도 압박 요인이 될 수 있으니, 파일 I/O 패턴과 캐시 전략을 점검
7) 예제: OOM 발생 지점 자동 수집 스크립트
운영 중 OOM이 뜰 때마다 수동으로 복기하기 어렵다면, “OOM 흔적”을 자동으로 모으는 것이 좋습니다. 아래는 단순 예시입니다.
#!/usr/bin/env bash
set -euo pipefail
OUT_DIR=${1:-/var/log/oom-triage}
TS=$(date +"%Y%m%d-%H%M%S")
mkdir -p "$OUT_DIR/$TS"
# 1) 커널 OOM 라인
journalctl -k -b --no-pager | egrep -i "out of memory|oom-kill|killed process|memory cgroup" \
> "$OUT_DIR/$TS/kernel-oom-grep.log" || true
# 2) 최근 15분 커널 로그
journalctl -k --since "-15min" --no-pager > "$OUT_DIR/$TS/kernel-last-15min.log" || true
# 3) 메모리/스왑 현황
free -h > "$OUT_DIR/$TS/free.log" || true
swapon --show > "$OUT_DIR/$TS/swapon.log" || true
# 4) 상위 메모리 프로세스
ps aux --sort=-rss | head -n 30 > "$OUT_DIR/$TS/ps-top-rss.log" || true
# 5) cgroup v2 루트 이벤트(가능하면)
if stat -fc %T /sys/fs/cgroup | grep -q cgroup2fs; then
cat /sys/fs/cgroup/memory.events > "$OUT_DIR/$TS/cgroup-root-memory-events.log" || true
fi
echo "saved to $OUT_DIR/$TS"
이 스크립트는 “사후 포렌식”을 돕는 최소한의 자료를 모읍니다. 더 나아가면, OOM 발생 시점에 해당 PID의 cgroup 경로를 찾아 memory.current/max/events까지 덤프하도록 확장할 수 있습니다.
8) 마무리: OOM 분석의 정답은 ‘증거의 연결’
OOM Killer는 결과일 뿐이고, 원인은 대개 다음 중 하나로 수렴합니다.
- (1) 전역 메모리 고갈(호스트 OOM)
- (2) cgroup 제한 초과(cgroup OOM)
- (3) 병목으로 인한 대기열/버퍼 누적(간접적 메모리 증가)
가장 효율적인 접근은 dmesg/journalctl로 kill 사실을 확정하고, cgroup의 제한/이벤트로 범위를 좁힌 뒤, 서비스/애플리케이션 로그로 트리거를 특정하는 것입니다. 이 흐름만 습관화해도 “원인 모를 재시작”이 “재현 가능한 결함”으로 바뀌고, 해결책(리밋 조정 vs 누수 수정 vs 병목 제거)도 명확해집니다.
쿠버네티스/EKS라면 OOM 이슈가 스케줄링/리소스 설계 문제로 이어지는 경우가 많으니, 필요 시 EKS Pod Pending(Insufficient memory)일 때 점검법과 함께 리밋/리퀘스트 및 노드 용량 설계까지 한 번에 점검하는 것을 권장합니다.