Published on

리눅스 OOM Kill 원인 추적 - dmesg·cgroup·journalctl

Authors

서버가 갑자기 죽거나(프로세스가 사라지거나) Pod가 재시작되는 사건의 상당수는 OOM(Out-Of-Memory) Kill로 귀결됩니다. 문제는 **“OOM이 났다”**가 아니라, **“왜 OOM이 났고, 커널이 왜 하필 그 프로세스를 죽였는지”**가 핵심이라는 점입니다.

이 글은 리눅스에서 OOM Kill의 원인을 추적할 때 가장 실전적인 3축—dmesg(커널 로그), cgroup(메모리 한계/압박), journalctl(서비스/컨테이너 관점 로그)—을 결합해 원인→근거→재발 방지까지 이어지는 흐름으로 정리합니다.

OOM Kill의 종류부터 구분하기

OOM Kill은 크게 두 갈래로 나뉩니다.

  1. Global OOM (시스템 전체 메모리 고갈)

    • 호스트 전체에서 reclaim이 실패하고 커널 OOM killer가 발동
    • 대개 dmesgOut of memory: Kill process ... 형태로 남음
  2. cgroup OOM (컨테이너/서비스 단위 메모리 제한 초과)

    • 시스템 메모리는 남아도 cgroup memory.max(또는 limit_in_bytes) 를 넘으면 해당 cgroup 안에서 OOM
    • 쿠버네티스라면 Pod OOMKilled로 보이고, 노드 전체는 멀쩡한 경우가 많음

둘을 구분하는 게 첫 단추입니다. “노드가 메모리 넉넉한데 Pod만 죽는다”면 cgroup OOM 가능성이 큽니다.

1단계: dmesg로 커널 OOM 이벤트 증거 확보

가장 먼저 커널이 OOM을 선언했는지 확인합니다.

# 최근 커널 로그에서 OOM 관련 메시지 찾기
sudo dmesg -T | egrep -i 'out of memory|oom-killer|killed process|oom_reaper|memory cgroup'

# 부팅 이후 전체가 길면, journal 기반 커널 로그도 가능
sudo journalctl -k -b | egrep -i 'out of memory|oom-killer|killed process|memory cgroup'

dmesg에서 꼭 봐야 할 패턴

  • Out of memory: Kill process 1234 (java) score 987 or sacrifice child
  • Killed process 1234 (java) total-vm:... anon-rss:... file-rss:... shmem-rss:...
  • oom-kill:constraint=CONSTRAINT_MEMCG ... task_memcg=/kubepods/...

특히 constraint=CONSTRAINT_MEMCG 또는 task_memcg=가 보이면 cgroup 제한에 의한 OOM 가능성이 매우 높습니다.

OOM 로그 해석 포인트(실전)

  • anon-rss: 익명 메모리(힙/스택). 누수/캐시 아닌 “진짜 사용”일 확률 높음
  • file-rss: 파일 매핑(페이지 캐시/매핑). 워크로드 특성일 수 있음
  • total-vm: 가상 메모리. 실제 RSS와 다르니 과대평가 금지
  • score: OOM 점수. 높을수록 죽을 확률 큼(보통 메모리 많이 쓰고 보호 설정이 낮음)

2단계: journalctl로 ‘누가 죽었는지’와 서비스 영향 확인

커널이 죽인 프로세스가 systemd 서비스인지, 애플리케이션이 자체적으로 크래시한 뒤 OOM이 따라온 건지(순서 중요)를 확인해야 합니다.

# 특정 부팅에서 OOM/kill/segfault 등 흔한 키워드로 검색
sudo journalctl -b --no-pager | egrep -i 'oom|killed process|out of memory|segfault|fatal|panic'

# 서비스 단위로 타임라인 확인
sudo journalctl -u myservice -b --since "-2h" --no-pager

# PID가 로그에 있다면 해당 PID 전후로 확인
sudo journalctl -b _PID=1234 --no-pager

systemd로 실행되는 서비스라면

systemd는 OOM 이후 재시작을 걸어두는 경우가 많아 “갑자기 재시작”처럼 보입니다. Restart=always 같은 설정이 있다면 OOM이 원인인데도 증상이 재시작으로만 관측될 수 있습니다.

