Back

[TIL] 메모리 릭 디버깅 - 도구부터 실전 포스트모템까지

2026. 03. 20.Yeji Kim
DebugCS

이전 세 글에서 V8 Heap, Buffer/Stream, libuv/OS까지 Node.js 메모리의 전체 계층을 다뤘다. 이제 실제 케이스로 들어가보자. 메모리가 새고 있다는 건 어떻게 알 수 있을까? 원인은 어떻게 찾을까? 이 글에서는 도구의 사용법과 실전 분석 워크플로우를 다룬다.


메모리 릭인지 어떻게 판단하는가

메모리 사용량이 높다고 반드시 릭은 아니다. 캐시를 의도적으로 쌓고 있을 수도 있고, 트래픽이 많아서 동시 처리 중인 요청이 많은 것일 수도 있다. 이란 더 이상 필요 없는 메모리가 해제되지 않고 계속 쌓이는 것이다.

text
정상적인 메모리 패턴:
 
메모리
 │    ╱╲    ╱╲    ╱╲    ╱╲
 │   ╱  ╲  ╱  ╲  ╱  ╲  ╱  ╲     ← GC 후 다시 내려옴
 │  ╱    ╲╱    ╲╱    ╲╱    ╲
 └──────────────────────────── 시간
 
메모리 릭 패턴:
 
메모리
 │              ╱╲    ╱╲
 │         ╱╲  ╱  ╲  ╱  ╲
 │    ╱╲  ╱  ╲╱    ╲╱    ╲     ← GC 후에도 바닥이 점점 올라감
 │   ╱  ╲╱
 └──────────────────────────── 시간

판단 기준:

js
// 간단한 릭 감지: GC 후 heapUsed의 바닥값이 계속 올라가는지 확인
const v8 = require('v8');
 
let baseline = null;
 
setInterval(() => {
  global.gc(); // --expose-gc 필요
  const used = process.memoryUsage().heapUsed;
 
  if (baseline === null) {
    baseline = used;
  } else {
    const growth = ((used - baseline) / 1024 / 1024).toFixed(1);
    console.log(`Heap baseline drift: +${growth}MB`);
    // 이 값이 시간이 지남에 따라 꾸준히 증가하면 → 릭 가능성
  }
}, 60_000); // 1분마다 체크

--trace-gc 로그에서도 확인할 수 있다. Mark-Compact 후의 Heap 크기(-> 뒤의 숫자)가 계속 커지면 릭이다.

bash
# --trace-gc 로그에서 릭 패턴 관찰
node --trace-gc server.js
 
# 정상: Mark-Compact 후 크기가 일정
# [1234] 10000 ms: Mark-Compact 45.3 -> 32.1 MB, 12.5 ms
# [1234] 20000 ms: Mark-Compact 47.2 -> 32.3 MB, 13.1 ms
# [1234] 30000 ms: Mark-Compact 46.8 -> 32.0 MB, 12.8 ms
#                                        ^^^^ 일정
 
# 릭: Mark-Compact 후 크기가 점점 증가
# [1234] 10000 ms: Mark-Compact 45.3 -> 32.1 MB, 12.5 ms
# [1234] 20000 ms: Mark-Compact 50.2 -> 38.5 MB, 14.2 ms
# [1234] 30000 ms: Mark-Compact 56.1 -> 45.3 MB, 16.0 ms
#                                        ^^^^ 계속 증가

메모리 릭의 4가지 패턴

Node.js에서 메모리 릭은 크게 네 가지 패턴으로 나뉜다.

1. 전역 변수 / 모듈 스코프 축적

js
// ❌ 모듈 스코프에 데이터가 무한히 쌓이는 패턴
const requestLog = []; // 모듈 스코프 = 프로세스 수명과 동일
 
app.use((req, res, next) => {
  requestLog.push({
    url: req.url,
    time: Date.now(),
    headers: req.headers, // 요청마다 수 KB
  });
  // requestLog는 절대 비워지지 않는다
  // → 요청 1만 건이면 수십 MB, 100만 건이면 수 GB
  next();
});
js
// ✅ 크기 제한이 있는 캐시 사용
const LRU = require('lru-cache');
const requestLog = new LRU({ max: 1000 }); // 최대 1000개만 유지
 
// 또는 주기적 정리
const requestLog = [];
setInterval(() => {
  if (requestLog.length > 10000) {
    requestLog.splice(0, requestLog.length - 1000); // 최근 1000개만 유지
  }
}, 60_000);

