Published on

리눅스 OOM Killer로 프로세스 죽음 진단·방지

Authors

서버에서 애플리케이션이 예고 없이 종료되고, 애플리케이션 로그에는 아무것도 남지 않는 상황이 종종 있습니다. Killed 한 줄만 찍히거나, systemd가 서비스 재시작을 반복하는데 원인이 불명확한 경우도 많습니다. 이때 가장 먼저 의심해야 할 것이 리눅스 OOM(Out-Of-Memory) Killer입니다.

OOM Killer는 “메모리가 부족하니 누군가를 죽여서라도 시스템을 살리자”라는 커널의 마지막 안전장치입니다. 즉, 프로세스 입장에서는 억울하게 죽지만, 시스템 입장에서는 전체 다운을 막는 선택입니다. 이 글에서는 OOM Killer로 인한 프로세스 죽음을 정확히 진단하고, 서비스/호스트/Kubernetes 레벨에서 방지하는 실전 방법을 정리합니다.

> systemd 재시작 루프가 함께 보인다면 원인 분석에 도움이 됩니다: systemd 서비스 무한 재시작 - Exit code 203 해결

OOM Killer가 발생하는 전형적인 증상

다음 중 하나라도 해당하면 OOM을 강하게 의심할 수 있습니다.

  • 프로세스가 갑자기 종료되며 터미널에 Killed만 출력
  • 애플리케이션 로그에는 스택트레이스/에러 없이 종료
  • dmesg 또는 /var/log/kern.logOut of memory: / Killed process 기록
  • Kubernetes에서는 Pod가 OOMKilled로 재시작 (kubectl describe pod에서 확인)
  • 노드에서 메모리 사용량이 지속적으로 상승하다가 특정 시점에 급락(프로세스 강제 종료)

핵심은 애플리케이션이 “정상 종료”한 것이 아니라 커널이 강제 종료했다는 점입니다.

OOM Killer의 동작 원리(최소한으로 알아야 할 것)

리눅스는 메모리가 부족해지면 다음을 시도합니다.

  1. 페이지 캐시 회수(reclaim)
  2. 스왑이 있으면 스왑 아웃(성능 저하 발생)
  3. 그래도 부족하면 OOM Killer 발동 → 특정 프로세스를 골라 SIGKILL로 종료

OOM Killer는 “누구를 죽일지”를 점수화하여 결정합니다. 커널 로그에는 보통 다음 정보가 함께 찍힙니다.

  • 어떤 프로세스를 죽였는지(PID, 이름)
  • 해당 프로세스의 메모리 사용량(RSS 등)
  • 당시 메모리 상태(Free, Cache, Swap, cgroup 메모리 등)

특히 컨테이너 환경(cgroup)에서는 노드 전체 OOM이 아니라 **cgroup OOM(컨테이너 한도 초과)**으로도 프로세스가 죽습니다. 이 둘은 진단/대응 방식이 다릅니다.

1단계: 커널 로그로 “OOM이 맞는지” 확정하기

가장 먼저 해야 할 일은 커널 로그에서 OOM 이벤트를 찾는 것입니다.

dmesg로 확인

sudo dmesg -T | egrep -i "out of memory|oom|killed process|oom-killer" | tail -n 50

journalctl로 확인(systemd 환경)

sudo journalctl -k --since "-2h" | egrep -i "out of memory|oom|killed process|oom-killer"

/var/log/kern.log (Ubuntu/Debian 계열에서 흔함)

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

로그에서 아래와 같은 패턴이 보이면 거의 확정입니다.

  • Out of memory: Kill process 12345 (java) score 987 or sacrifice child
  • Killed process 12345 (python) total-vm:... anon-rss:... file-rss:...

여기서 중요한 포인트:

  • Killed process 줄에 나온 PID/프로세스명이 “죽은 당사자”입니다.
  • anon-rss가 큰 경우가 많습니다(힙/익명 메모리).
  • 컨테이너라면 Memory cgroup out of memory 같은 문구가 함께 나올 수 있습니다.

2단계: “노드 OOM” vs “cgroup OOM(컨테이너 OOM)” 구분

Kubernetes에서 빠르게 확인

kubectl describe pod <pod-name> -n <ns>

Events에 다음이 보이면 컨테이너 OOM일 가능성이 큽니다.

  • Reason: OOMKilled
  • Last State: Terminated / Exit Code: 137

137SIGKILL(9)로 종료되었음을 의미합니다(꼭 OOM만은 아니지만 OOM에서 흔함).

