개발.ZIP/Web.ZIP

[Node.js] 메모리 누수 검사.ZIP

NURGET 2024. 10. 18. 15:22

# V8 가비지 컬렉션

힙은 메모리 할당이 필요한 곳이고, 이는 여러 generational regions로 나눠지는데이 region들은 단순히 generations이라고 불리우고, 이 객체들은 라이프 사이클 동안 같은 세대(generation)를 공유한다.

 

여기에는 young generation과 old generation이 있는데, young generation의 young objects는 또 다시
nursery(유아)와 intermediate(중간) 세대로 나뉘게 된다.

 

이 객체들이 가비지 컬렉션에서 살아남게 되면, older generation에 합류하게 된다.

 

 

# Node.js 메모리 소비 영역

1. Code

2. Call Stack (숫자, 문자열, boolean과 같은 primitive values 함수)

3. heap memory

 

먼저 내 프로젝트에서 pm2 배포 과정에서 아래와 같은 오류가 발생했다.

0|project_name  | <--- Last few GCs --->
0|project_name  | [111912:0x54568b0]    58724 ms: Mark-sweep (reduce) 252.7 (258.4) -> 252.0 (258.4) MB, 337.6 / 0.0 ms  (+ 186.4 ms in 48 steps since start of marking, biggest step 58.3 ms, walltime since start of marking 1445 ms) (average mu = 0.786, current mu = 0.774) [111912:0x54568b0]    59984 ms: Mark-sweep 253.3 (258.7) -> 252.7 (260.4) MB, 1202.4 / 0.0 ms  (average mu = 0.537, current mu = 0.046) allocation failure; scavenge might not succeed
0|project_name  | <--- JS stacktrace --->
0|project_name  | FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
0|project_name  |  1: 0xb9c1f0 node::Abort() [node]
0|project_name  |  2: 0xaa27ee  [node]
0|project_name  |  3: 0xd73950 v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, bool) [node]
0|project_name  |  4: 0xd73cf7 v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, bool) [node]
0|project_name  |  5: 0xf51075  [node]
0|project_name  |  6: 0xf6354d v8::internal::Heap::CollectGarbage(v8::internal::AllocationSpace, v8::internal::GarbageCollectionReason, v8::GCCallbackFlags) [node]
0|project_name  |  7: 0xf3dc3e v8::internal::HeapAllocator::AllocateRawWithLightRetrySlowPath(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) [node]
0|project_name  |  8: 0xf3f007 v8::internal::HeapAllocator::AllocateRawWithRetryOrFailSlowPath(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) [node]
0|project_name  |  9: 0xf2020a v8::internal::Factory::NewFillerObject(int, v8::internal::AllocationAlignment, v8::internal::AllocationType, v8::internal::AllocationOrigin) [node]
0|project_name  | 10: 0x12e543f v8::internal::Runtime_AllocateInYoungGeneration(int, unsigned long*, v8::internal::Isolate*) [node]
0|project_name  | 11: 0x17120b9  [node]
0|project_name  | Aborted (core dumped)

 

너무 기니까 이 부분을 보면 된다.

0|finemotion | FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory

→ 이 부분을 확인해보면 자바스크립트 힙 메모리 부족으로 인한 문제임을 확인할 수 있다.

 

Node.js 어플리케이션이 힙 메모리 한도를 초과했기 때문에 발생하는 오류다. Node.js는 기본적으로 약 512MB에서 1.5GB의 메모리를 사용하도록 제한되어 있는데, 이를 초과했기 때문에 오류가 발생한 것으로 유추할 수 있다!

 

[해결책 1] Node.js의 메모리 할당량 늘리기

-max-old-space-size 플래그를 사용하면 힙 메모리의 최대 크기를 설정할 수 있다.

pm2 restart finemotion --node-args="--max-old-space-size=4096"

 

이 명령어는 Node.js 어플리케이션에 4GB의 힙 메모리를 할당할 수 있다. 필요한 경우 2048 또는 8192 등으로 조정할 수 있다.

 

[해결책 2] 메모리 누수 검사

사실 메모리 할당량을 느리는 건 임시 방편이고, 만약 끈임없이 메모리 사용량이 증가하면 힙 메모리의 크기를 설정해봤자라고 생각했다.

 

그래서 memory leak을 확인해보려고 한다. 일정 시간 동안 어플리케이션에 메모리 사용량이 계속 증가한 경우,

불필요한 객체가 메모리에 남아있다는 건데.. Node.js 어플리케이션을 프로파일링하거나 로깅을 통해 메모리 사용 패턴을 추적해야 한다.

 

그리고 데이터베이스 쿼리나 대량 데이터 처리 중 불필요한 데이터를 메모리에 오래 유지하지 않도록 개선해주려고 한다.

예시로 페이징을 통해 한 번에 처리하는 데이터 양을 줄여보는게 필요한 것 같다.

 

- 메모리 누수를 일으키는 대표적인 요인들

  1. 전역변수
  2. 해제되지 않은 타이머 (clearTimeout 등)
  3. 클로저

 

1. chrome://inspect 노드 크롬 디버거

 

commonJS 환경에서 디버깅하는 명령어를 사용했다. (—inspect)

지금 상황 같은 경우 힙 메모리 부족으로 중단되는 걸 방지하기 위해 임시로 메모리 한도를 4GB로 늘리고 진행했다.

node --inspect --max-old-space-size=4096 -r ts-node/register src/app.ts

 

스냅샷 찍어서 노가다로 비교하면 된다...

 

 

나는 1, 2번을 많이 사용했다. (Heap snapshot, Allocation instrumentation on timeline)

어느 정도 사용량을 주고 스냅샷을 여러개 찍어서, Statistics로 확인해보면 원형 차트로 한 눈에 볼 수 있다!

 

 

 

 

* Inspect 검사 도구에서 알아두면 좋은 단어들

  1. shallow Size(얕은 크기): 오브젝트 자신의 크기 (bytes)
  2. Retined Size(유지된 크기): 나 자신 + 참조하고 있는 오브젝트들의 크기 (bytes)

→ Shallow Size 크기 대비 Retained Size가 큰 걸 찾아야 함. (Retained Size를 내림차순으로 찾아보기)

 

2. GC 모니터링 코드

Node.js 에서는 V8 모듈을 이용해 GC 이벤트를 추적할 수 있다. 

아래 작성한 코드를 통해 메모리 사용량을 모니터링 하고, 누수가 발생하는지 감지 할 예정이다.

 

const v8 = require('v8');
const { exec } = require('child_process');

// 주기적으로 메모리 사용량 확인
setInterval(() => {
  const memoryUsage = process.memoryUsage();
  const heapStatistics = v8.getHeapStatistics();

  console.log('Heap Total:', memoryUsage.heapTotal / 1024 / 1024, 'MB');
  console.log('Heap Used:', memoryUsage.heapUsed / 1024 / 1024, 'MB');
  console.log('Heap Limit:', heapStatistics.heap_size_limit / 1024 / 1024, 'MB');

  // 힙 메모리 누적 확인
  if (memoryUsage.heapUsed > 300 * 1024 * 1024) { // 300MB를 초과하면 경고
    console.warn('메모리 사용량이 너무 높습니다!!!');
  }
}, 10000); // 10초마다 실행

 

그럼 아래와 같은 로그를 터미널에서 확인할 수 있다.

Heap Total: 337.40625 MB
Heap Used: 325.72188568115234 MB
Heap Limit: 4144 MB
메모리 사용량이 너무 높습니다!!!