2. 클로저가 의도치 않게 참조를 유지

js
// ❌ 클로저가 큰 객체의 참조를 잡고 있는 패턴
function createHandler() {
  const hugeData = Buffer.alloc(10 * 1024 * 1024); // 10MB
 
  return function handler(req, res) {
    // handler가 살아있는 한 hugeData도 GC되지 않음
    // handler 내부에서 hugeData를 안 써도!
    // → V8은 클로저의 렉시컬 스코프에 있는 변수를 보수적으로 유지한다
    res.end('ok');
  };
}
 
// handler가 라우터에 등록되어 프로세스 수명만큼 살아있으면
// hugeData 10MB도 영원히 살아남는다
app.get('/api', createHandler());
js
// ✅ 클로저 스코프에 불필요한 대형 객체를 두지 않기
function createHandler() {
  const hugeData = Buffer.alloc(10 * 1024 * 1024);
  const result = processData(hugeData); // 필요한 결과만 추출
  // hugeData는 이 함수가 끝나면 참조를 잃음
 
  return function handler(req, res) {
    res.json(result); // result만 참조 (작은 크기)
  };
}
V8의 클로저 최적화

V8은 실제로 클로저 내부에서 참조하지 않는 외부 변수를 GC 대상으로 만들 수 있다. 하지만 이 최적화는 항상 보장되지 않는다. eval()이 있거나, with 문이 있거나, 디버거가 붙어있으면 V8은 보수적으로 전체 스코프를 유지한다. 안전하게 가려면 불필요한 참조를 명시적으로 정리하는 것이 좋다.

3. 이벤트 리스너 미해제

js
// ❌ 요청마다 리스너를 등록하고 해제하지 않는 패턴
const emitter = new EventEmitter();
 
app.get('/stream', (req, res) => {
  function onData(data) {
    res.write(data);
  }
 
  emitter.on('data', onData);
  // 요청이 끝나도 리스너가 남아있다
  // → 요청 100개면 리스너 100개, 각각 res 객체를 참조
  // → res가 GC되지 않음 → 메모리 릭
 
  res.on('close', () => {
    // 여기서 리스너를 해제해야 한다
    emitter.removeListener('data', onData); // ← 이게 없으면 릭
  });
});

Node.js는 이 패턴을 감지하기 위해 기본적으로 하나의 EventEmitter에 리스너가 10개 이상 등록되면 경고를 출력한다.

text
(node:1234) MaxListenersExceededWarning: Possible EventEmitter memory leak detected.
11 data listeners added to [EventEmitter].
Use emitter.setMaxListeners() to increase limit
setMaxListeners로 경고를 끄지 말자

이 경고가 뜨면 setMaxListeners(0)으로 끄는 것이 아니라, 왜 리스너가 쌓이는지 원인을 찾아야 한다. 경고를 끄면 릭이 조용히 진행된다. 대부분의 경우 리스너 해제(removeListener 또는 off)가 빠져있는 것이 원인이다.

4. 타이머 / 반복 작업 미정리

js
// ❌ 정리되지 않는 setInterval
function startMonitoring(resource) {
  const data = loadExpensiveData(resource); // 10MB
 
  setInterval(() => {
    // data를 클로저로 참조
    checkHealth(data);
  }, 5000);
  // 이 interval을 clearInterval하지 않으면
  // data는 영원히 GC되지 않음
  // resource가 삭제되어도 interval은 계속 도는다
}
 
// 리소스가 추가될 때마다 호출
resources.forEach(startMonitoring);
// → 리소스 100개면 interval 100개, 각각 10MB 참조
js
// ✅ 정리 가능한 구조로
function startMonitoring(resource) {
  const data = loadExpensiveData(resource);
  const intervalId = setInterval(() => checkHealth(data), 5000);
 
  // 정리 함수 반환
  return () => clearInterval(intervalId);
}
 
const cleanups = resources.map(startMonitoring);
// 나중에 정리: cleanups.forEach(fn => fn());

Heap Snapshot - 메모리 릭의 결정적 증거

--trace-gc로 릭이 있다는 것을 확인했다면, 다음 단계는 무엇이 메모리를 잡고 있는지 찾는 것이다. 이때 사용하는 것이 Heap Snapshot이다.

Heap Snapshot이란

