반응형

AWS EC2, Google Cloud, 자체 Linux 서버 또는 Kubernetes(EKS·GKE) 환경에서 Docker 컨테이너가 아무런 애플리케이션 에러 로그도 남기지 않고 갑자기 종료되며 Exited (137) 상태가 되는 현상은 백엔드·AI 인프라 운영에서 가장 빈번하게 겪는 장애입니다. 특히 Node.js, Spring Boot(JVM), Python(FastAPI·PyTorch·로컬 LLM) 컨테이너를 구동할 때 컨테이너 내부 프로세스가 cgroups 메모리 한도를 초과해 Linux 커널 OOM Killer(Out of Memory Killer)에게 강제 종료(SIGKILL)당하는 경우가 전체 원인의 90% 이상을 차지합니다.

이번 글에서는 1) Docker 종료 코드(137, 143, 139)별 정확한 원인 구분과 1분 진단 명령어, 2) docker-compose.yml 메모리 제한 및 Node.js·Java·Python 런타임별 OOM 원천 차단 설정, 그리고 3) 무작정 클라우드 서버 스펙을 올리기 전 확인해야 할 서버 Scale-up vs 클라우드 APM 모니터링(Datadog, Prometheus·Grafana) 비용 최적화 기준을 정리합니다.


1. Docker 컨테이너 종료 코드(Exit Code) 비교 및 OOM Killed 1분 진단법

리눅스 시스템에서 종료 코드 128 + N은 프로세스가 시그널 N에 의해 종료되었음을 뜻합니다. 즉 Exit Code 137은 128 + 9 (SIGKILL)로, 커널이 메모리를 확보하기 위해 프로세스를 즉시 강제 사살했거나 외부에서 docker kill을 호출한 결과입니다.

종료 코드 시그널 및 의미 핵심 발생 원인 우선 조치 사항
Exit Code 137 SIGKILL (9)
즉시 강제 종료
Linux OOM Killer 발동, 컨테이너 mem_limit 초과, 또는 docker stop 타임아웃(10초) 초과 docker inspect의 OOMKilled 플래그 및 dmesg 확인
Exit Code 143 SIGTERM (15)
정상 종료 요청
배포·재시작 시 docker stop 수신 후 애플리케이션이 정상적으로 Graceful Shutdown 수행 정상 종료 동작 (의도치 않은 경우 오토스케일러·헬스체크 점검)
Exit Code 139 SIGSEGV (11)
세그멘테이션 오류
잘못된 메모리 참조, 네이티브 C/C++ 라이브러리 충돌, Alpine(musl) vs glibc 호환성 문제 베이스 이미지를 alpine에서 debian-slim으로 변경 테스트
Exit Code 1 / 255 Application Error 애플리케이션 내부 예외(Uncaught Exception), 환경변수·설정 파일 누락, DB 연결 실패 docker logs --tail 200으로 스택 트레이스 확인

컨테이너가 137 코드로 죽었을 때 정말 메모리 부족(OOM) 때문인지 아니면 배포 스크립트의 강제 종료 때문인지는 아래 명령어 2줄로 즉시 판별할 수 있습니다.

# 1. Docker 컨테이너 상태 메타데이터에서 OOMKilled 여부가 true인지 확인
docker inspect <컨테이너_이름_또는_ID> --format '{{json .State}}' | grep -E 'OOMKilled|ExitCode'
# 출력 예시: {"Status":"exited", ..., "ExitCode":137, "OOMKilled":true, ...}

# 2. 호스트 Linux 커널 링 버퍼(dmesg)에서 언제, 어떤 프로세스가 몇 MB를 쓰다 강제 종료되었는지 확인
sudo dmesg -T | grep -i -E 'out of memory|killed process|oom-kill' | tail -n 20

만약 로컬·클라우드 환경에서 LLM 모델이나 AI 컨테이너를 띄우다 메모리 한도로 종료되는 경우라면, 하드웨어/VRAM 요구량 계산법을 먼저 점검해 보세요.

👉 관련 필독 가이드: 애플 실리콘 Mac(M3·M4 Max/Pro) 로컬 LLM 구동 메모리(VRAM) 계산법 및 Ollama·클라우드 GPU 비용 최적화 가이드

반응형

2. Docker Compose 메모리 제한 및 런타임별(Node.js·Java·Python) OOM 해결 설정

① 왜 컨테이너 제한을 걸었는데도 OOM Killed가 발생할까?