Kubernetes에서 노드 자원이 빡빡해지면 스케줄링/안정성 문제도 같이 나타날 수 있습니다. 노드 수 부족/자원 부족으로 Pod가 못 뜨는 이슈는 다음 글이 맥락상 함께 도움이 됩니다: K8s Pod Pending(0/노드) - 스케줄 불가 원인·해결

호스트에서 cgroup OOM 흔적 찾기(개념)

  • systemd 서비스라면 해당 유닛이 cgroup에 속함
  • Docker/Kubernetes 컨테이너는 각자 메모리 제한(cgroup limit)을 가질 수 있음

cgroup OOM은 “노드 메모리는 남아 있는데도” 컨테이너가 죽을 수 있습니다. 이유는 단순합니다. 컨테이너에 할당한 상한을 넘었기 때문입니다.

3단계: 당시 메모리 상황을 수치로 복원하기

OOM은 “순간”에 발생하므로, 평소에 지표를 수집하지 않았다면 사후 분석이 어렵습니다. 그래도 기본 명령으로 현재 상태와 패턴을 확인할 수 있습니다.

메모리/스왑 현황

free -h
swapon --show
  • Swap이 0인데 메모리 압박이 크면 OOM이 더 빨리 올 수 있습니다.
  • Swap이 너무 크면 OOM은 늦게 오지만, 대신 심각한 성능 저하(스래싱)가 올 수 있습니다.

어떤 프로세스가 많이 쓰는지

ps -eo pid,comm,rss,%mem --sort=-rss | head -n 20

vmstat로 압박 징후 보기

vmstat 1 10
  • si/so(swap in/out)가 증가하면 메모리 부족 신호입니다.
  • r이 높고 wa가 높다면 I/O 대기/스왑 영향 가능성이 있습니다.

4단계: OOM Killer가 “왜 그 프로세스를 죽였는지” 이해하기

커널은 프로세스별로 OOM 점수(oom_score)를 계산합니다. 점수가 높을수록 “죽이기 좋은 후보”입니다.

현재 OOM 점수 확인

PID=<pid>
cat /proc/$PID/oom_score
cat /proc/$PID/oom_score_adj
  • oom_score: 커널이 계산한 현재 점수
  • oom_score_adj: 관리자가 조정 가능한 값(-1000 ~ 1000)

예를 들어, 중요한 프로세스(예: DB)가 OOM으로 죽는 것을 줄이려면 oom_score_adj를 낮추는 방법이 있습니다. 단, 이것은 OOM을 해결하는 게 아니라 “누가 대신 죽을지”를 바꾸는 것에 가깝습니다.

방지 전략 1: 애플리케이션 레벨(가장 근본적)

OOM의 1순위 원인은 보통 다음 중 하나입니다.

  • 메모리 릭(해제되지 않는 객체/버퍼)
  • 캐시/버퍼를 무제한으로 쌓음
  • 동시성 증가로 per-request 메모리가 선형 증가
  • 큰 파일/응답을 한 번에 메모리에 올림

빠른 방지 체크리스트

  • 캐시에는 상한(LRU/TTL/Max size)을 둔다
  • 요청 바디/파일 처리 시 스트리밍을 사용한다
  • 워커/스레드 수를 메모리 기반으로 제한한다
  • 언어 런타임(Java/Node/Python 등)의 힙/메모리 옵션을 명시한다

예: Node.js 힙 상한 설정

node --max-old-space-size=2048 server.js

예: Java 힙 상한 설정

java -Xms512m -Xmx2048m -jar app.jar

애플리케이션이 메모리를 무한정 쓰지 못하게 “상한”을 잡아두면, OOM이 나더라도 예측 가능한 실패 모드로 바뀝니다(예: 500 증가, 재시작 등).

방지 전략 2: 호스트(리눅스) 레벨에서의 완화

(1) 메모리 여유분 확보와 스왑 전략

  • 스왑 0은 깔끔하지만, 순간 스파이크에 취약합니다.
  • 작은 스왑(예: RAM의 25~50% 또는 워크로드에 맞게)은 “완충재”가 될 수 있습니다.

스왑을 쓰되 과도한 스왑 사용을 줄이려면 vm.swappiness를 조정합니다.

# 현재 값 확인
cat /proc/sys/vm/swappiness

# 임시 적용(재부팅 시 원복)
sudo sysctl -w vm.swappiness=10

# 영구 적용
echo "vm.swappiness=10" | sudo tee /etc/sysctl.d/99-swappiness.conf
sudo sysctl --system

(2) systemd 서비스에 메모리 상한을 걸어 “폭발 반경” 줄이기

특정 서비스가 메모리를 다 먹어 시스템 전체를 불안정하게 만들면, systemd cgroup 제한으로 피해를 국소화할 수 있습니다.

