Published on

Linux OOM Killer 로그 추적과 메모리 누수 진단

Authors

서버가 멀쩡히 돌아가다가 갑자기 프로세스가 죽고(혹은 Pod가 재시작되고) 로그에는 Killed process ... 한 줄만 남는다면, 대개 Linux OOM(Out Of Memory) Killer가 개입한 상황입니다. 문제는 OOM은 “결과”일 뿐이고, 실제 원인은 애플리케이션 누수, 잘못된 cgroup 제한, 커널 슬랩 증가, 페이지 캐시/버퍼 압박, 파일 디스크립터/메모리 맵 폭증 등 다양하다는 점입니다.

이 글은 다음을 목표로 합니다.

  • OOM Killer 이벤트를 정확히 로그로 추적하고 타임라인을 복원한다.
  • oom_score_adj, RSS, cgroup 메모리 제한 등 왜 그 프로세스가 선택됐는지 근거를 남긴다.
  • “메모리가 부족했다”를 넘어 누수인지/정상 부하인지/커널 메모리 문제인지 분해한다.
  • 재발 방지를 위해 관측(메트릭) + 진단(덤프/프로파일) + 완화(튜닝) 순서로 접근한다.

쿠버네티스(EKS 등) 환경에서 OOM이 발생하면 원인 추적이 더 어려워집니다. 네트워크 오류처럼 보이는 장애가 사실은 OOM으로 인한 프로세스 재시작인 경우도 흔합니다. 예를 들어 CoreDNS가 죽으면 DNS가 간헐적으로 실패하며 SERVFAIL/NXDOMAIN가 보일 수 있는데, 이럴 땐 네트워크 튜닝 글보다 먼저 “노드/파드 OOM 여부”부터 확인해야 합니다(참고: EKS CoreDNS CrashLoopBackOff - upstream 타임아웃 해결).

1) OOM Killer가 발생하는 기본 메커니즘

Linux는 메모리가 부족해지면 다음과 같은 순서로 버팁니다.

  1. 페이지 회수(캐시/익명 페이지 reclaim)
  2. 스왑 사용(가능한 경우)
  3. 메모리 압박이 지속되면 OOM 조건 진입
  4. OOM Killer가 희생 프로세스(victim) 를 골라 강제 종료

여기서 중요한 포인트는 “메모리”가 단순히 RSS만이 아니라는 점입니다.

  • 익명 메모리(anonymous): 힙/스택 등, 누수의 주범이 되기 쉬움
  • 페이지 캐시(page cache): 파일 I/O가 많으면 커질 수 있으나 필요 시 회수 가능
  • 슬랩(slab): 커널 오브젝트 캐시. 네트워크/파일시스템/컨테이너 레이어에서 증가 가능
  • cgroup 메모리: 컨테이너/파드 제한이 있으면 노드가 여유 있어도 컨테이너만 OOM 날 수 있음

즉 “노드 free가 남아 있는데도 OOM” 또는 “컨테이너 OOMKilled인데 노드에는 여유” 같은 현상은 흔합니다.

2) OOM 로그를 어디서 어떻게 찾나 (journalctl/dmesg)

OOM Killer는 커널 로그에 남습니다. 배포판에 따라 위치가 다릅니다.

2.1 systemd 환경: journalctl로 커널 메시지 필터링

# 최근 부팅에서 OOM 관련 메시지
journalctl -k -b | egrep -i "oom|out of memory|killed process"

# 시간 범위를 좁혀 검색
journalctl -k --since "2026-02-23 10:00" --until "2026-02-23 11:00" \
  | egrep -i "oom|out of memory|killed process"

# 커널 로그만 follow
journalctl -k -f

2.2 dmesg로 즉시 확인(버퍼 크기/유실 주의)

dmesg -T | egrep -i "oom|out of memory|killed process" | tail -n 200

2.3 /var/log/messages 또는 /var/log/kern.log

systemd가 아닌 환경이거나 rsyslog 설정에 따라 아래 파일에 기록될 수 있습니다.