3단계: cgroup 메모리 제한/압박 상태 확인 (v2 기준)

컨테이너/서비스 단위 OOM은 cgroup 설정을 보면 거의 결론이 납니다. 최근 배포판은 대개 cgroup v2를 씁니다.

내 프로세스가 속한 cgroup 경로 찾기

PID=1234
cat /proc/$PID/cgroup

cgroup v2라면 보통 0::/some/path 형태가 나옵니다. 그 경로를 기준으로 메모리 상태 파일을 읽습니다.

memory.max / memory.current / memory.events 확인

CG=/sys/fs/cgroup/some/path

# 제한(없으면 "max")
cat $CG/memory.max

# 현재 사용량
cat $CG/memory.current

# OOM/pressure 이벤트 카운터
cat $CG/memory.events

# 상세 통계(anon/file/cache 등)
cat $CG/memory.stat | head

memory.events에서 다음이 중요합니다.

  • oom: cgroup OOM 발생 횟수
  • oom_kill: 실제 kill이 발생한 횟수
  • high: memory.high(소프트 제한) 초과 이벤트(스로틀/리클레임 유도)

예를 들어 memory.max가 2GiB인데 memory.current가 그 근처까지 치솟고 oom_kill이 증가한다면, 원인은 거의 확정입니다.

cgroup v1(구형)이라면

# v1은 보통 이런 경로
CG=/sys/fs/cgroup/memory/some/path
cat $CG/memory.limit_in_bytes
cat $CG/memory.usage_in_bytes
cat $CG/memory.failcnt
cat $CG/memory.stat | head

failcnt가 증가하면 제한에 걸려 할당 실패가 났다는 뜻입니다.

4단계: “왜 저 프로세스가 죽었나” — OOM score/adj 확인

OOM killer는 후보 프로세스에 점수를 매겨 죽입니다. 같은 OOM이라도 어떤 프로세스가 희생되는지는 설정에 따라 달라집니다.

PID=1234
cat /proc/$PID/oom_score
cat /proc/$PID/oom_score_adj
  • oom_score_adj는 -1000(거의 보호) ~ 1000(우선 제거) 범위
  • DB나 핵심 데몬을 보호하려고 oom_score_adj=-900 같은 튜닝을 하기도 하지만, 근본 해결(메모리 계획/누수 제거/limit 조정) 없이 보호만 하면 다른 프로세스가 대신 죽어 장애가 더 복잡해질 수 있습니다.

5단계: 메모리 압박(pressure)과 스왑/리클레임 상황 확인

OOM은 “순간 피크”로도 나고, “지속 압박”으로도 납니다. PSI(Pressure Stall Information)를 보면 지속 압박을 정량화할 수 있습니다.

# 시스템 전체 메모리 압박
cat /proc/pressure/memory

# 스왑/메모리 개요
free -h
swapon --show
vmstat 1 5

# 프로세스별 RSS 상위
ps -eo pid,comm,rss,pmem --sort=-rss | head
  • vmstat에서 si/so(swap in/out)가 지속적으로 발생하면 이미 성능 저하 구간일 수 있습니다.
  • 스왑이 꺼져 있는 환경(특히 컨테이너 노드)에서는 OOM이 더 빨리 발생합니다.

6단계: 쿠버네티스/EKS에서의 실전 추적 포인트

K8s에서는 OOM이 노드 OOM인지 Pod cgroup OOM인지 구분이 특히 중요합니다.

Pod가 OOMKilled인지 확인

kubectl describe pod <pod> -n <ns>
kubectl get pod <pod> -n <ns> -o jsonpath='{.status.containerStatuses[*].lastState.terminated.reason}'

OOMKilled가 뜨면 거의 항상 컨테이너 메모리 limit와 맞물립니다. 이때는 노드 dmesg에서 task_memcg=/kubepods/...를 찾아 매칭하면 확실해집니다.

EKS 운영 중이라면 네트워크/스토리지 문제로 Pod가 뜨지 않는 케이스도 자주 섞여 들어오는데, OOM과 증상이 비슷하게 “계속 재시작/대기”로 보일 수 있습니다. 그런 경우 아래 글처럼 빠르게 분기 진단하는 습관이 도움이 됩니다: EKS Pod가 ContainerCreating에 멈출 때 10분 진단