V8 Heap에 있는 모든 JS 객체의 스냅샷이다. 각 객체의 타입, 크기, 그리고 누가 이 객체를 참조하고 있는지(retainer chain)를 담고 있다. Chrome DevTools Memory 패널 공식 문서에서 사용법을 다루고 있다.

스냅샷 찍는 방법

js
// 방법 1: 코드에서 프로그래밍 방식으로
const v8 = require('v8');
const fs = require('fs');
 
// 특정 시점에 스냅샷 저장
const snapshotStream = v8.writeHeapSnapshot();
console.log(`Heap snapshot written to: ${snapshotStream}`);
// → Heap.20260320.123456.7890.0.001.heapsnapshot 파일 생성
 
// 또는 시그널로 트리거 (프로덕션에서 유용)
process.on('SIGUSR2', () => {
  const file = v8.writeHeapSnapshot();
  console.log(`Snapshot: ${file}`);
});
// kill -USR2 [PID] 로 외부에서 트리거 가능
js
// 방법 2: --inspect로 Chrome DevTools 연결
// node --inspect server.js
// → chrome://inspect 에서 연결
// → Memory 탭 → Take heap snapshot
bash
# 방법 3: 프로세스 외부에서 (Node.js 12+)
node --inspect=[port] -e "setInterval(() => {}, 1000)"
# 또는 실행 중인 프로세스에 --inspect 활성화
kill -USR1 [PID]  # Node.js에서 USR1은 --inspect 활성화 시그널

스냅샷 분석: 핵심 개념

Chrome DevTools에서 .heapsnapshot 파일을 열면 세 가지 뷰가 있다.

Summary View

text
Constructor    | Count | Shallow Size | Retained Size
───────────────┼───────┼──────────────┼──────────────
(string)       | 50000 |    2,400,000 |    2,400,000
Object         | 30000 |    1,440,000 |    8,500,000
(array)        | 10000 |      480,000 |   12,000,000  ← 의심!
Map            |     5 |          320 |   15,000,000  ← 의심!

Shallow Size vs Retained Size

text
Shallow Size = 이 객체 자체가 차지하는 바이트 수
               (객체 헤더 + 프로퍼티 슬롯)
 
Retained Size = 이 객체가 GC되면 함께 해제될 수 있는 전체 바이트 수
               (이 객체만이 유일하게 참조하는 모든 하위 객체 포함)
text
예시:
 
const cache = new Map();     // Shallow: 64B, Retained: 15MB!
cache.set('a', hugeObject);  // Map 자체는 작지만
cache.set('b', hugeObject2); // Map이 사라져야만 해제되는 것들이 15MB
 
Map (Shallow: 64B)
 ├── 'a' → hugeObject (5MB)     ← Map만이 참조
 └── 'b' → hugeObject2 (10MB)   ← Map만이 참조
 
Retained Size = 64B + 5MB + 10MB ≈ 15MB
→ 이 Map을 비우면 15MB가 해제된다
Retained Size가 큰 객체를 먼저 보라

릭 분석에서 Shallow Size는 별로 중요하지 않다. Retained Size로 정렬해서 가장 큰 것부터 봐야 한다. Retained Size가 비정상적으로 큰 객체는 그 객체가 많은 하위 객체를 잡고 있다는 뜻이고, 이것이 릭의 근원(retainer)일 가능성이 높다.

Retainers (이게 핵심)

특정 객체가 왜 GC되지 않는지, 누가 이 객체를 참조하고 있는지를 역추적할 수 있다.

text
Retainer chain 예시:
 
hugeObject (5MB)
  ← cache (Map) 의 value
    ← (모듈 스코프 변수)
      ← (GC root)
 
해석: hugeObject는 cache(Map)가 참조하고 있고,
      cache는 모듈 스코프에 있어서 GC root에서 도달 가능하다.
      → cache.delete() 하거나 cache = null로 참조를 끊어야
         hugeObject가 GC된다.

3-Snapshot 기법 - 릭 원인 추적 워크플로우

Heap Snapshot 하나만으로는 "이 객체가 원래 있어야 하는 건지, 릭인지" 구별하기 어렵다. Chrome DevTools 문서에서 소개하는 3-Snapshot 기법이 효과적이다.

text
1. 스냅샷 A: 초기 상태 (서버 기동 직후, GC 1회)
2. 작업 수행: 릭이 의심되는 동작을 반복 (예: API 100번 호출)
3. 스냅샷 B: 작업 직후 (GC 1회 후)
4. 같은 작업을 한 번 더 반복
5. 스냅샷 C: 두 번째 작업 후 (GC 1회 후)
 