sudo egrep -i "oom|out of memory|killed process" /var/log/messages
sudo egrep -i "oom|out of memory|killed process" /var/log/kern.log

3) OOM 로그 한 줄에서 ‘누가 왜 죽었는지’ 읽는 법

대표적인 커널 로그는 다음과 같은 패턴을 가집니다.

  • Out of memory: Kill process 1234 (myapp) score 987 or sacrifice child
  • Killed process 1234 (myapp) total-vm:... anon-rss:... file-rss:... shmem-rss:...

여기서 핵심은 scorerss breakdown입니다.

  • anon-rss: 힙/스택 등 익명 메모리(누수 가능성이 큼)
  • file-rss: 파일 매핑(라이브러리/파일 mmap 등)
  • shmem-rss: 공유 메모리(/dev/shm, tmpfs, IPC)

3.1 OOM 점수(oom_score / oom_score_adj)

OOM Killer는 프로세스의 “죽이기 적합함”을 점수화합니다. 대략적으로:

  • 메모리를 많이 쓰는 프로세스일수록 점수↑
  • oom_score_adj가 높을수록 더 잘 죽음
  • root/특정 보호 설정이 있으면 덜 죽을 수 있음

실시간으로 확인:

PID=1234
cat /proc/$PID/oom_score
cat /proc/$PID/oom_score_adj

운영에서 흔한 실수는, 중요한 데몬에 oom_score_adj를 낮추지 않아서(혹은 반대로 너무 높여서) 핵심 프로세스가 먼저 죽는 상황입니다.

4) “노드 OOM” vs “cgroup OOM(컨테이너 OOM)” 구분

쿠버네티스에서는 다음 두 가지가 자주 혼동됩니다.

  • 노드 OOM(커널 전역 OOM): 노드 전체 메모리 압박 → 커널이 희생 프로세스 선택
  • cgroup OOM(컨테이너 OOM): 특정 cgroup의 memory.max/memory.limit_in_bytes 초과 → 그 cgroup 내 프로세스 kill

4.1 컨테이너 OOMKilled 확인(kubectl)

kubectl describe pod <pod> -n <ns> | egrep -i "oom|killed|reason"

kubectl get pod <pod> -n <ns> -o jsonpath='{.status.containerStatuses[*].lastState.terminated.reason}'
  • Reason: OOMKilled면 cgroup OOM 가능성이 큽니다.
  • 노드 OOM은 파드 이벤트에 명확히 안 남고, 노드 커널 로그에만 남는 경우도 있습니다.

4.2 노드에서 cgroup 메모리 제한 확인

cgroup v2 기준(최근 배포판에서 일반적):

# 현재 프로세스가 속한 cgroup의 메모리 상한
cat /sys/fs/cgroup/memory.max

# 현재 사용량
cat /sys/fs/cgroup/memory.current

# OOM/kill 이벤트 카운트
cat /sys/fs/cgroup/memory.events

cgroup v1이라면:

cat /sys/fs/cgroup/memory/memory.limit_in_bytes
cat /sys/fs/cgroup/memory/memory.usage_in_bytes
cat /sys/fs/cgroup/memory/memory.oom_control

5) OOM 타임라인 복원: “직전 메모리 상태”를 남겨라

OOM은 이미 터지고 난 뒤에는 “왜 그 순간에 메모리가 급증했는지”를 놓치기 쉽습니다. 그래서 사전 관측이 중요합니다.

5.1 vmstat로 압박 징후 확인

vmstat 1 10
  • si/so(swap in/out) 증가: 스왑 압박
  • r(run queue) 증가 + wa(iowait) 증가: 메모리 회수/스왑으로 인한 성능 저하 가능

5.2 /proc/meminfo, slabtop로 커널 메모리 확인

cat /proc/meminfo | egrep -i "MemFree|MemAvailable|Cached|Buffers|Swap|Slab|SReclaimable|SUnreclaim"