7단계: 원인별로 ‘재발 방지’ 처방하기

OOM 원인 추적의 목적은 “로그 찾기”가 아니라 “다음엔 안 터지게”입니다. 아래는 자주 만나는 원인과 처방을 연결한 체크리스트입니다.

1) 단순히 limit이 너무 낮다 (cgroup OOM)

  • 증거: memory.max가 작고 memory.eventsoom_kill 증가
  • 처방:
    • 워크로드 피크를 반영해 limit 상향
    • memory.high를 활용해 소프트 스로틀로 급격한 OOM 완화
    • 애플리케이션 캐시/버퍼 상한 설정(예: JVM -Xmx, Node --max-old-space-size)

2) 메모리 누수/비정상 증가

  • 증거: 시간에 따라 anon-rss가 계속 증가, 재시작하면 정상화
  • 처방:
    • 힙 덤프/프로파일링, 누수 지점 수정
    • 배치/쿼리 폭주가 원인이라면 쿼리 튜닝이 더 직접적인 해결책일 수 있음

DB가 같이 올라간 서버에서 OOM이 반복된다면, “DB가 느려져서 워커가 쌓이고 메모리가 불어나는” 연쇄도 흔합니다. Postgres라면 실행 계획/슬로우쿼리 계측부터 잡는 게 지름길인 경우가 많습니다: PostgreSQL 느린 쿼리 튜닝 - auto_explain+pg_stat_statements

3) 페이지 캐시/파일 매핑이 커져서 밀려난다

  • 증거: file-rss/cache 비중이 크고, I/O 패턴이 강함
  • 처방:
    • 애플리케이션이 mmap을 과도하게 쓰는지 확인
    • 컨테이너 환경이면 emptyDir/로그 파일 폭증 등도 점검
    • 무작정 drop_caches는 응급처치일 뿐(재발 방지 아님)

4) 노드 전체 메모리 부족 (Global OOM)

  • 증거: 여러 프로세스가 동시에 메모리 압박, dmesg에 글로벌 OOM
  • 처방:
    • 노드/인스턴스 메모리 증설 또는 워크로드 분산
    • 쿠버네티스면 리소스 requests/limits 재정렬(오버커밋 축소)
    • 스왑 정책 검토(가능한 환경에서만)

빠르게 끝내는 “현장용” 원인 추적 플레이북

아래 순서대로 하면, 대부분의 OOM 사건은 10~20분 내에 1차 결론이 납니다.

# 1) 커널 OOM 증거
sudo dmesg -T | egrep -i 'out of memory|oom-killer|killed process|memory cgroup' | tail -n 50

# 2) 서비스/프로세스 타임라인
sudo journalctl -b --no-pager | egrep -i 'killed process|oom|out of memory' | tail -n 200

# 3) (PID를 찾았다면) cgroup 경로
PID=1234
cat /proc/$PID/cgroup

# 4) (cgroup v2) 제한/이벤트
CG=/sys/fs/cgroup/<path-from-above>
cat $CG/memory.max
cat $CG/memory.current
cat $CG/memory.events
cat $CG/memory.stat | egrep 'anon|file|slab|kernel|sock' | head

# 5) OOM score
cat /proc/$PID/oom_score
cat /proc/$PID/oom_score_adj

여기서 결론은 보통 둘 중 하나입니다.

  • constraint=CONSTRAINT_MEMCG + memory.max 근접 + oom_kill 증가 → limit 조정/앱 메모리 상한 설정
  • 글로벌 OOM + 여러 프로세스 압박 → 노드 용량/오버커밋/워크로드 분산

마무리: “OOM은 버그가 아니라 설계의 결과”

OOM Kill은 커널이 마지막으로 선택하는 안전장치입니다. 그래서 “누가 죽었는지”만 보면 늘 사후 처리로 끝나고, “왜 그 시점에 메모리가 부족해졌는지(또는 cgroup 한계를 넘었는지)”를 증거로 연결해야 재발을 막을 수 있습니다.

dmesg로 커널의 판결문을 읽고, journalctl로 서비스 타임라인을 맞추고, cgroup으로 제한/압박을 수치로 확인하면 OOM 원인 추적은 감이 아니라 재현 가능한 분석이 됩니다.