비교:
  B와 C 사이에 "새로 할당되었지만 해제되지 않은" 객체들
  = Comparison view에서 "# Delta"가 양수인 것들
  = 릭 후보
js
// 3-Snapshot 자동화 스크립트
const v8 = require('v8');
 
async function threeSnapshotAnalysis(workload) {
  // 1. 초기 스냅샷
  global.gc();
  const snap1 = v8.writeHeapSnapshot();
  console.log(`Snapshot 1 (baseline): ${snap1}`);
 
  // 2. 작업 수행
  console.log('Running workload (round 1)...');
  await workload();
 
  // 3. 스냅샷 B
  global.gc();
  const snap2 = v8.writeHeapSnapshot();
  console.log(`Snapshot 2 (after round 1): ${snap2}`);
 
  // 4. 같은 작업 반복
  console.log('Running workload (round 2)...');
  await workload();
 
  // 5. 스냅샷 C
  global.gc();
  const snap3 = v8.writeHeapSnapshot();
  console.log(`Snapshot 3 (after round 2): ${snap3}`);
 
  console.log('\nOpen in Chrome DevTools:');
  console.log('1. chrome://inspect → Open dedicated DevTools for Node');
  console.log('2. Memory tab → Load all 3 snapshots');
  console.log('3. Select snapshot 3 → Comparison with snapshot 2');
  console.log('4. Sort by "# Delta" descending → these are leak candidates');
}
 
// 사용 예시
// node --expose-gc three-snapshot.js
threeSnapshotAnalysis(async () => {
  // 릭이 의심되는 동작을 여기에
  for (let i = 0; i < 1000; i++) {
    await fetch('http://localhost:3000/api/suspected-endpoint');
  }
});

Allocation Timeline - 릭이 언제 발생하는지

Heap Snapshot이 "지금 누가 메모리를 잡고 있는지"를 보여준다면, Allocation Timeline은 "시간 흐름에 따라 언제 어떤 할당이 일어났는지"를 보여준다. Chrome DevTools Memory 탭에서 "Allocation instrumentation on timeline"을 선택하면 사용할 수 있다.

text
Allocation Timeline:
 
시간 →
  │  ▓▓    ▓▓▓▓    ▓▓▓▓▓▓    ▓▓▓▓▓▓▓▓     ← 해제되지 않은 할당 (진한회색)
  │ ░░░░  ░░░░░░  ░░░░░░░░  ░░░░░░░░░░    ← 해제된 할당 (회색)
  └────────────────────────────────────── 시간
 
파란색 막대가 점점 커지는 구간 = 릭이 발생하는 시점
→ 그 구간을 선택하면 해당 시간에 할당된 객체들을 볼 수 있다
→ 어떤 API 호출이나 동작이 릭을 유발하는지 좁힐 수 있다
Allocation Sampling과의 차이

Allocation Timeline은 모든 할당을 추적하므로 오버헤드가 크다. 개발 환경에서 릭을 재현할 때 사용한다. Allocation Sampling은 통계적 샘플링으로 오버헤드가 작다. 프로덕션에 가까운 환경에서 긴 시간 동안 돌릴 때 적합하다.


Clinic.js - Node.js 전용 진단 도구

Clinic.js는 NearForm이 만든 Node.js 성능/메모리 분석 도구 모음이다. 네 가지 도구가 있다.

Clinic Doctor

프로세스의 전반적인 건강 상태를 진단한다. CPU, 메모리, 이벤트 루프 지연, 핸들 수를 시각화한다.

bash
npx clinic doctor -- node server.js
# → 부하를 준 뒤 Ctrl+C로 종료하면 HTML 리포트 생성
# → "memory" 그래프가 우상향하면 릭 가능성 표시

Clinic HeapProfiler

Heap 할당을 프로파일링한다. 어떤 함수가 가장 많은 메모리를 할당하는지를 flame graph로 보여준다.

bash
npx clinic heapprofiler -- node server.js
# → 어떤 함수 호출 경로에서 메모리 할당이 집중되는지 시각화

Clinic Flame

CPU flame graph를 생성한다. GC 시간이 길어서 성능이 떨어지는 경우, flame graph에서 GC 관련 함수가 큰 비중을 차지하는지 확인할 수 있다.

