- Published on
리눅스 OOM Killer 로그로 원인 프로세스 찾기
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버가 갑자기 느려지거나 애플리케이션이 예고 없이 죽었다면, 상당수는 **Linux OOM Killer(Out-Of-Memory Killer)**가 개입한 결과입니다. 문제는 “누가 죽었나”보다 “왜 메모리가 부족해졌고, 어떤 프로세스가 원인이었나”를 빠르게 특정하는 것입니다. 다행히 커널은 OOM 상황에서 꽤 많은 단서를 로그로 남깁니다. 이 글에서는 OOM Killer 로그만으로 원인 프로세스를 찾는 실전 루틴을 단계별로 정리합니다.
> 더 깊은 메모리 누수/추세 분석까지 이어가고 싶다면: Linux OOM Killer 로그 추적과 메모리 누수 진단
OOM Killer가 발생하는 대표 시나리오
OOM은 단순히 “RAM이 꽉 찼다”만이 아닙니다. 로그를 보기 전에, 어떤 종류의 OOM인지 감을 잡으면 원인 범위를 좁히기 쉽습니다.
- 전역 메모리 부족(Global OOM): 노드 전체 메모리가 고갈
- cgroup OOM(컨테이너/서비스 단위 제한 초과): systemd slice, Docker/K8s cgroup 메모리 제한을 넘김
- 커밋 한도/오버커밋 이슈:
vm.overcommit_memory/CommitLimit관련 - /dev/shm 등 특정 영역 부족: 공유메모리(shm) 부족으로 OOM 유사 증상(특히 컨테이너)
K8s/EKS에서 shm 문제로 터지는 케이스는 별도 접근이 필요할 수 있습니다: EKS에서 Pod /dev/shm 부족으로 OOM 해결하기
1) 커널 로그에서 OOM 이벤트를 먼저 찾기
OOM은 커널이 찍는 메시지이므로 우선 커널 로그에서 “Killed process”를 찾습니다.
systemd/journald 환경
# 부팅 이후 커널 메시지 중 OOM 관련만
journalctl -k -b | egrep -i 'oom|out of memory|killed process'
# 최근 1시간만
journalctl -k --since "1 hour ago" | egrep -i 'oom|killed process'
전통적인 dmesg
dmesg -T | egrep -i 'oom|out of memory|killed process'
여기서 목표는 아래 2가지를 확보하는 것입니다.
- OOM이 발생한 시각(장애 타임라인과 맞추기)
- Killed process 라인(죽은 PID/프로세스명/메모리 사용량)
2) “Killed process” 라인에서 핵심 필드 읽기
대표적인 OOM 로그는 이런 형태로 나옵니다(커널 버전/설정에 따라 조금씩 다름).
Out of memory: Killed process 12345 (python) total-vm:2048000kB, anon-rss:1536000kB, file-rss:10240kB, shmem-rss:0kB, UID:1000 pgtables:4096kB oom_score_adj:0
이 한 줄에서 바로 읽어야 할 포인트:
- PID / comm:
12345 (python)→ “무엇이 죽었나” - anon-rss: 익명 메모리(힙 등). 누수/캐시 폭증의 1차 단서
- file-rss: 파일 매핑 메모리
- shmem-rss: shared memory 사용
- oom_score_adj: OOM에서 우선적으로 죽도록 조정된 값(컨테이너 런타임/서비스가 조정하는 경우가 많음)
여기서 중요한 함정이 하나 있습니다.
- 죽은 프로세스가 ‘원인’이 아닐 수도 있습니다.
- OOM은 “가장 큰 놈”이 아니라 “점수(oom_score)”가 높은 놈을 죽입니다.
- 따라서 OOM 직전 메모리를 가장 많이 먹은 주범과 실제로 죽은 희생양이 다를 수 있습니다.
그래서 다음 단계에서 “OOM 직전 상황”을 더 읽어야 합니다.
3) OOM 직전의 “Mem-Info”/프로세스 테이블 덤프 확인
OOM 직전에는 종종 메모리 상태와 프로세스별 사용량 테이블이 같이 찍힙니다.
검색 키워드:
Mem-Info:Tasks state/Tasks/pid테이블oom-kill:
# OOM 발생 시점 주변 200줄 확인(예: journalctl)
journalctl -k -b | egrep -ni 'oom|mem-info|killed process|oom-kill' | head
# 특정 시각 전후로 범위 조회
journalctl -k --since "2026-02-23 10:00:00" --until "2026-02-23 10:10:00"
프로세스 테이블이 찍히면 보통 이런 식입니다.
[ pid ] uid tgid total_vm rss pgtables_bytes swapents oom_score_adj name
[ 1111] 1000 1111 500000 200000 1234123 0 0 myapp
[ 2222] 0 2222 300000 150000 512000 0 -998 systemd
...
여기서 확인할 것:
- rss가 가장 큰 프로세스가 누구인지
- swapents가 증가했는지(스왑 사용)
oom_score_adj가 비정상적으로 큰 프로세스가 있는지(희생양 가능)
프로세스 테이블이 없다면, 최소한 Killed process 라인과 그 직전의 oom-kill: 라인을 최대한 확보해두세요.
4) PID를 “실제 서비스/컨테이너”로 매핑하기
로그에 python, java만 찍히면 원인 서비스가 여러 개일 수 있습니다. PID를 기준으로 커맨드라인/유닛/cgroup을 매핑해야 합니다.
(1) 커맨드라인 확인
OOM 직후 프로세스는 이미 죽었을 수 있지만, 동일한 서비스가 재시작했다면 비슷한 커맨드 패턴을 찾는 데 도움이 됩니다.
# 살아있다면
ps -p 12345 -o pid,ppid,user,cmd --forest
# /proc가 남아있다면(죽기 전/직후 타이밍에는 남아있을 때도 있음)
tr '\0' ' ' < /proc/12345/cmdline
(2) systemd 서비스로 매핑
# PID가 속한 systemd unit
systemctl status 12345
# 또는 cgroup 경로로 unit 추정
cat /proc/12345/cgroup
/proc/<pid>/cgroup 출력에서 system.slice/myservice.service 같은 경로가 보이면 거의 확정입니다.
(3) 컨테이너(Docker/containerd/K8s)로 매핑
컨테이너 환경에서는 cgroup 경로에 컨테이너 ID 일부가 섞여 나오는 경우가 많습니다.
cat /proc/12345/cgroup
- Docker:
docker/<container-id> - containerd(K8s):
kubepods/.../<container-id>
K8s라면 노드에서 다음처럼 컨테이너 ID로 역추적합니다.
# containerd 예시: crictl로 컨테이너 검색
crictl ps -a | grep -i <container-id-prefix>
crictl inspect <container-id> | jq -r '.status.labels["io.kubernetes.pod.name"], .status.labels["io.kubernetes.pod.namespace"], .status.metadata.name'
이 단계까지 오면 “죽은 PID = 어떤 Pod/컨테이너/서비스인지”가 명확해집니다.
5) Global OOM vs cgroup OOM 구분하기
원인 분석에서 가장 중요한 갈림길입니다.
cgroup OOM의 전형적인 로그
커널/환경에 따라 다르지만, 보통 아래 단서가 있습니다.
Memory cgroup out of memory:oom_memcg=같은 필드Tasks in /kubepods/... are being killed류 메시지
journalctl -k -b | egrep -i 'memcg|memory cgroup|kubepods|oom_memcg'
cgroup OOM이면 결론은 대개 둘 중 하나입니다.
- 메모리 제한이 너무 작다(요청/제한 재설계 필요)
- 특정 컨테이너가 제한 내에서 비정상적으로 메모리를 먹는다(누수/캐시 폭증)
Global OOM이면 노드 전체에서 경쟁이 벌어진 것이므로, “주범 프로세스”가 다른 서비스일 가능성이 큽니다(희생양과 주범 분리).
6) OOM 점수(oom_score, oom_score_adj)로 “왜 얘가 죽었는지” 확인
OOM Killer는 대략적으로 “메모리 많이 쓰는 정도 + 조정값”으로 대상을 고릅니다.
살아있는 프로세스라면 다음으로 확인할 수 있습니다.
# 현재 OOM 점수
cat /proc/12345/oom_score
cat /proc/12345/oom_score_adj
oom_score_adj가 양수로 크게 설정되어 있으면 희생양으로 선택되기 쉽습니다.- 반대로
-1000에 가까우면(예: systemd, sshd) 웬만해선 안 죽습니다.
컨테이너 런타임/오케스트레이터가 특정 프로세스에 oom_score_adj를 부여하는 경우가 있어, “왜 A가 아니라 B가 죽었나”의 답이 여기에 있는 경우가 많습니다.
7) 로그만으로 “원인 프로세스”를 특정하는 실전 체크리스트
OOM 로그를 찾았다고 끝이 아닙니다. 아래 체크리스트를 순서대로 밟으면, 로그 기반으로도 원인에 상당히 근접할 수 있습니다.
체크리스트
Killed process라인의 PID/프로세스명/anon-rss 확보- 같은 시각의
Mem-Info또는 프로세스 테이블 덤프 확인 - rss 최상위 프로세스와 killed 프로세스가 같은지 비교
/proc/<pid>/cgroup로 서비스/컨테이너 매핑memcg단서로 cgroup OOM인지 Global OOM인지 구분oom_score_adj가 희생양 선택에 영향을 줬는지 확인- 장애 시각 전후로 재시작/배포/트래픽 급증 이벤트와 교차검증
8) 재발 방지를 위한 최소 조치(원인별)
원인을 특정했다면, 다음은 재발 방지입니다. 여기서는 “로그 기반으로 원인 프로세스를 찾은 뒤” 바로 적용 가능한 최소 조치만 정리합니다.
(1) 특정 프로세스 메모리 폭증(누수/캐시)
- 애플리케이션 레벨: 힙 덤프/프로파일링, 캐시 상한, 배치 처리 chunk 크기 제한
- 운영 레벨: systemd/K8s에서 메모리 제한과 재시작 정책 정교화
(2) cgroup 제한이 너무 작음
- K8s:
resources.requests/limits재설계(요청은 현실적으로, 제한은 OOM 방지 수준으로) - JVM/Node/Python 등 런타임별 메모리 옵션을 제한에 맞게 조정
(3) Global OOM(노드 전체)
- 노드 사이징/오토스케일
- 메모리 많이 쓰는 워크로드 분리(노드풀 분리)
- 파일 캐시/페이지 캐시가 과도한지(스토리지/IO 패턴) 점검
9) 한 번에 뽑아보는 OOM 로그 수집 스니펫
장애 대응 중에는 “필요한 것만 빠르게” 뽑는 게 중요합니다.
#!/usr/bin/env bash
set -euo pipefail
SINCE=${1:-"2 hours ago"}
echo "== OOM related kernel logs since: $SINCE =="
journalctl -k --since "$SINCE" | egrep -i 'out of memory|oom-kill|killed process|memcg|memory cgroup|kubepods' || true
echo
echo "== Recent Killed process lines =="
journalctl -k --since "$SINCE" | egrep -i 'killed process' || true
이 스니펫 출력에서 Killed process <pid>를 얻으면, 앞서 설명한 /proc/<pid>/cgroup 매핑 루틴으로 바로 이어가면 됩니다.
마무리
OOM Killer 로그는 단순한 “프로세스 하나 죽음” 이벤트가 아니라, 커널이 남겨주는 미니 포렌식 리포트에 가깝습니다. Killed process 한 줄만 보지 말고, 그 전후의 Mem-Info, 프로세스 테이블, memcg(cgroup) 단서, oom_score_adj까지 함께 읽으면 “희생양”이 아니라 **진짜 원인 프로세스(또는 원인 워크로드)**에 도달할 확률이 크게 올라갑니다.
장애가 반복된다면, 로그 기반 특정 후에는 메모리 추세/누수 진단까지 이어가는 것을 권합니다: Linux OOM Killer 로그 추적과 메모리 누수 진단