# 슬랩 상위 오브젝트 확인
sudo slabtop -o | head -n 30
  • SUnreclaim가 비정상적으로 크면 커널 메모리(드라이버/네트워크/파일시스템) 문제 가능성이 있습니다.

5.3 PSI(Pressure Stall Information)로 “압박의 질” 측정

최근 커널은 PSI를 제공합니다.

cat /proc/pressure/memory
  • some/full이 높게 유지되면, 단순 사용량이 아니라 메모리 때문에 실제로 스톨이 발생 중이라는 뜻입니다.

6) 메모리 누수 진단 로드맵 (언어/런타임별)

OOM의 가장 흔한 원인은 결국 애플리케이션 메모리 누수입니다. 다만 “누수처럼 보이지만 정상”인 경우도 많습니다(캐시, JIT, arena 등). 아래는 실무에서 재현 가능한 진단 순서입니다.

6.1 1단계: RSS 증가가 진짜인지 확인(pmap/smaps)

PID=1234
# 전체 매핑 요약
pmap -x $PID | tail -n 20

# smaps에서 익명/파일/공유 메모리 합계
awk '
  /^Rss:/ {rss+=$2}
  /^Pss:/ {pss+=$2}
  /^Private_Dirty:/ {pd+=$2}
  /^Private_Clean:/ {pc+=$2}
  END {printf("RSS=%d kB PSS=%d kB PrivateDirty=%d kB\n", rss, pss, pd)}
' /proc/$PID/smaps
  • Private_Dirty가 꾸준히 증가하면 누수 가능성이 큽니다.
  • file-rss 위주면 mmap/페이지 캐시/파일 매핑 성격일 수 있습니다.

6.2 2단계: 런타임/언어별 프로파일링

(1) Java: 힙/네이티브 구분이 핵심

  • 힙이 커서 OOM인지, 네이티브(DirectByteBuffer, JNI, thread stack)인지 분리해야 합니다.
# 힙 요약
jcmd <pid> GC.heap_info

# 클래스 히스토그램(급증 객체 확인)
jcmd <pid> GC.class_histogram | head -n 50

# 힙 덤프
jcmd <pid> GC.heap_dump /tmp/heap.hprof

(2) Go: pprof로 힙 추적

애플리케이션에 pprof 엔드포인트를 열어두는 것이 가장 빠릅니다.

# 60초간 힙 프로파일 수집
curl -s "http://127.0.0.1:6060/debug/pprof/heap" > heap.pb.gz

go tool pprof -top ./myapp heap.pb.gz

(3) Python: tracemalloc/objgraph

import tracemalloc
tracemalloc.start()
# ... workload ...
snapshot = tracemalloc.take_snapshot()
for stat in snapshot.statistics('lineno')[:20]:
    print(stat)

(4) Node.js: heap snapshot

kill -USR2 <pid>  # 설정에 따라 inspector/heapdump 트리거

6.3 3단계: “메모리 누수”가 아니라 “메모리 파편화”일 수도

특히 glibc malloc 기반 C/C++ 서비스에서 흔합니다.

  • RSS는 계속 증가하지만 실제로는 free list에 남아 OS로 반환되지 않는 경우
  • MALLOC_ARENA_MAX(스레드 많을수록 arena 증가) 이슈

점검 힌트:

# 스레드 수가 많고 RSS가 비정상적으로 크면 파편화 의심
ps -o pid,thcount,rss,cmd -p 1234

필요 시 jemalloc/tcmalloc 도입, arena 제한, 메모리 반환 정책 튜닝을 검토합니다.

7) 운영에서 바로 쓰는 “OOM 발생 시” 체크리스트

장애 중에는 우선순위가 중요합니다.

  1. 커널 로그 확보: journalctl -k -b 결과를 파일로 저장
  2. 희생 PID/프로세스/컨테이너 식별: Killed process 라인에서 PID/커맨드 확인
  3. cgroup OOM 여부: 파드 이벤트(OOMKilled) 및 memory.events 확인
  4. 메모리 유형 분해: anon-rss 중심인지, slab/캐시가 큰지
  5. 재발 방지 임시조치: limit 상향/스케일아웃/캐시 상한/트래픽 차단
  6. 근본 원인 분석: 힙덤프/pprof/smaps 추출, 누수 재현