bash
npx clinic flame -- node server.js
# → GC 함수(v8::internal::*)가 전체 CPU의 30% 이상이면
#    메모리 압박으로 GC가 과도하게 돌고 있다는 신호

Clinic Bubbleprof

비동기 작업의 흐름과 지연을 시각화한다. 이벤트 루프 블로킹이나 비동기 작업의 병목을 찾을 때 유용하다.

bash
npx clinic bubbleprof -- node server.js

v8.writeHeapSnapshot() 활용 패턴

프로덕션 환경에서 Heap Snapshot을 안전하게 수집하는 패턴들이다.

메모리 임계값 도달 시 자동 스냅샷

js
const v8 = require('v8');
 
const THRESHOLD_MB = 400; // 400MB 넘으면 스냅샷
let snapshotTaken = false; // 한 번만 찍기
 
setInterval(() => {
  const heapUsed = process.memoryUsage().heapUsed / 1024 / 1024;
 
  if (heapUsed > THRESHOLD_MB && !snapshotTaken) {
    snapshotTaken = true;
    console.warn(`Heap exceeded ${THRESHOLD_MB}MB, taking snapshot...`);
 
    const file = v8.writeHeapSnapshot();
    console.warn(`Snapshot saved: ${file}`);
    // 이 파일을 나중에 DevTools로 분석
  }
}, 30_000);
프로덕션 스냅샷 주의사항

Heap Snapshot을 찍는 동안 프로세스가 수 초간 멈출 수 있다. Heap이 클수록 오래 걸린다. 1GB Heap이면 수십 초가 걸릴 수도 있다. 또한 스냅샷 파일에는 Heap의 모든 데이터(문자열, 객체)가 포함되므로 비밀번호, API 키 등 민감한 데이터가 들어있을 수 있다. 스냅샷 파일은 보안에 주의해서 다뤄야 한다.

시그널 기반 스냅샷 (프로덕션 권장)

js
// 프로세스 시작 시 등록
process.on('SIGUSR2', () => {
  const file = v8.writeHeapSnapshot();
  console.log(`Heap snapshot: ${file}`);
});
 
// 외부에서 필요할 때만 트리거
// $ kill -USR2 $(pgrep -f "node server.js")
// → 스냅샷 파일이 프로세스의 cwd에 생성됨

llnode - Core Dump에서 JS 분석

llnode는 Node.js의 core dump를 분석하는 LLDB 플러그인이다. 프로세스가 크래시했거나, 프로덕션에서 core dump를 떴을 때 사후 분석(post-mortem)에 사용한다.

bash
# core dump 생성 (Linux)
# 1. ulimit 설정
ulimit -c unlimited
 
# 2. Node.js가 크래시하면 core 파일이 생성됨
# 또는 수동으로:
gcore [PID]  # 실행 중인 프로세스의 core dump
 
# 3. llnode로 분석
npm install -g llnode
llnode node -c core.[PID]
bash
# llnode 주요 명령어
(lldb) v8 findjsobjects           # Heap의 모든 JS 객체 타입별 통계
# Instances  Size     Name
#   500000  24000000  Object       ← 의심!
#   100000   4800000  (string)
#        5       320  Map
 
(lldb) v8 findjsobjects -d        # 상세 (프로퍼티 구조별 분류)
(lldb) v8 inspect [address]       # 특정 객체의 내용 확인
(lldb) v8 findrefs [address]      # 이 객체를 참조하는 다른 객체 찾기
llnode는 언제 쓰는가

Heap Snapshot은 살아있는 프로세스에서만 찍을 수 있다. 프로세스가 OOMKilled로 이미 죽었다면? Core dump + llnode가 유일한 방법이다. SRE/인프라 팀이 Node.js 장애를 분석할 때 사용하는 고급 도구다.


실전 포스트모템: 흔한 시나리오들

몇몇 기술 블로그에서 공개한 실제 메모리 릭 사례들의 패턴을 정리한다.

시나리오 1: 무한히 자라는 캐시

text
증상:
- 배포 후 수 시간에 걸쳐 메모리가 서서히 증가
- 재시작하면 정상으로 돌아옴
- 트래픽이 많을수록 빠르게 증가
 
원인:
- 인메모리 캐시에 TTL이 없음
- 또는 캐시 키가 사용자별로 달라서 무한히 커짐
 
Heap Snapshot에서 보이는 것:
- Map 또는 Object의 Retained Size가 비정상적으로 큼
- Retainer chain: GC root → 모듈 변수 → Map → 대량의 value 객체
js
// 전형적인 원인 코드
const cache = {}; // 또는 new Map()
 
