- Published on
리눅스 OOM Killer로 프로세스 죽을 때 원인 추적
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버에서 프로세스가 아무 메시지 없이 종료되거나, 애플리케이션 로그에는 남는 게 없는데 서비스가 내려가는 경우가 있습니다. 이때 가장 먼저 의심해야 하는 게 OOM Killer 입니다. OOM은 Out Of Memory의 약자이고, 커널이 메모리가 부족해 더 이상 시스템을 유지할 수 없다고 판단하면 특정 프로세스를 강제로 종료해 전체 시스템을 살리는 메커니즘입니다.
문제는 “왜 죽었는지”가 애플리케이션 관점에서는 잘 안 보인다는 점입니다. 이 글에서는 리눅스 환경에서 OOM Killer로 프로세스가 죽을 때, 어떤 로그를 어디서 보고, 어떤 지표를 확인하고, 컨테이너나 쿠버네티스 환경에서는 무엇이 달라지는지까지 원인 추적 루트를 실무 관점으로 정리합니다.
관련해서 쿠버네티스에서 프로세스가 반복적으로 죽는 케이스는 K8s CrashLoopBackOff 원인별 로그·Probe 해결 가이드도 함께 참고하면 연결해서 진단하기 좋습니다.
OOM Killer의 종류부터 구분하기
OOM은 크게 두 갈래로 나뉩니다.
- 시스템 OOM: 노드 전체 메모리가 고갈되어 커널이 프로세스를 죽임
- cgroup OOM: 컨테이너 또는 특정 cgroup의 메모리 한도를 초과해 그 그룹 안에서 프로세스를 죽임
둘은 증상은 비슷하지만 “원인”과 “해결책”이 달라집니다.
- 시스템 OOM은 보통 노드 메모리 과다 사용, swap 설정, 메모리 단편화, 커널 설정 등과 연관됩니다.
- cgroup OOM은 보통 컨테이너 메모리 limit이 너무 낮거나, 애플리케이션이 limit을 고려하지 않고 힙을 키우는 문제, 캐시 폭증 등이 원인입니다.
1단계: 커널 로그에서 OOM 흔적 찾기
가장 먼저 확인할 것은 커널 로그입니다. OOM Killer는 거의 항상 커널 로그에 흔적을 남깁니다.
journalctl로 빠르게 확인
# 최근 부팅 이후 커널 로그에서 OOM 관련 라인만
journalctl -k -b | grep -E "Out of memory|Killed process|oom-killer|oom_reaper"
# 시간 범위를 지정해서 확인
journalctl -k --since "2026-02-24 00:00:00" --until "2026-02-24 23:59:59" | \
grep -E "Out of memory|Killed process|oom-killer|oom_reaper"
dmesg에서도 확인 가능
dmesg -T | grep -E "Out of memory|Killed process|oom-killer|oom_reaper"
로그에서 꼭 읽어야 하는 포인트
OOM 로그는 보통 다음 정보를 줍니다.
- 어떤 프로세스가 죽었는지:
Killed process 12345 (myapp) - 왜 선택됐는지:
oom_score,oom_score_adj - 당시 메모리 상황: free 메모리, anon, file cache, swap 사용량
- cgroup OOM인지 여부:
Memory cgroup out of memory같은 문구
예시 형태(설명용):
Out of memory: Killed process ...Memory cgroup out of memory: Kill process ...
이 두 문구는 의미가 다릅니다. 전자는 시스템 OOM 가능성이 높고, 후자는 cgroup limit 초과가 거의 확정입니다.
2단계: 죽은 프로세스의 메모리 사용량 역추적
OOM 발생 시점에 프로세스가 실제로 얼마나 메모리를 먹었는지, 어떤 종류의 메모리였는지를 알아야 합니다.
프로세스 단위 RSS, VSZ 확인(사후에는 한계)
프로세스가 이미 죽었다면 ps로는 못 봅니다. 대신 다음 중 하나가 필요합니다.
- OOM 직전의 모니터링 지표(프로메테우스, CloudWatch, Datadog 등)
systemd-cgtop,cgroup메모리 통계- 애플리케이션 자체 메모리 덤프 또는 힙 프로파일
그래도 “현재 살아있는 유사 프로세스”가 있다면 패턴을 볼 수 있습니다.
ps -eo pid,comm,rss,vsz,%mem --sort=-rss | head -n 20
RSS는 실제 점유 물리 메모리(대략)VSZ는 가상 메모리(매핑 포함)
OOM 원인 분석에서는 보통 RSS가 더 직접적입니다.
메모리 압박 상황 확인
free -h
vmstat 1 5
cat /proc/meminfo | egrep "MemFree|MemAvailable|SwapTotal|SwapFree|Cached|Buffers"
MemAvailable이 바닥인데 swap도 거의 없으면 시스템 OOM 확률이 큽니다.- swap이 충분한데도 OOM이 났다면
vm.swappiness, cgroup 제한, 또는 특정 메모리 유형(예: 커널 메모리 압박)을 의심해야 합니다.
3단계: cgroup OOM 여부 확인(컨테이너 포함)
컨테이너 환경에서 가장 흔한 케이스는 “노드는 메모리가 남아 있는데 내 컨테이너만 죽는” 상황입니다. 이는 대개 cgroup limit 때문입니다.
systemd 기반 호스트에서 cgroup 메모리 확인
프로세스가 속한 cgroup을 찾고, 해당 cgroup의 메모리 제한과 사용량을 확인합니다.
# PID가 살아있을 때만 가능
PID=12345
cat /proc/$PID/cgroup
cgroup v2 환경이라면 보통 0::/some/path 형태가 나오고, 해당 경로에서 메모리를 봅니다.
CGPATH="/sys/fs/cgroup/some/path"
cat $CGPATH/memory.current
cat $CGPATH/memory.max
cat $CGPATH/memory.events
memory.max가 숫자면 제한이 있는 상태memory.current가memory.max에 근접하거나 초과 직전이었다면 cgroup OOM 가능성이 큼memory.events에서oom또는oom_kill카운트가 증가했는지 확인
쿠버네티스에서의 확인 포인트
쿠버네티스에서는 Pod가 OOM으로 죽으면 흔히 컨테이너 종료 사유가 OOMKilled로 찍힙니다.
kubectl describe pod <pod-name>
# 마지막 종료 사유만 빠르게
kubectl get pod <pod-name> -o jsonpath='{.status.containerStatuses[0].lastState.terminated.reason}'
주의할 점은, OOMKilled는 “컨테이너 limit을 넘어서 죽었다”에 가까운 의미로 쓰이는 경우가 많지만, 노드 OOM으로 인해 해당 컨테이너 프로세스가 희생된 경우도 있을 수 있습니다. 그래서 커널 로그(노드)와 함께 보는 게 안전합니다.
쿠버네티스 운영 중 이미지 풀 실패나 인증 만료로 장애가 섞여 보일 때는 Kubernetes ImagePullBackOff - ECR 인증 만료 해결처럼 다른 원인도 동시에 배제해 두는 것이 좋습니다.
4단계: OOM Killer가 왜 “그 프로세스”를 죽였는지 이해하기
OOM Killer는 아무 프로세스나 죽이지 않습니다. 커널은 점수 기반으로 희생자를 고릅니다.
oom_score: 메모리를 많이 먹고, 시스템에 덜 중요한 프로세스일수록 높아지기 쉬움oom_score_adj: 관리자가 조정 가능한 가중치
확인은 다음처럼 합니다.
PID=12345
cat /proc/$PID/oom_score
cat /proc/$PID/oom_score_adj
운영 관점에서 중요한 서비스가 자주 희생된다면 다음을 검토합니다.
- 해당 프로세스가 진짜로 메모리를 과다 사용했는지(근본 원인)
- 다른 프로세스가 더 큰 메모리를 먹었는데도 살아남는 이유가 있는지(예:
oom_score_adj) - systemd 서비스라면
OOMScoreAdjust=설정 여부
다만 oom_score_adj로 “안 죽게 만들기”는 임시방편입니다. 메모리 고갈이 계속되면 결국 커널은 더 위험한 대상을 죽이거나 시스템이 불안정해질 수 있습니다.
5단계: 애플리케이션 관점의 원인 분류(실무에서 많이 나오는 패턴)
OOM의 근본 원인은 대체로 다음 중 하나로 귀결됩니다.
1) 메모리 릭(누수)
- 트래픽이 일정해도 RSS가 계속 우상향
- 재시작하면 메모리가 떨어지고 다시 증가
대응
- 언어별 힙 프로파일 수집
- 캐시, 전역 컬렉션, 큐 적체, 이벤트 리스너 누수 점검
2) 캐시 폭증 또는 제한 없는 버퍼
- LRU 없는 in-memory 캐시
- 요청 바디를 전부 메모리에 올리는 구현
- 압축 해제, 이미지 처리 등에서 순간 피크
대응
- 캐시 상한, TTL, eviction 정책
- 스트리밍 처리로 변경
3) 컨테이너 limit 대비 런타임 힙 설정 미스
대표적으로 JVM, Node.js가 컨테이너 메모리 limit을 제대로 반영하지 않거나, 기본값이 너무 커서 limit을 넘는 경우가 있습니다.
예: Node.js는 --max-old-space-size 미설정 시 컨테이너 환경에서 기대와 다르게 힙을 잡을 수 있습니다.
# 예시: 512MB 근처로 힙 상한을 두고, 나머지는 네이티브/버퍼를 고려해 여유를 둠
node --max-old-space-size=384 server.js
JVM은 컨테이너 인식 옵션과 MaxRAMPercentage 등을 점검합니다.
# 예시: 컨테이너 메모리의 70%까지만 힙 사용
java -XX:MaxRAMPercentage=70 -jar app.jar
4) 파일 캐시가 커져서 사용자 메모리를 밀어내는 경우
리눅스는 파일 캐시를 적극적으로 사용합니다. “메모리가 꽉 차 보이는데도 정상”인 경우가 많지만, 특정 워크로드에서는 캐시가 과도해져 anon 메모리(애플리케이션 힙)가 밀리면서 OOM으로 이어질 수 있습니다.
이때는 OOM 시점 로그의 anon-rss와 file-rss 비중을 보고 판단합니다.
6단계: 재현 가능한 진단을 위한 최소한의 계측
OOM은 “그 순간”이 중요합니다. 사후에 남는 정보가 제한적이기 때문에 다음 계측을 추천합니다.
1) 커널 로그 보존
journald보존 기간 및 디스크 사용량 제한 확인- 노드 재부팅 시 로그 유실 방지
2) 주기적 메모리 스냅샷
간단히는 cron으로 상위 메모리 프로세스를 주기 저장해도 도움이 됩니다.
# 1분마다 상위 20개 RSS를 로그로 남기는 예시
( date; ps -eo pid,comm,rss,vsz,%mem --sort=-rss | head -n 20; echo ) >> /var/log/top-mem.log
3) 컨테이너 메모리 이벤트 수집
cgroup v2라면 memory.events의 oom_kill 증가를 모니터링하면 “죽기 직전”을 잡을 수 있습니다.
CGPATH="/sys/fs/cgroup/kubepods.slice/..."
watch -n 1 "cat $CGPATH/memory.current; echo; cat $CGPATH/memory.events"
7단계: 해결 전략 체크리스트
원인 추적이 끝나면 해결은 보통 아래 중 하나로 수렴합니다.
- 애플리케이션 메모리 사용 상한 설정(런타임 힙 제한)
- 캐시, 큐, 버퍼에 상한 추가
- 메모리 릭 수정 및 프로파일 기반 회귀 테스트
- 컨테이너
requests와limits재설계 - 노드 사이징 조정, swap 정책 재검토
- 동시성 제한(한 번에 처리하는 작업 수 제한)
쿠버네티스에서는 특히 requests를 낮게 잡고 limits만 타이트하게 잡으면, 스케줄링은 많이 되는데 실제로는 노드가 메모리 압박을 받는 형태가 나오기 쉽습니다. OOM이 간헐적으로만 발생한다면 피크 트래픽 시점의 동시성, 배치 작업, 사이드카 메모리 사용량까지 같이 보세요.
마무리: “OOM 로그 한 줄”에서 시작해 구조적으로 좁혀가기
OOM Killer는 결과이지 원인이 아닙니다. 커널 로그에서 Killed process 라인을 찾고, 시스템 OOM인지 cgroup OOM인지부터 분기한 다음, 메모리 사용 패턴을 계측으로 붙잡아야 재발을 막을 수 있습니다.
- 노드 커널 로그로 OOM 유형 판별
- cgroup 메모리 제한과 이벤트 확인
- 애플리케이션 힙과 캐시 상한을 명시적으로 설계
- 재현 가능한 계측을 추가해 “다음 OOM”을 증거로 남기기
이 과정을 한 번 체계화해두면, “갑자기 죽었다” 같은 장애도 로그와 수치로 설명 가능한 사건으로 바뀌고, 해결도 훨씬 빨라집니다.