/etc/systemd/system/myapp.service.d/override.conf

[Service]
MemoryMax=2G
# OOM 상황에서 해당 서비스만 먼저 종료되도록 유도(환경에 따라 선택)
OOMScoreAdjust=500
sudo systemctl daemon-reload
sudo systemctl restart myapp
  • MemoryMax는 강력한 안전장치입니다.
  • 다만 서비스가 제한을 넘으면 종료될 수 있으므로, 애플리케이션이 재시작에 견고해야 합니다.

(3) EarlyOOM / systemd-oomd 같은 사용자 공간 OOM 대응

커널 OOM까지 가기 전에, 사용자 공간 데몬이 먼저 개입해 “덜 중요한 것”을 정리하도록 할 수 있습니다.

  • systemd-oomd: PSI(Pressure Stall Information) 기반으로 압박을 감지해 조치
  • earlyoom: 메모리/스왑이 임계치에 근접하면 프로세스 종료

운영 환경에서는 “커널 OOM 한 방”보다 “조기 개입”이 장애 전파를 줄이는 경우가 많습니다.

방지 전략 3: Kubernetes에서 OOMKilled 줄이기

Kubernetes에서는 requests/limits 설계가 OOM의 체감 빈도를 좌우합니다.

(1) requests/limits를 현실적으로 잡기

  • requests: 스케줄러가 노드에 배치할 때 기준
  • limits: 컨테이너가 사용할 수 있는 상한(초과 시 OOMKilled 가능)

예시:

resources:
  requests:
    cpu: "250m"
    memory: "512Mi"
  limits:
    cpu: "1"
    memory: "1Gi"
  • limit을 너무 낮게 잡으면 정상 트래픽에서도 OOMKilled가 납니다.
  • request를 너무 낮게 잡으면 노드에 과밀 배치되어 노드 OOM 위험이 커집니다.

(2) Exit code 137/OOMKilled를 “관측 가능”하게 만들기

  • kubectl get pod에서 재시작 횟수 증가 감시
  • 이벤트/로그 수집(예: kubelet 이벤트)
  • 노드 메모리/PSI 지표 수집(Prometheus node-exporter 등)

(3) 노드 레벨 OOM 방지: 여유 용량과 eviction 설정

노드가 꽉 차면 kubelet eviction이 동작하기도 하지만, 설정/워크로드 특성에 따라 OOM이 먼저 터질 수 있습니다.

  • 노드 메모리 사용률이 장기간 80~90% 이상이면 구조적으로 위험
  • Burstable/BestEffort Pod가 많으면 희생 대상이 예측 불가

클러스터에서 서비스 불안정(예: 503)이 함께 나타난다면, OOM으로 인한 백엔드 프로세스 재시작이 원인일 수 있습니다. 장애 징후를 빠르게 좁히는 관점에서는 다음 글도 참고가 됩니다: EKS에서 503 Service Unavailable 원인 10분 진단

실전 트러블슈팅 절차(요약)

  1. 커널 로그에서 OOM 확정: journalctl -k / dmesg -T
  2. 누가 죽었는지 식별: Killed process ... (name)
  3. 노드 OOM vs cgroup OOM 구분: Kubernetes 이벤트/ExitCode 137, cgroup 메시지
  4. 메모리 사용 패턴 확인: free -h, vmstat, ps --sort=-rss
  5. 방지책 적용
    • 앱 상한(힙/캐시/동시성)
    • systemd MemoryMax, OOMScoreAdjust
    • Kubernetes requests/limits 재설계
    • 조기 OOM 대응(systemd-oomd/earlyoom), 스왑/스와피니스 조정

마무리

OOM Killer는 “원인”이라기보다 메모리 부족이라는 더 큰 문제의 결과입니다. 따라서 진단의 핵심은 커널 로그로 OOM을 확정하고, 노드/컨테이너 경계를 구분한 뒤, 어떤 워크로드가 어떤 패턴으로 메모리를 소모했는지 좁혀가는 것입니다.

운영에서 가장 효과적인 방지는 보통 두 가지의 조합입니다.

  • 애플리케이션이 메모리를 무한정 먹지 못하도록 상한을 설계하고
  • 호스트/클러스터에서 한 서비스의 폭주가 전체 장애로 번지지 않도록 **격리(리소스 제한)와 관측(지표/이벤트)**을 갖추는 것

이 두 축을 갖추면 OOM은 “미스터리한 프로세스 증발”이 아니라, 예측 가능하고 통제 가능한 운영 이슈로 바뀝니다.