nodejs memory leak

v8과 원인에 대하여

myungshinKang
Research Team — DAWN
8 min readOct 31, 2021

--

주제 선정 배경

nodejs로 프로그램을 만들고 24/7 프로세스를 돌리던 중 프로세스가 아무 오류 없이 셧다운되는것을 발견하였다. 프로세스 매니저인 pm2에 no-auto-restart 플래그를 붙이고 돌려보았더니 비슷한 시간대에 프로세스가 종료되는 것을 알 수 있었다. 그래서 여러가지로 원인을 조사해본 결과 메모리 누수때문에 메모리가 계속 올라가서 v8엔진의 메모리 한계를 넘어버렸기 때문에 프로세스가 강제로 종료됐다고 분석하였다.

결과적으로 내 프로그램의 메모리 누수는 전역변수 메모리의 미회수와 다중참조때문이었다. 이를 해결하기 위한 방법을 생각해두었으나 다른 프로젝트의 문제가 산적해있어 일단은 issue에만 남겨두고 nodejs의 child process를 통해 exec를 수행하면서 일정 시간마다 프로세스를 재시작 하는것으로 미봉책을 하였다. 이번 문제를 통해 몰랐던 지식을 알게 되어서 다행이라고 생각한다.

메모리 누수란?

필요하지 않은 메모리를 계속 점유하고 있는 현상이다. 할당된 메모리를 사용한 다음 반환하지 않는 것이 누적되면 메모리가 낭비된다. 즉, 더 이상 불필요한 메모리가 해제되지 않으면서 메모리 할당을 잘못 관리할 때 발생한다. 간단하게는 운영체제나 사용 가능한 메모리 풀에 반환되지 않은 메모리라고 정의할 수 있다. 만약에 프로그램이 사용하지 않는 힙을 헤제하지 않고 지속한다면 프로그램 충돌이 발생할 것이다.

V8과 NodeJS

official nodejs

위 설명에서 알 수 있듯이 nodejs의 메모리 관리 방식을 안다는 것은 v8의 메모리 관리 방식을 안다는 것과 동일한 말이다. 아래는 nodejs 의 구조이다. libuv는 따로 깊게 다루지 않고 v8 engine이 nodejs를 구동하는 엔진이라는 것만 보면된다.

V8은 자바스크립트를 native code로 컴파일해서 실행한다. 실행되는 동안 메모리를 필요한 만큼 할당하고 회수한다. 이는 위에서도 언급했듯이 nodejs의 메모리 관리에 대해서 논한다면 반드시 V8에 대해서 언급할 수 밖에 없다는 것을 뜻한다.

V8 메모리 관리

precise
simple
  • Code : 실행될 실제 코드를 저장해둔 곳이다.
  • Stack : 힙에 있는 오브젝트를 참조하는 포인터와 함께 모드 value 타입을 포함한다. (integer, boolean과 같은 기본 요소) 변수, 함수, 클래스, 함수프레임과 같은 정적 데이터가 저장되는 장소이다. Number, String, boolean, Null, Undefined, Symbol.
  • Heap : 오브젝트, 스트링, 클로저와 같은 레퍼런스 타입을 저장하기 위한 전용 메모리 세그먼트이다.

V8 은 두 가지 주요 메모리 범주를 처리한다. stack 과 heap 이다. 여기서 알아야 할 것은 자바스크립트가 어떤 것을 레퍼런스로 콜하고 어떤 것을 밸류로 콜하는지이다.

  • 배열과 객체는 레퍼런스(주소)로 콜한다.
  • 그 외의 모든것(리터럴)은 벨류로 콜한다.
  • 배열과 객체의 대입은 해당 데이터가 저장된 메모리의 주소가 대입되는 것이다.
  • 그 외 모든 것의 대입은 해당 데이터의 복제 값이 대입되는 것

V8 Garbage Collector (gc)

V8은 두가지 타입의 garbage collection을 사용한다.

  • Scavenge : 빠르고 불완전하다.
  1. 새로운 데이터를 From 영역의 빈 공간에 할당하려고 시도한다.
  2. 할당할 수 없다면, 스케벤져를 실행시켜 살려야 할 데이터를 선택한다.
  3. 살릴 데이터만 From 영역에서 To 영역으로 이동시킨다.
  4. 아직도 From 영역에 남아있는 데이터를 제거한다.
  5. 이제 To 영역을 From 영역처럼 사용한다.
  • Mark-Sweep : 느리지만 참조하지 않은 데이터를 회수한다.

scavenger의 원리 중 참조카운팅에서 순환참조의 문제로 근래에는 Mark Sweep (And Compact) 방식을 사용한다. 핵심 원리는 Root에서 닿을 수 있는 데이터만 표시(Mark)하고 마킹되지 않은 데이터는 청소(Sweep)하는 것이다. 따라서 이 전략은 루트에서 닿을 수 없는 데이터를 쓰레기로 정의한다.

실제로 V8이 사용하고 있는 Major GC 는 다음 3가지의 세부 동작을 실행한다.

  • Mark : 루트에서 시작해서 닿을 수 있는 데이터에 표시를 한다.
  • Sweep : 마크되지 않은 데이터를 전부 삭제한다.
  • Compact : 단편화를 줄이기 위해 데이터를 한쪽으로 끌어모은다. (=디스크 조각 모음)

Leak 의 원인

  1. 전역변수 : 전역변수는 항상 루트에서 도달할 수 있으므로, 결코 가비지 컬렉터에 의해 수집되지 않는다. 전역변수가 의도치 않게 생기는 경우도 있다.
  2. 클로저 : 함수가 종료되면 해당 함수의 모든 지역변수는 파기되어야 한다. 하지만 클로저의 경우 그렇지 않다. 자바스크립트는 함수가 종료된 이후에도 지역변수가 제거되지 않도록 할 수 있다. 클로저를 통해서이다. 내부참조에 의해 지역변수의 파기를 늦추는 것이다. 클로저로 인해 지역변수가 힙으로 이동되었다면 클로저가 제거될 때까지 garbage collector의 대상이 되지 않는다.
  3. 다중참조, console 함수에서 생성된 변수 등 다양한 원인이 있다.

Leak을 찾는 방법

node — inspect 라는 내장 기능을 통해 파일을 실행한다. 그 이후 chrome://inspect를 통해 확인한다.

chrome://inspect

node-memwatch와 같은 써드파티 방법도 있다.

reference

--

--