현상#
운영 환경의 Node.js 컨테이너가 주기적으로 종료되고 Docker 상태에서 State.OOMKilled=true 또는 exit code 137이 관찰된다. 애플리케이션 로그에는 JavaScript 예외나 V8 stack trace가 남지 않을 수 있다.
정정된 원인 모델#
이 장애를 docker-compose.yml에 mem_limit이 없어서 V8이 항상 1.5GB 기본 힙으로 잘못 추정했다`로 단정하면 안 된다.
더 안전한 설명은 다음과 같다.
- 컨테이너에 명시적 메모리 제한이 없으면, 해당 컨테이너는 Docker/호스트/상위 orchestrator가 허용하는 범위까지 메모리를 사용할 수 있다. 이 경우 한 프로세스의 RSS 증가가 호스트 또는 상위 cgroup의 메모리 압박으로 이어져 OOM killer의 대상이 될 수 있다.
- Node.js/V8의 기본 heap sizing과 cgroup 인식은 Node/V8 버전, cgroup v1/v2, Docker Engine, 베이스 이미지와 실행 환경에 따라 달라진다. 따라서
V8 기본 힙은 항상 1.5GB또는V8은 컨테이너 제한을 전혀 보지 못한다같은 고정값·보편 명제는 피한다. --max-old-space-size는 V8 old generation heap 한도일 뿐, 전체 RSS 한도가 아니다. Buffer, native addon, libuv, mmap, 코드 영역, 스레드 stack, allocator overhead 등은 별도로 메모리를 사용한다.- exit code
137은 OOM kill에서 흔하지만, 모든 137 종료가 동일한 원인은 아니다.docker inspect, Docker event, kernel log, cgroup 메트릭, 애플리케이션 메모리 지표를 함께 확인해야 한다.
확인 절차#
docker inspect <container>에서 다음을 확인한다. -State.OOMKilled-State.ExitCode-HostConfig.Memory-HostConfig.MemorySwap-HostConfig.NanoCpus,CpuQuota,CpuPeriod,PidsLimit등 관련 제한- 컨테이너 내부/호스트에서 cgroup 메모리 값을 확인한다.
- cgroup v2라면
memory.max,memory.current,memory.events등 - cgroup v1이라면 memory limit/usage/failcnt 계열 파일 - Node.js 런타임에서 실제 관측값을 남긴다.
-
process.memoryUsage().rss-heapUsed,heapTotal,external,arrayBuffers-v8.getHeapStatistics() - 커널 로그 또는 Docker events에서 OOM kill 시점과 프로세스명을 대조한다.
수정 가이드#
1. Compose에서 메모리 한도를 명시한다#
단일 호스트 docker compose up 운영이라면 서비스 레벨 제한을 우선 확인한다.
services:
app:
image: example/app:latest
mem_limit: 1g
memswap_limit: 1g
environment:
NODE_OPTIONS: --max-old-space-size=768
deploy.resources.limits.memory는 Compose/Swarm/Compose CLI 버전에 따라 적용 범위와 기대 동작을 확인해야 한다. 운영에서는 docker inspect로 실제 HostConfig.Memory에 반영됐는지 검증한다.
2. Node heap 한도는 RSS 예산에서 역산한다#
NODE_OPTIONS=--max-old-space-size=...는 컨테이너 전체 메모리보다 충분히 낮게 둔다. 단, “항상 10%만 남기면 된다”가 아니라 워크로드별 RSS 구성을 보고 정한다.
예시:
- 컨테이너 메모리 한도:
1g - V8 old-space-size:
700~800MB부터 시작 - Buffer/native/external memory가 큰 서비스라면 더 낮게 설정
- 부하 테스트에서 RSS peak와 OOM event를 확인한 뒤 조정
3. swap 설정은 Docker/커널/cgroup 조건을 검증한다#
Docker의 memswap_limit 또는 --memory-swap은 memory+swap 총량을 제어한다. 일반적으로 memory와 memory-swap을 같은 값으로 두면 swap 사용을 막으려는 의도에 맞지만, 실제 의미는 Docker Engine, cgroup 버전, 호스트 swap 활성화 여부에 따라 검증해야 한다.
검증 없이 memswap_limit: mem_limit이 모든 환경에서 반드시 swap 우회를 금지한다고 단정하지 않는다.
4. healthcheck는 생존성 신호이고 메모리 관측은 별도 지표로 둔다#
healthcheck로 RSS 임계값 초과 시 unhealthy를 만들 수는 있지만, 메모리 누수 탐지의 주 수단은 메트릭과 알림이어야 한다.
권장 관측 항목:
- container RSS / working set
- cgroup memory current/max/events
- Node
process.memoryUsage() - GC pause/heap statistics
- OOMKilled count와 restart count
핵심 교훈#
- 컨테이너 메모리 한도는 Compose 설정만 믿지 말고
docker inspect와 cgroup 파일로 실제 반영 여부를 확인한다. - Node/V8의 기본 heap 동작은 버전 의존적이므로 고정된 1.5GB 규칙으로 설명하지 않는다.
--max-old-space-size는 전체 RSS 제한이 아니라 V8 old heap 제한이다.memswap_limit은 의도와 실제 동작을 부하 테스트와 cgroup 관측으로 검증한다.- OOM kill 분석은 exit code 137 하나가 아니라 Docker 상태, kernel log, cgroup event, 런타임 메트릭을 함께 봐야 한다.
Sagwan Revalidation 2026-09-27T02:17:46Z#
- verdict:
refresh - note: 기술 내용은 대체로 유효하지만 원인 설명의 백틱 오류로 문서 정리가 필요하다.
Sagwan Revalidation 2026-10-01T07:52:08Z#
- verdict:
refresh - note: 기술 내용은 대체로 유효하지만 원인 설명의 백틱 오류가 남아 있어 수정이 필요하다.
Sagwan Revalidation 2026-10-04T03:05:22Z#
- verdict:
refresh - note: 기술 내용은 대체로 유효하지만 원인 설명의 백틱 오류와 확인 절차의 목록 서식을 수정해야 한다.
Sagwan Revalidation 2026-10-07T04:29:15Z#
- verdict:
refresh - note: 기술 내용은 대체로 유효하지만 원인 설명의 백틱 오류와 확인 절차의 목록 서식이 남아 있다.
Sagwan Revalidation 2026-10-10T01:30:52Z#
- verdict:
refresh - note: 기술 내용은 대체로 유효하지만 백틱 오류와 확인 절차의 목록 서식이 남아 있다.
Sagwan Revalidation 2026-10-12T19:57:30Z#
- verdict:
refresh - note: 기술 내용은 대체로 유효하지만 백틱 오류와 확인 절차의 목록 서식이 남아 있다.
Sagwan Revalidation 2026-10-15T15:34:10Z#
- verdict:
refresh - note: 기술 내용은 대체로 유효하지만 원인 설명의 백틱 오류와 확인 절차의 목록 서식이 남아 있다.
Sagwan Revalidation 2026-10-18T12:36:19Z#
- verdict:
refresh - note: 기술 내용은 대체로 유효하지만 백틱 오류와 확인 절차의 목록 서식이 여전히 남아 있다.