리눅스 커널은 메모리가 완전히 고갈되면 시스템 전체가 멈추는 것을 막기 위해 OOM(Out Of Memory) Killer를 실행한다. OOM Killer는 메모리를 가장 많이 쓰고 있는 프로세스를 골라서 SIGKILL(9) 신호로 강제 종료한다.
프로세스 입장에서는 아무 예고 없이 죽기 때문에, 로그도 남기지 못하고 사라진다. "분명 실행해놨는데 없어졌다"는 상황이 바로 이것이다.
OOM Killer가 작동하면 dmesg나 /var/log/messages에 아래와 같은 로그가 남는다.
[ 3412.876543] python3 invoked oom-killer: gfp_mask=0x6200ca(GFP_HIGHUSER_MOVABLE), order=0
[ 3412.876545] Out of memory: Killed process 1234 (python3) total-vm:524288kB, anon-rss:131072kB
[ 3412.876547] oom_reaper: reaped process 1234 (python3), now anon-rss:0kB
| 로그 항목 | 의미 |
|---|---|
Killed process 1234 (python3) | 죽은 프로세스 PID와 이름 |
total-vm | 가상 메모리 사용량 |
anon-rss | 실제 물리 메모리 사용량 — 이게 핵심 |
프로세스가 갑자기 죽었을 때, OOM Killer가 범인인지 확인하는 방법이다.
# dmesg에서 OOM 관련 로그 검색 $ dmesg | grep -i "oom\|killed process\|out of memory" [ 3412.876543] python3 invoked oom-killer: gfp_mask=0x6200ca(GFP_HIGHUSER_MOVABLE) [ 3412.876545] Out of memory: Killed process 1234 (python3) total-vm:524288kB, anon-rss:131072kB # 시간 포함해서 보기 $ dmesg -T | grep -i "killed process" [Sat Mar 22 14:23:45 2026] Out of memory: Killed process 1234 (python3)
# /var/log/messages에서 확인 $ grep -i "oom\|killed process" /var/log/messages Mar 22 14:23:45 server1 kernel: Out of memory: Killed process 1234 (python3) # journalctl로 확인 (systemd 환경) $ journalctl -k | grep -i "oom\|killed process" Mar 22 14:23:45 server1 kernel: Out of memory: Killed process 1234 (python3)
Docker 컨테이너라면 호스트 dmesg가 아니라 Docker 자체에서 OOM 여부를 확인할 수 있다.
# OOM으로 죽었는지 확인 $ docker inspect oom-lab --format '{{.State.OOMKilled}}' true # exit code 137 = OOM (128 + SIGKILL 9) $ docker inspect oom-lab --format '{{.State.ExitCode}}' 137
dmesg는 커널 링 버퍼라서 재부팅하면 사라진다. 영구 기록은 /var/log/messages나 journalctl에서 확인해야 한다.OOM Killer는 아무 프로세스나 죽이지 않는다. 각 프로세스에 oom_score라는 점수를 매기고, 점수가 가장 높은 프로세스를 죽인다. 점수 범위는 0에서 1000까지이며, 1000에 가까울수록 먼저 죽는다.
# 특정 프로세스의 oom_score $ cat /proc/1234/oom_score 666 # RSS(실제 물리 메모리) 기준 상위 프로세스 $ ps -eo pid,comm,rss --sort=-rss | head -10 PID COMMAND RSS 1234 python3 131072 456 mysqld 65536 789 nginx 32768
# oom_score가 높은 순으로 정렬 $ for pid in $(ps -eo pid --no-headers); do if [ -f /proc/$pid/oom_score ]; then score=$(cat /proc/$pid/oom_score 2>/dev/null) cmd=$(cat /proc/$pid/cmdline 2>/dev/null | tr '\0' ' ' | head -c 30) [ -n "$cmd" ] && printf "PID %-6s score: %-5s %s\n" "$pid" "$score" "$cmd" fi done | sort -t: -k2 -rn | head -10 PID 1234 score: 666 python3 memory_leak.py PID 456 score: 333 /usr/sbin/mysqld PID 789 score: 167 nginx: worker process
| oom_score 계산 기준 | 설명 |
|---|---|
| 물리 메모리(RSS) 사용량 | 높을수록 점수가 높음 |
| 점수 범위 | 0 ~ 1000 (1000이면 가장 먼저 죽음) |
| root 프로세스 | 약간의 감점 보너스를 받음 |
oom_score는 읽기 전용이다. 커널이 메모리 사용량 기반으로 자동 계산한다. 우리가 조절할 수 있는 건 oom_score_adj다 (시나리오 4에서 설명).OOM이 발생했다면, 어떤 프로세스가 메모리를 다 잡아먹었는지 찾아야 한다. 핵심은 VSZ(가상)가 아니라 RSS(실제 물리 메모리)를 봐야 한다는 것이다.
$ ps aux --sort=-rss | head -10 USER PID %CPU %MEM VSZ RSS TTY STAT COMMAND root 1234 5.2 62.3 524288 131072 ? S python3 memory_leak.py mysql 456 1.2 31.1 262144 65536 ? S /usr/sbin/mysqld root 789 0.5 15.5 131072 32768 ? S nginx: worker process
$ cat /proc/1234/status | grep -i "vm\|rss" VmPeak: 524288 kB ← 최대 가상 메모리 VmSize: 524288 kB ← 현재 가상 메모리 VmRSS: 131072 kB ← 실제 물리 메모리 (핵심!) VmSwap: 0 kB ← 스왑 사용량
$ free -h total used free shared buff/cache available Mem: 7.8Gi 7.1Gi 102Mi 12Mi 580Mi 412Mi Swap: 1.0Gi 980Mi 44Mi $ grep -E "MemTotal|MemFree|MemAvailable|SwapTotal|SwapFree|Committed_AS" /proc/meminfo MemTotal: 8025636 kB MemFree: 104448 kB MemAvailable: 422912 kB ← 이게 0에 가까우면 위험 SwapTotal: 1048576 kB SwapFree: 45056 kB ← 스왑도 거의 다 참 Committed_AS: 9437184 kB ← overcommit 포함
MemAvailable이 전체의 10% 이하면 위험. SwapFree가 0에 가까우면 OOM 직전. Committed_AS가 MemTotal + SwapTotal보다 크면 overcommit 상태다.$ top -o %MEM -b -n 1 | head -15 top - 14:23:45 up 3 days, 5:12, 1 user MiB Mem : 7838.5 total, 102.0 free, 7156.5 used, 580.0 buff/cache MiB Swap: 1024.0 total, 44.0 free, 980.0 used. 412.0 avail Mem PID USER PR NI VIRT RES SHR S %CPU %MEM COMMAND 1234 root 20 0 512000 131072 1024 S 5.2 62.3 python3 456 mysql 20 0 256000 65536 4096 S 1.2 31.1 mysqld 789 root 20 0 128000 32768 2048 S 0.5 15.5 nginx
VSZ/VIRT(가상 메모리)가 아닌 RSS/RES(실제 물리 메모리)를 봐야 한다. VSZ는 예약만 한 메모리를 포함하기 때문에 실제 사용량과 다르다.중요한 프로세스(DB, 웹서버 등)가 OOM Killer에 죽지 않도록 보호하는 방법이다.
# 현재 값 확인 $ cat /proc/$(pgrep mysqld)/oom_score_adj 0 # -1000으로 설정 → OOM Killer 대상에서 제외 $ echo -1000 > /proc/$(pgrep mysqld)/oom_score_adj # 확인 — oom_score가 0으로 떨어짐 $ cat /proc/$(pgrep mysqld)/oom_score 0
| oom_score_adj 값 | 의미 |
|---|---|
| -1000 | OOM Killer 대상에서 완전 제외 (절대 보호) |
| 0 | 기본값 — 커널이 알아서 점수 매김 |
| +1000 | 최우선 제거 대상 — 이 프로세스부터 죽임 |
위 방법은 재부팅하면 초기화된다. systemd 서비스에 영구적으로 설정하려면 오버라이드 파일을 만들어야 한다.
# mysqld 서비스 오버라이드 파일 생성 $ systemctl edit mysqld # 에디터에서 아래 내용 추가: [Service] OOMScoreAdjust=-1000 # 적용 확인 $ systemctl cat mysqld | grep OOMScore OOMScoreAdjust=-1000 # 서비스 재시작 $ systemctl restart mysqld # 확인 $ cat /proc/$(pgrep mysqld)/oom_score_adj -1000
OOM이 자주 발생한다면 swap 공간을 추가해서 시간을 벌 수 있다. 다만 swap은 응급 조치일 뿐이다.
$ dd if=/dev/zero of=/swapfile bs=1M count=2048 2048+0 records in 2048+0 records out $ chmod 600 /swapfile $ mkswap /swapfile Setting up swapspace version 1, size = 2 GiB $ swapon /swapfile # 확인 $ free -h total used free Swap: 3.0Gi 980Mi 2.0Gi # 재부팅 후에도 유지 $ echo '/swapfile swap swap defaults 0 0' >> /etc/fstab
리눅스는 기본적으로 실제 메모리보다 더 많은 메모리를 할당할 수 있게 허용한다(overcommit). 이 정책을 변경하면 OOM 자체를 예방할 수 있다.
# 현재 설정 확인 $ cat /proc/sys/vm/overcommit_memory 0 # 0 = 커널이 휴리스틱으로 판단 (기본값) # 1 = 무조건 허용 (위험!) # 2 = 물리 메모리 + swap 이상 할당 금지 (안전) # 2로 변경 → OOM 대신 malloc 실패(ENOMEM) 발생 $ sysctl -w vm.overcommit_memory=2 # 영구 설정 $ echo 'vm.overcommit_memory=2' >> /etc/sysctl.d/99-oom.conf $ sysctl -p /etc/sysctl.d/99-oom.conf
overcommit_memory=2로 설정하면 OOM Killer가 작동할 일이 거의 없어진다. 대신 메모리 할당 요청이 거부(ENOMEM)되므로, 애플리케이션이 이를 처리할 수 있어야 한다. Redis 등 일부 애플리케이션은 overcommit_memory=1을 권장하니 확인 후 설정하라.실제로 OOM Killer를 발생시켜 보자. 메모리를 128MB로 제한한 컨테이너에서 200MB를 할당하면 OOM이 발생한다.
# 메모리 128MB 제한 컨테이너 실행 $ docker run -d --name oom-lab --memory=128m --memory-swap=128m rockylinux:9 sleep infinity # 메모리 제한 확인 $ docker stats oom-lab --no-stream --format "{{.MemUsage}}" 688KiB / 128MiB # 200MB 할당 시도 → OOM 발생! $ docker exec oom-lab python3 -c "a = ' ' * (200 * 1024 * 1024)" (프로세스가 SIGKILL로 종료, 출력 없음) # exit code 확인 $ echo $? 137 # OOM 확인 $ docker inspect oom-lab --format '{{.State.OOMKilled}}' true
# OOM 로그 확인 $ dmesg -T | grep -i "oom\|killed process" $ journalctl -k | grep -i "oom\|killed process" $ grep -i "oom\|killed process" /var/log/messages # 메모리 상태 확인 $ free -h $ cat /proc/meminfo # 메모리 많이 쓰는 프로세스 (RSS 기준) $ ps aux --sort=-rss | head -10 # oom_score 확인 $ cat /proc/<PID>/oom_score $ cat /proc/<PID>/oom_score_adj # 프로세스 OOM 보호 (-1000 = 제외) $ echo -1000 > /proc/<PID>/oom_score_adj # systemd 서비스 OOM 보호 (영구) $ systemctl edit <서비스명> # → [Service] OOMScoreAdjust=-1000 # swap 확인 & 추가 $ swapon --show $ dd if=/dev/zero of=/swapfile bs=1M count=2048 $ chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile # overcommit 정책 확인 $ cat /proc/sys/vm/overcommit_memory # Docker 컨테이너 OOM 확인 $ docker inspect <컨테이너> --format '{{.State.OOMKilled}}'
| 항목 | 버전 |
|---|---|
| OS | Rocky Linux 9.3 (Blue Onyx) |
| Kernel | 5.14 |
| Docker | 메모리 제한(--memory=128m)으로 OOM 재현 |
dmesg는 커널 링 버퍼로, 크기가 제한되어 있고 재부팅하면 사라진다. 로그가 덮여 쓸 수 있으므로, /var/log/messages나 journalctl -k에서 영구 기록을 확인하라. 이것도 없다면 journalctl --vacuum-size로 로그가 잘려나간 게 아닌지 확인한다.-1000으로 설정된 프로세스는 OOM Killer 대상에서 제외된다. 하지만 모든 프로세스를 -1000으로 설정하면 안 된다 — 커널이 죽일 프로세스를 찾지 못하면 시스템 전체가 멈추는 커널 패닉이 발생할 수 있다.kill -9로 수동 종료한 경우에도 137이 나온다. OOM인지 확인하려면 반드시 dmesg나 journalctl에서 "Killed process" 로그를 확인해야 한다. Docker 환경이라면 docker inspect --format '{{.State.OOMKilled}}'로 직접 확인 가능하다.overcommit_memory=2는 물리 메모리 + swap 이상의 메모리를 할당하지 않겠다는 의미다. OOM 발생 확률이 크게 줄어들지만, 대신 메모리 할당 요청이 거부(ENOMEM)될 수 있다. 애플리케이션이 ENOMEM을 제대로 처리하지 못하면 다른 방식으로 크래시할 수 있다.dmesg -T | grep "killed process"로 OOM 발생 여부를 확인한다 — exit code 137은 SIGKILL을 의미한다ps aux --sort=-rss로 메모리 범인을 찾고, free -h와 /proc/meminfo로 시스템 상태를 파악한다oom_score_adj=-1000으로 보호하고, systemd의 OOMScoreAdjust로 영구 설정한다