쿠버네티스 환경에서는 OOM이 네트워크 장애(502/504 등)처럼 보이기도 합니다. 예를 들어 Ingress 502/504가 반복되는데 파드가 간헐 재시작 중이라면, 네트워크 튜닝 이전에 OOM 이벤트를 먼저 확인해야 합니다(참고: EKS ALB Ingress 504인데 Pod는 정상일 때).

8) OOM 재발 방지: 튜닝 포인트(과하면 독이 됨)

8.1 overcommit 정책 이해

sysctl vm.overcommit_memory
sysctl vm.overcommit_ratio
  • overcommit_memory=0(기본): 휴리스틱
  • 1: 항상 overcommit 허용(대규모 할당 후 폭발 가능)
  • 2: 엄격(메모리 부족을 더 빨리 드러냄)

서비스 특성에 따라 다르며, 무작정 바꾸면 예기치 않은 실패 패턴이 생길 수 있습니다.

8.2 스왑과 swappiness

컨테이너 환경에서는 스왑을 꺼두는 경우가 많지만, 특정 워크로드에는 “완충재”가 되기도 합니다.

swapon --show
sysctl vm.swappiness

8.3 OOM 우선순위 조정(중요 프로세스 보호)

중요한 프로세스를 덜 죽게 설정:

PID=1234
echo -500 | sudo tee /proc/$PID/oom_score_adj

주의: 너무 많은 프로세스를 보호하면 결국 다른 프로세스가 연쇄적으로 죽거나 시스템이 더 불안정해질 수 있습니다.

9) 실전 예시: OOM 로그에서 바로 뽑아내는 “증거 묶음” 스크립트

장애 시점에 증거를 자동 수집해두면 재현이 훨씬 쉬워집니다.

#!/usr/bin/env bash
set -euo pipefail
OUT=${1:-"/tmp/oom-evidence-$(date +%F-%H%M%S)"}
mkdir -p "$OUT"

journalctl -k -b > "$OUT/kernel.log" || true

dmesg -T > "$OUT/dmesg.log" || true

cp /proc/meminfo "$OUT/meminfo" || true
cp /proc/pressure/memory "$OUT/psi-memory" || true

ps auxww --sort=-rss | head -n 50 > "$OUT/top-rss.txt" || true

# slab summary(권한 필요)
(slabtop -o | head -n 50) > "$OUT/slabtop.txt" 2>/dev/null || true

echo "Saved to $OUT"

이 정도만 있어도 “노드 메모리 압박이었는지 / 특정 프로세스가 튀었는지 / 슬랩이 비정상인지”를 빠르게 가를 수 있습니다.

10) 마무리: OOM은 ‘로그 한 줄’이 아니라 ‘원인 분해’ 문제

OOM Killer는 마지막 안전장치이지만, 운영 관점에서는 가장 비싼 장애 신호입니다. 핵심은 다음 3가지입니다.

  • 커널 로그에서 OOM 이벤트를 정확히 수집하고, Killed process의 RSS 분해(anon/file/shmem)로 원인을 좁힌다.
  • cgroup OOM과 노드 OOM을 구분해 “리소스 제한 문제”인지 “실제 누수/부하”인지 판단한다.
  • 런타임별(Go/Java/Python/Node/C++) 프로파일링 도구로 누수를 증명하고, 재발 방지(상한/스케일/튜닝)를 적용한다.

만약 EKS 같은 환경에서 장애가 401/502/504/DNS 실패처럼 보이더라도, 파드 재시작과 OOM 이벤트를 먼저 확인해보세요. 겉으로는 네트워크처럼 보여도, 원인은 메모리일 때가 많습니다.