이전 두 글에서 V8 Heap(JS 객체)과 Buffer/Stream(off-heap 바이너리)을 다뤘다. 하지만 Node.js 프로세스가 OOMKilled 되는 원인은 이 두 영역만으로 설명되지 않는 경우가 많다. heapUsed도 정상이고, external도 크지 않은데 프로세스가 죽는다. 왜?
답은 더 아래 계층에 있다. Node.js는 JavaScript만으로 동작하지 않는다. libuv라는 C 라이브러리가 실제 I/O를 처리하고, 그 아래에는 OS의 메모리 관리 시스템이 있다. 이 글에서는 JS → V8 → Node → libuv → OS의 전체 흐름과, OS가 메모리를 어떻게 다루는지를 살펴보자.
Node.js의 아키텍처
Node.js를 "JavaScript 런타임"이라고 부르지만, 실제 구조는 여러 계층의 조합이다.
┌──────────────────────────────────────────────┐
│ JavaScript 코드 (우리가 쓰는 코드) │
├──────────────────────────────────────────────┤
│ Node.js API (fs, net, http, stream, ...) │
│ → JS + C++ 바인딩 │
├──────────────────────────────────────────────┤
│ V8 Engine │ Node.js C++ bindings │
│ (JS 실행, Heap, │ (node_buffer.cc, │
│ GC) │ node_file.cc, ...) │
├─────────────────────┼────────────────────────┤
│ │ libuv │
│ │ (이벤트 루프, 비동기 │
│ │ I/O, 스레드 풀) │
├──────────────────────────────────────────────┤
│ OS Kernel (syscall, epoll/kqueue, TCP/IP) │
└──────────────────────────────────────────────┘각 계층이 메모리를 각각 따로 사용한다.
| 계층 | 메모리 종류 | 추적 방법 |
|---|---|---|
| JS 코드 | V8 Heap (heapUsed) | process.memoryUsage(), --trace-gc |
| Buffer/ArrayBuffer | off-heap (external) | process.memoryUsage().external |
| libuv | C malloc (스레드 풀, 핸들 등) | OS 도구 (pmap, top) |
| OS | 가상 메모리, 페이지 캐시 | /proc/[pid]/status, ps |
process.memoryUsage()는 V8 Heap과 external만 보여준다. libuv가 내부적으로 할당한 메모리, 스레드 스택, OS 페이지 캐시 등은 보이지 않는다. 하지만 이것들도 전부 프로세스의 RSS에 포함되고, cgroup 메모리 제한에 걸린다. heapUsed만 보면 안 보이는 메모리가 프로세스를 죽일 수 있다.
libuv - Node.js의 I/O 엔진
libuv는 Node.js를 위해 만들어진 비동기 I/O 라이브러리다. 현재는 독립 프로젝트로, Julia, Neovim 등 다른 프로젝트에서도 사용한다. libuv 공식 문서에 설계 개요가 있다.
libuv가 하는 일:
- 이벤트 루프 관리
- 비동기 I/O: 파일, 네트워크, DNS, 파이프
- 스레드 풀: 파일 시스템 작업 등 블로킹 I/O를 비동기로 전환
- 타이머, 시그널, 프로세스 관리
이벤트 루프
Node.js의 "싱글 스레드"라 불리는 것의 실체가 libuv의 이벤트 루프다. Node.js 공식 가이드에 상세히 설명되어 있다.
libuv 이벤트 루프의 각 phase:
┌───────────────────────────┐
┌→ │ timers │ ← setTimeout, setInterval 콜백
│ └────────────┬──────────────┘
│ ┌────────────┴──────────────┐
│ │ pending callbacks │ ← 이전 루프에서 지연된 I/O 콜백
│ └────────────┬──────────────┘
│ ┌────────────┴──────────────┐
│ │ idle, prepare │ ← 내부용
│ └────────────┬──────────────┘
│ ┌────────────┴──────────────┐
│ │ poll │ ← I/O 이벤트 대기 (핵심!)
│ │ (epoll/kqueue/IOCP) │ 여기서 네트워크, 파일 I/O 완료를 감지
│ └────────────┬──────────────┘
│ ┌────────────┴──────────────┐
│ │ check │ ← setImmediate 콜백
│ └────────────┬──────────────┘
│ ┌────────────┴──────────────┐
│ │ close callbacks │ ← socket.on('close') 등
│ └────────────┬──────────────┘
└───────────────┘libuv의 메모리 사용
libuv는 자체적으로 C malloc을 사용해 메모리를 할당한다. 이 메모리는 V8의 heapUsed나 external에 잡히지 않는다.
libuv가 할당하는 메모리 (V8에 보이지 않음):
1. 핸들(Handle)
- uv_tcp_t, uv_udp_t, uv_fs_t, uv_timer_t 등
- TCP 커넥션 하나당 uv_tcp_t 구조체 하나 (수백 바이트)
- 10만 커넥션이면 ~수십MB
2. 스레드 풀 스택
- 기본 4개 스레드 (UV_THREADPOOL_SIZE로 조절 가능, 최대 1024)
- 스레드 하나당 스택 ~2MB (OS 기본값)
- 기본 설정: 4 × 2MB = ~8MB
3. I/O 버퍼
- 네트워크 데이터 수신 시 libuv가 할당하는 read buffer
- alloc_cb 콜백으로 크기를 제어할 수 있음// 스레드 풀 크기 확인 및 조절
console.log(process.env.UV_THREADPOOL_SIZE); // 기본: undefined (4개)
// 더 많은 병렬 파일 I/O가 필요하면:
// UV_THREADPOOL_SIZE=16 node server.js
// → 16개 스레드 × ~2MB 스택 = ~32MB 추가 메모리UV_THREADPOOL_SIZE를 무작정 키우면 스레드 스택 메모리가 늘어난다. 128로 설정하면 스택만 거의 256MB까지다. 또한 CPU 코어 수보다 스레드가 많으면 컨텍스트 스위칭 오버헤드도 증가한다. 대부분의 경우 기본값 4이면 충분하고, DNS/파일 I/O가 많은 경우에만 8~16 정도로 올리는 것이 적절하다. libuv 스레드 풀 문서를 참고하자.
libuv 소스에서 보는 I/O
네트워크 데이터가 왜 Buffer로 오는지, libuv 소스 src/unix/stream.c를 보면 이해된다.
// libuv src/unix/stream.c (개념적 흐름)
static void uv__read(uv_stream_t* stream) {
// 1. Node.js에 "버퍼를 할당해줘" 요청 (alloc_cb 호출)
// → Node.js는 여기서 Buffer를 할당해서 반환
buf = stream->alloc_cb(stream, suggested_size);
// 2. OS로부터 데이터를 읽어서 그 버퍼에 채움
nread = read(stream->io_watcher.fd, buf.base, buf.len);
// 3. Node.js에 "데이터 왔어" 콜백 (read_cb 호출)
// → Node.js는 이걸 'data' 이벤트로 JS에 전달
stream->read_cb(stream, nread, &buf);
}네트워크 데이터가 JS까지 올라오는 경로:
OS kernel (TCP 수신 버퍼)
↓ read() syscall
libuv (alloc_cb로 Buffer 할당)
↓ read_cb
Node.js C++ 바인딩
↓ 'data' event
JS 코드의 socket.on('data', callback)이 과정에서 생기는 메모리:
- OS 커널의 TCP 수신 버퍼 (
net.core.rmem_default) - libuv가 요청한 Buffer (Node.js가
alloc_cb에서 할당) - JS 콜백에서 사용하는 변수들
세 계층 모두 메모리를 쓰지만, process.memoryUsage()에는 두 번째(Buffer)만 external로 잡힌다.
RSS vs VSZ vs Heap
프로세스의 메모리를 이야기할 때 RSS, VSZ, Heap은 각각 다른 것을 가리킨다. 이 차이를 정확히 아는 것이 OOM 분석의 기본이다.
Virtual Memory (VSZ)
프로세스가 접근할 수 있는 가상 주소 공간의 총 크기다. 실제 물리 메모리와는 다르다.
64비트 프로세스의 가상 주소 공간:
0x000000000000 ┌────────────────────┐
│ 코드 세그먼트 │ ← 실행 파일, 공유 라이브러리
├────────────────────┤
│ 데이터 세그먼트 │ ← 전역 변수
├────────────────────┤
│ Heap (V8 + malloc) │ ← 동적 할당 (위로 성장)
│ ↓ │
│ (빈 공간) │
│ ↑ │
│ Stack │ ← 함수 호출 스택 (아래로 성장)
├────────────────────┤
│ mmap 영역 │ ← 메모리 맵핑 파일, 공유 메모리
├────────────────────┤
│ 커널 영역 │
0xFFFFFFFFFFFF └────────────────────┘
VSZ = 이 전체 가상 공간의 크기
→ 실제로 물리 메모리에 올라가지 않은 부분도 포함
→ 그래서 VSZ가 매우 커도 실제 메모리 사용은 적을 수 있다Resident Set Size (RSS)
실제로 물리 메모리(RAM)에 올라가 있는 페이지의 총 크기다. OS가 프로세스를 죽일지 말지 판단하는 기준이 된다.
가상 페이지 → 물리 페이지 매핑:
가상 주소 공간 물리 메모리 (RAM)
┌──────┐ ┌──────┐
│ 페이지1│ ────→ │ 프레임A│ ← 매핑됨 (RSS에 포함)
├──────┤ ├──────┤
│ 페이지2│ (미접근) │ 프레임B│ ← 다른 프로세스 사용 중
├──────┤ ├──────┤
│ 페이지3│ ────→ │ 프레임C│ ← 매핑됨 (RSS에 포함)
├──────┤ └──────┘
│ 페이지4│ → swap 디스크
├──────┤ ┌──────┐
│ 페이지5│ (미접근) │ swap │ ← 물리 메모리에 없음
└──────┘ └──────┘
RSS = 페이지1 + 페이지3 = 실제 RAM에 올라간 것만
VSZ = 페이지1~5 전체 = 가상 공간 전체정리
| VSZ | RSS | heapUsed | |
|---|---|---|---|
| 무엇 | 가상 주소 공간 전체 | 물리 메모리에 올라간 부분 | V8 Heap 중 사용 중인 부분 |
| 포함 범위 | 코드 + 데이터 + Heap + 스택 + mmap + 미할당 | 실제 RAM에 매핑된 페이지만 | JS 객체만 |
| OOM 기준 | 아님 | 이것 (cgroup 기준) | V8 내부 OOM만 |
| 확인 방법 | ps aux VSZ 열 | ps aux RSS 열 | process.memoryUsage().heapUsed |
# Node.js 프로세스의 VSZ와 RSS 확인
ps aux | grep node
# USER PID %CPU %MEM VSZ RSS TTY STAT TIME COMMAND
# yeji 1234 0.5 1.2 890000 45000 ... node server.js
# ↑ VSZ ↑ RSS (KB 단위)페이지 폴트와 메모리의 실제 동작
OS는 가상 메모리를 페이지(보통 4KB) 단위로 관리한다. 프로세스가 malloc으로 메모리를 요청해도, OS는 즉시 물리 메모리를 할당하지 않는다. 실제로 그 메모리에 접근(쓰기)할 때 비로소 물리 메모리를 할당한다. 이것이 demand paging이다.
Demand Paging (지연 할당):
1. malloc(100MB) 호출
→ OS: "가상 주소 100MB 예약해줄게" (VSZ += 100MB)
→ 물리 메모리는 아직 0 할당 (RSS 변동 없음)
2. 첫 번째 페이지에 쓰기: buffer[0] = 1
→ Page Fault 발생! (Minor)
→ OS: "아, 진짜 쓰네. 물리 페이지 하나 할당할게" (RSS += 4KB)
3. 두 번째 페이지에 쓰기: buffer[4096] = 1
→ 또 Page Fault
→ OS: 물리 페이지 하나 더 할당 (RSS += 4KB)
→ 100MB를 malloc해도, 실제로 1MB만 접근하면 RSS는 ~1MB만 증가한다이것이 Buffer.alloc()과 Buffer.allocUnsafe()의 메모리 동작이 다른 또 다른 이유다.
// Buffer.alloc(100MB) → 0으로 초기화 → 모든 페이지에 쓰기 발생
// → 즉시 RSS가 ~100MB 증가
// Buffer.allocUnsafe(100MB) → 초기화 안 함 → 아직 접근 안 한 페이지는 물리 할당 안 됨
// → RSS는 실제로 접근한 부분만큼만 증가
// (단, 작은 크기에서는 pool을 사용하므로 이 차이가 드러나지 않을 수 있음)Minor vs Major Page Fault
| Minor Page Fault | Major Page Fault | |
|---|---|---|
| 원인 | 가상 페이지에 처음 접근 (물리 매핑 없음) | 필요한 페이지가 디스크(swap)에 있음 |
| 비용 | 가볍다 (커널 내에서 페이지 테이블 업데이트) | 무겁다 (디스크 I/O 발생) |
| 성능 영향 | 거의 없음 | 심각 (ms 단위 지연) |
# 프로세스의 page fault 통계 확인 (Linux)
cat /proc/[PID]/stat | awk '{print "minor:", $10, "major:", $12}'
# 또는 time 명령으로
/usr/bin/time -v node server.js 2>&1 | grep "page faults"
# Minor (reclaiming) page faults: 15234
# Major (requiring I/O) page faults: 0 ← 0이 정상Major page fault는 swap이 발생하고 있다는 신호다. Node.js에서 swap이 발생하면 GC가 swap된 페이지를 스캔하면서 디스크 I/O가 폭발하고, 응답 시간이 수백ms~수초로 치솟는다. 프로덕션 Node.js 서버에서 swap은 사실상 장애다. K8s 환경에서는 swap을 비활성화하는 것이 권장되며, 메모리가 부족하면 swap 대신 OOMKill이 발생하도록 하는 것이 오히려 낫다.
cgroup과 OOM Killer
컨테이너(Docker, K8s) 환경에서 Node.js를 실행하면, OS 수준의 메모리 제한인 cgroup(control group)이 적용된다. OOMKilled의 진짜 원인을 이해하려면 cgroup을 알아야 한다.
cgroup 메모리 제한
# K8s Pod 설정 예시
resources:
limits:
memory: "512Mi" # ← 이게 cgroup 메모리 제한이 된다
requests:
memory: "256Mi"이 설정이 되면 커널이 이 컨테이너의 cgroup에 512MB 제한을 건다. 컨테이너 안의 프로세스가 합산으로 512MB를 넘으면 OOM Killer가 동작한다.
cgroup 메모리 제한의 범위:
cgroup memory limit (512MB)
┌─────────────────────────────────────────────────┐
│ │
│ Node.js 프로세스 RSS │
│ ├── V8 Heap (heapUsed + 빈 공간) │
│ ├── Buffer / ArrayBuffer (external) │
│ ├── libuv (핸들, 스레드 스택) │
│ ├── JIT 코드 (Code Space) │
│ ├── 공유 라이브러리 (.so 파일 매핑) │
│ └── 기타 (스택, 환경 변수 등) │
│ │
│ + 커널 메모리 (page cache, slab 등) ← 이것도! │
│ │
└─────────────────────────────────────────────────┘
↑
이 전체 합이 512MB를 넘으면 → OOM Killer 발동가장 흔한 원인 세 가지:
1. Page Cache 포함: Linux는 파일 I/O 시 page cache를 적극적으로 사용한다. 파일을 많이 읽으면 page cache가 cgroup 메모리에 포함되어 제한에 걸릴 수 있다. heapUsed가 200MB인데 512MB 제한에서 죽는다면, page cache가 나머지를 차지하고 있을 수 있다.
2. V8 Heap이 제한보다 큼: --max-old-space-size를 cgroup 제한보다 크게 잡으면, V8이 Heap을 확장하다가 cgroup 제한에 걸려서 OOMKilled 된다. V8의 에러 메시지 대신 커널의 SIGKILL(exit code 137)이 날아온다.
3. 메모리 단편화: RSS에 잡히는 물리 메모리와 실제 "사용 중인" 메모리는 다를 수 있다. 단편화된 메모리는 free list에 있어도 물리 페이지를 차지한다.
OOM Killer의 동작
Linux OOM Killer는 메모리 제한에 도달하면 oom_score가 가장 높은 프로세스를 SIGKILL로 강제 종료한다. 컨테이너 환경에서는 cgroup 내에서만 판단한다.
# 프로세스의 oom_score 확인 (Linux)
cat /proc/[PID]/oom_score # 0~1000, 높을수록 먼저 죽음
cat /proc/[PID]/oom_score_adj # -1000~1000, 관리자가 조정 가능
# cgroup 메모리 사용량 확인 (cgroup v2)
cat /sys/fs/cgroup/memory.current # 현재 사용량 (bytes)
cat /sys/fs/cgroup/memory.max # 제한 (bytes)
# K8s에서 확인
kubectl top pod [pod-name]
kubectl describe pod [pod-name] | grep -A5 "Last State"
# OOMKilled이면 exit code 137 (128 + 9 = SIGKILL)--max-old-space-size와 cgroup의 관계
V8의 --max-old-space-size와 cgroup 메모리 제한은 독립적으로 동작한다. 둘 다 맞춰야 한다.
잘못된 설정:
cgroup limit: 512MB
--max-old-space-size: 1024MB ← V8은 1GB까지 Heap을 키울 수 있다고 생각
→ V8이 Heap을 600MB로 키우려는 순간
→ RSS가 cgroup 512MB를 넘김
→ OS가 SIGKILL (exit 137)
→ V8의 "JavaScript heap out of memory" 메시지도 안 뜸!
올바른 설정:
cgroup limit: 512MB
--max-old-space-size: 384MB ← 나머지 ~128MB는 Buffer, libuv, 스택 등에 여유// 프로그래밍 방식으로 cgroup 제한을 읽어서 자동 설정하는 패턴
const os = require('os');
const fs = require('fs');
function getMemoryLimit() {
try {
// cgroup v2
const limit = fs.readFileSync('/sys/fs/cgroup/memory.max', 'utf8').trim();
if (limit !== 'max') return parseInt(limit);
} catch {}
try {
// cgroup v1
const limit = fs.readFileSync(
'/sys/fs/cgroup/memory/memory.limit_in_bytes', 'utf8'
).trim();
const parsed = parseInt(limit);
// 매우 큰 값이면 제한 없음
if (parsed < os.totalmem()) return parsed;
} catch {}
return os.totalmem(); // cgroup 없으면 시스템 전체 메모리
}
const limitMB = Math.floor(getMemoryLimit() / 1024 / 1024);
console.log(`Memory limit: ${limitMB}MB`);
// → 이 값의 약 75%를 --max-old-space-size로 설정하는 것이 안전Node.js(V8)는 cgroup 메모리 제한을 감지하여 Heap 상한을 조정하는 기능이 있다 (관련 이슈 #27508). 하지만 cgroup v2 환경에서 감지가 실패하는 버그가 보고된 바 있어, 모든 환경에서 완벽하게 동작하지는 않는다. 프로덕션에서는 --max-old-space-size를 명시적으로 설정하는 것이 권장된다.
메모리 모니터링 실전
각 계층의 메모리를 종합적으로 모니터링하는 방법을 정리한다.
Node.js 레벨
// 종합 메모리 모니터링
const v8 = require('v8');
const os = require('os');
function memoryReport() {
const proc = process.memoryUsage();
const heap = v8.getHeapStatistics();
return {
// OS 레벨
rss_mb: (proc.rss / 1024 / 1024).toFixed(1),
system_total_mb: (os.totalmem() / 1024 / 1024).toFixed(0),
system_free_mb: (os.freemem() / 1024 / 1024).toFixed(0),
// V8 Heap
heap_used_mb: (proc.heapUsed / 1024 / 1024).toFixed(1),
heap_total_mb: (proc.heapTotal / 1024 / 1024).toFixed(1),
heap_limit_mb: (heap.heap_size_limit / 1024 / 1024).toFixed(0),
// Off-heap
external_mb: (proc.external / 1024 / 1024).toFixed(1),
array_buffers_mb: (proc.arrayBuffers / 1024 / 1024).toFixed(1),
// 계산값: "설명 안 되는" 메모리
// RSS - (heapTotal + external) = libuv, 스택, 코드, page cache 등
unaccounted_mb: (
(proc.rss - proc.heapTotal - proc.external) / 1024 / 1024
).toFixed(1),
};
}
// 주기적 로깅
setInterval(() => {
console.log(JSON.stringify(memoryReport()));
}, 30_000);OS 레벨 (Linux)
# 프로세스 상세 메모리 맵
cat /proc/[PID]/status | grep -E "VmRSS|VmSize|VmSwap|RssAnon|RssFile"
# VmSize: 890000 kB ← VSZ
# VmRSS: 45000 kB ← RSS
# RssAnon: 30000 kB ← 익명 메모리 (Heap, 스택 등)
# RssFile: 15000 kB ← 파일 매핑 (공유 라이브러리, page cache)
# VmSwap: 0 kB ← swap된 메모리 (0이 정상!)
# 메모리 영역별 상세 맵
pmap -x [PID] | tail -1
# total kB 890000 45000 30000
# VSZ RSS Dirty
# cgroup 메모리 사용량 (컨테이너 환경)
cat /sys/fs/cgroup/memory.current
cat /sys/fs/cgroup/memory.max
cat /sys/fs/cgroup/memory.stat | head -5RssAnon(Anonymous RSS)은 Heap, 스택, mmap(MAP_ANONYMOUS) 등 파일에 기반하지 않는 물리 메모리다. V8 Heap, Buffer, libuv 할당이 여기에 포함된다. RssFile은 공유 라이브러리(.so), 실행 파일, 파일 mmap 등 파일에 기반한 물리 메모리다. 메모리 릭 분석 시 RssAnon이 계속 올라가는지를 먼저 확인하는 것이 효과적이다.
직접 확인해보기
각 계층의 메모리 사용량을 관찰하는 실험을 해보자.
// memory-layers.js
// 실행: node --expose-gc memory-layers.js
function printAll(label) {
global.gc && global.gc();
const mem = process.memoryUsage();
const unaccounted = mem.rss - mem.heapTotal - mem.external;
console.log(`\n[${label}]`);
console.log(` RSS: ${(mem.rss / 1024 / 1024).toFixed(1)}MB`);
console.log(` Heap Used: ${(mem.heapUsed / 1024 / 1024).toFixed(1)}MB`);
console.log(` Heap Total: ${(mem.heapTotal / 1024 / 1024).toFixed(1)}MB`);
console.log(` External: ${(mem.external / 1024 / 1024).toFixed(1)}MB`);
console.log(` Unaccounted: ${(unaccounted / 1024 / 1024).toFixed(1)}MB`);
// ↑ libuv, 스택, 코드, page cache 등
}
printAll('초기 상태');
// 1. JS 객체 → heapUsed 증가
const jsObjects = [];
for (let i = 0; i < 100_000; i++) {
jsObjects.push({ i, data: 'x'.repeat(100) });
}
printAll('JS 객체 10만 개');
// 2. Buffer → external 증가, heapUsed 거의 불변
const buffers = [];
for (let i = 0; i < 50; i++) {
buffers.push(Buffer.alloc(1024 * 1024)); // 50MB
}
printAll('Buffer 50MB');
// 3. TCP 서버 → libuv 핸들 할당 (unaccounted 증가)
const net = require('net');
const server = net.createServer((socket) => {
// 커넥션마다 uv_tcp_t 핸들이 libuv에서 할당됨
socket.on('data', () => {});
});
server.listen(0, () => {
// 자기 자신에게 커넥션 100개 생성
const connections = [];
for (let i = 0; i < 100; i++) {
const client = net.connect(server.address().port);
connections.push(client);
}
setTimeout(() => {
printAll('TCP 커넥션 100개');
// → unaccounted가 약간 증가한 것을 관찰할 수 있다
// (각 커넥션의 uv_tcp_t + 소켓 버퍼)
// 정리
connections.forEach(c => c.destroy());
server.close();
}, 1000);
});정리
JS → V8 → Node.js → libuv → OS
각 계층에서 메모리가 할당되고, 각각 다른 방식으로 추적/관리된다:
JS 객체 → V8 Heap → heapUsed → V8 GC가 관리
Buffer → off-heap → external → GC + C++ destructor
libuv → C malloc → (보이지 않음) → libuv 내부 관리
OS → 가상 메모리 → RSS/VSZ → 커널이 관리
cgroup → 물리 메모리 → memory.current → OOM Killer| 개념 | 핵심 | 기억할 것 |
|---|---|---|
| libuv | Node.js의 I/O 엔진, C로 작성 | 자체 메모리는 heapUsed에 안 잡힘 |
| VSZ | 가상 주소 공간 전체 | 커도 괜찮다, 실제 사용량이 아님 |
| RSS | 물리 메모리 사용량 | OOM 판단의 실제 기준 |
| Page Fault | 가상 → 물리 매핑 시 발생 | Major page fault(swap)은 장애 신호 |
| cgroup | 컨테이너 메모리 제한 | page cache도 포함됨 |
| OOM Killer | RSS가 cgroup 제한 넘으면 SIGKILL | exit code 137 |
| --max-old-space-size | cgroup의 ~75%로 설정 | V8 제한 > cgroup이면 SIGKILL |
다음 글에서는 이 모든 지식을 활용해서 실제로 메모리 릭을 잡는 방법을 다룬다. Heap Snapshot, Chrome DevTools, Clinic.js 등 디버깅 도구와 실전 포스트모템까지!
레퍼런스
공식 문서
- libuv Design Overview - libuv 아키텍처
- libuv Thread Pool - UV_THREADPOOL_SIZE, 비동기 I/O
- Node.js Event Loop - 이벤트 루프 phase별 동작
- Node.js CLI Options -
--max-old-space-size등 V8 플래그
소스코드
- libuv src/unix/stream.c - 네트워크 I/O 구현, read/write 흐름
- libuv src/unix/tcp.c - TCP 핸들 구현
- libuv src/threadpool.c - 스레드 풀 구현
OS / 시스템
- Linux cgroup v2 - Memory Controller - cgroup 메모리 제한 공식 문서
- Brendan Gregg - Linux Performance - RSS, VSZ, page fault 등 시스템 성능 분석
- proc(5) man page -
/proc/[pid]/status,/proc/[pid]/stat필드 설명