function getUser(id) {
  if (!cache[id]) {
    cache[id] = db.fetchUser(id);
    // 삭제 로직이 없으면 → 사용자 수만큼 캐시가 커짐
  }
  return cache[id];
}

시나리오 2: 클로저에 잡힌 요청 컨텍스트

text
증상:
- 특정 API 엔드포인트를 호출할 때마다 메모리 증가
- GC 후에도 줄지 않음
 
원인:
- 비동기 콜백이나 이벤트 리스너가 req/res 객체의 참조를 유지
- 요청이 끝나도 콜백이 정리되지 않아서 req/res가 GC되지 않음
 
Heap Snapshot에서 보이는 것:
- IncomingMessage(req), ServerResponse(res) 객체가 비정상적으로 많음
- Retainer chain이 EventEmitter의 리스너 배열로 이어짐

시나리오 3: 외부 메모리 릭 (heapUsed 정상, RSS 증가)

text
증상:
- heapUsed는 안정적인데 RSS가 계속 증가
- --trace-gc 로그에서 이상 없음
- 결국 OOMKilled (exit 137)
 
원인 후보:
- Buffer를 생성하고 해제하지 않음 (external 증가)
- native addon(C++ 모듈)의 메모리 릭
- 파일 디스크립터 누수 (소켓, 파일 핸들을 안 닫음)
- child_process에서 stdout/stderr 파이프를 소비하지 않음
 
분석 방법:
- process.memoryUsage().external 추적
- lsof -p [PID] | wc -l 로 열린 파일 디스크립터 수 확인
- /proc/[PID]/status의 RssAnon 추적

디버깅 워크플로우 요약

릭이 의심될 때의 단계별 접근법이다.

text
Step 1: 릭인지 확인
  → --trace-gc: Mark-Compact 후 Heap 바닥이 올라가는가?
  → process.memoryUsage() 주기적 로깅: 어떤 필드가 올라가는가?
     heapUsed ↑ → V8 Heap 릭 (JS 객체)
     external ↑ → off-heap 릭 (Buffer, native)
     rss만 ↑   → libuv, native addon, page cache
 
Step 2: 범위 좁히기
  → Clinic Doctor: 전반적 건강 상태
  → Allocation Timeline: 릭이 어느 시간대에 발생하는가
  → 특정 API 호출 후 메모리가 올라가면 → 해당 코드 경로 집중
 
Step 3: 원인 찾기
  → 3-Snapshot 기법: B → C 사이 새로 할당된 객체 비교
  → Retained Size 정렬 → 가장 큰 retainer 찾기
  → Retainer chain 역추적 → GC root까지의 참조 경로 확인
  → 이 참조를 끊으면 릭이 해결됨
 
Step 4: 수정 후 검증
  → 같은 부하로 다시 테스트
  → --trace-gc에서 Mark-Compact 후 Heap이 안정적인지 확인
  → process.memoryUsage()가 일정 범위에서 유지되는지 확인

정리

도구용도환경
--trace-gcGC 빈도, pause, Heap 추이 확인개발 / 스테이징
process.memoryUsage()각 메모리 영역 모니터링모든 환경
Heap Snapshot객체별 메모리 점유, retainer 분석개발 / 스테이징
3-Snapshot 기법릭 객체 특정개발
Allocation Timeline릭 발생 시점 특정개발
Clinic.js종합 진단 (Doctor, Flame, HeapProfiler)개발 / 스테이징
v8.writeHeapSnapshot()프로그래밍 방식 스냅샷프로덕션 가능 (주의)
llnodecore dump 사후 분석프로덕션 장애 분석

메모리 릭의 본질을 한 줄로 요약하면,

GC root에서 도달 가능한 참조가 의도치 않게 남아있는 것이다.

이 시리즈에서 다룬 JS → V8 → Node → libuv → OS의 전체 흐름을 이해하면, 어디에서 메모리가 새는지를 계층별로 좁혀서 분석할 수 있다. Heap Snapshot의 retainer chain이 바로 그 참조를 역추적하는 도구일 수 있겠다.


레퍼런스

공식 문서

도구

  • Clinic.js - Node.js 성능/메모리 진단 도구 모음
  • llnode - Core dump에서 V8 Heap 분석
  • v8 모듈 - writeHeapSnapshot, getHeapStatistics 등

실전 사례 / 참고