핵심 원인: Node.js(V8 엔진)나 구버전 JVM, Python 워커는 기본적으로 컨테이너에 할당된 cgroups 메모리 제한이 아니라 호스트 물리 서버 전체 RAM 크기를 기준으로 가비지 컬렉션(GC) 임계치를 잡는 경우가 있습니다. 예를 들어 16GB 호스트 서버에서 컨테이너에 mem_limit: 1g를 걸어두면, Node.js는 아직 메모리가 넉넉하다고 판단해 GC를 미루다가 1GB를 넘는 순간 커널에게 즉시 SIGKILL(137)을 맞고 종료됩니다.

② 실전 docker-compose.yml 및 런타임 힙(Heap) 한도 동기화 설정

컨테이너 하드 리밋(mem_limit)의 약 75%~80% 수준으로 언어 런타임의 최대 힙 메모리를 명시적으로 제한하면, 커널 OOM Killer가 개입하기 전에 언어 자체 GC가 작동하여 컨테이너 다운타임을 완벽히 예방할 수 있습니다.

version: "3.8"
services:
  api-server:
    image: my-org/production-api:latest
    restart: unless-stopped
    # 1. 컨테이너 레벨 메모리 하드/소프트 리밋 및 스왑 설정
    mem_limit: 2048m
    mem_reservation: 1024m
    memswap_limit: 2560m
    environment:
      # [Node.js] 컨테이너 2GB 기준 75%인 1536MB로 V8 Heap 제한
      - NODE_OPTIONS=--max-old-space-size=1536
      # [Java / Spring Boot] cgroups 인식 활성화 및 힙 비율 75% 제한
      - JAVA_TOOL_OPTIONS=-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -XX:+ExitOnOutOfMemoryError
    logging:
      driver: "json-file"
      options:
        max-size: "50m"
        max-file: "3"

③ Python (FastAPI / Gunicorn / Celery) 메모리 누수 방어 설정

Python 웹 서버나 AI 추론 워커는 C 확장 모듈(NumPy, Pandas, PyTorch)의 메모리 단편화로 인해 시간이 지날수록 RSS 메모리가 우상향하는 특성이 있습니다. 워커당 일정 요청 수를 처리하면 자동으로 프로세스를 재활용하도록 --max-requests 옵션을 반드시 추가하세요.

# Gunicorn + Uvicorn 워커 메모리 누수 자동 초기화 실행 예시
gunicorn main:app \
  --workers 4 \
  --worker-class uvicorn.workers.UvicornWorker \
  --max-requests 2000 \
  --max-requests-jitter 200 \
  --timeout 120
반응형

3. 클라우드 서버 증설(Scale-up) vs APM 모니터링 도구 비용·효율 비교

Exit Code 137이 발생했을 때 원인 분석 없이 AWS EC2나 클라우드 인스턴스 등급(예: t3.large → m6i.xlarge)부터 두 배로 올리면 매달 수십만 원의 고정 인프라 비용이 낭비됩니다. 아래 기준표를 통해 현재 팀 상황에 맞는 모니터링·인프라 전략을 선택하세요.

대응 방식 / 솔루션 예상 월 비용 구조 장점 및 핵심 기능 추천 도입 환경
리눅스 스왑(Swap) + 런타임 GC 튜닝 $0 (무료)
추가 인프라 과금 없음
일시적인 트래픽 스파이크 시 OOM 강제 종료 방지, 즉각적인 안정성 확보 단일 EC2/VPS 서버, 초기 스타트업 및 사이드 프로젝트
오픈소스 모니터링
(Prometheus + cAdvisor + Grafana)
소형 수집 서버 비용
(월 $10~$30 수준)
컨테이너별 실시간 RSS/Cache 메모리 그래프 및 Slack/Discord 임계치 알림 무료 구축 호스트당 과금 부담 없이 다수의 Docker/K8s 노드를 운영할 때
상용 클라우드 APM
(Datadog / New Relic / Dynatrace)
호스트당 월 $15~$31+
(로그·APM 추가 과금)
OOM 직전 힙 덤프·트레이스 자동 수집, 코드 레벨 메모리 누수 지점 즉시 특정 장애 1회당 매출 손실이 큰 프로덕션 커머스·핀테크·B2B SaaS
클라우드 인스턴스 Scale-up / Fargate 기존 서버 비용의
1.8배 ~ 3배 증가
엔지니어링 튜닝 시간 절약, 실제 데이터 처리량 증가에 따른 물리적 한계 해소 GC 튜닝 후에도 상시 메모리 점유율이 85%를 넘는 고부하 서버

4. 마무리 점검 리스트 및 함께 읽으면 좋은 인프라·AI 가이드

반응형
Posted by Real_G