heap out of memory 에러 해결과 메모리 누수 검사

Server The SOPT·2022년 7월 22일
11
post-thumbnail

작성자: 추서연
작성자의 한마디: "가장 좋은 방법은 누수 없는 코드 짜기,,"

[에러] JavaScript heap out of memory

무중단 배포를 위해 build 하던 중 터미널에서 요런 에러가 발생했습니다.

$ yarn run build
yarn run build
yarn run v1.22.19
$ tsc

<--- Last few GCs --->
ll[7385:0x55e7520]    16439 ms: Mark-sweep (reduce) 486.9 (495.9) -> 485.7 (496.1) MB, 394.6 / 0.0 ms  (+ 63.3 ms in 15 steps since start of marking, biggest step 18.9 ms, walltime since start of marking 493 ms) (average mu = 0.237, current mu = 0.247) allo[7385:0x55e7520]    17047 ms: Mark-sweep (reduce) 486.7 (496.1) -> 486.1 (496.1) MB, 392.2 / 0.0 ms  (+ 57.2 ms in 14 steps since start of marking, biggest step 29.2 ms, walltime since start of marking 495 ms) (average mu = 0.248, current mu = 0.261) allo

<--- JS stacktrace --->

FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
 1: 0xb0a860 node::Abort() [/home/ubuntu/.nvm/versions/node/v16.16.0/bin/node]
 2: 0xa1c193 node::FatalError(char const*, char const*) [/home/ubuntu/.nvm/versions/node/v16.16.0/bin/node]
 3: 0xcf9a6e v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, bool) [/home/ubuntu/.nvm/versions/node/v16.16.0/bin/node]
 4: 0xcf9de7 v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, bool) [/home/ubuntu/.nvm/versions/node/v16.16.0/bin/node]
 5: 0xeb1685  [/home/ubuntu/.nvm/versions/node/v16.16.0/bin/node]
 6: 0xeb2166  [/home/ubuntu/.nvm/versions/node/v16.16.0/bin/node]
 7: 0xec068e  [/home/ubuntu/.nvm/versions/node/v16.16.0/bin/node]
 8: 0xec10d0 v8::internal::Heap::CollectGarbage(v8::internal::AllocationSpace, v8::internal::GarbageCollectionReason, v8::GCCallbackFlags) [/home/ubuntu/.nvm/versions/node/v16.16.0/bin/node]
 9: 0xec404e v8::internal::Heap::AllocateRawWithRetryOrFailSlowPath(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) [/home/ubuntu/.nvm/versions/node/v16.16.0/bin/node]
10: 0xe8558a v8::internal::Factory::NewFillerObject(int, bool, v8::internal::AllocationType, v8::internal::AllocationOrigin) [/home/ubuntu/.nvm/versions/node/v16.16.0/bin/node]
11: 0x11fe2d6 v8::internal::Runtime_AllocateInYoungGeneration(int, unsigned long*, v8::internal::Isolate*) [/home/ubuntu/.nvm/versions/node/v16.16.0/bin/node]
12: 0x15f2d39  [/home/ubuntu/.nvm/versions/node/v16.16.0/bin/node]
Aborted (core dumped)
error Command failed with exit code 134.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

'JavaScript heap out of memory'라는 이 에러는 Heap 메모리가 부족해서 발생한 것입니다.

[해결책 1] 더 큰 메모리 할당

  • 현재용량 확인하는 방법
$ node -e 'console.log(v8.getHeapStatistics().heap_size_limit/(1024*1024))'
$ 499.25
  • 용량 늘리는 방법
$ export NODE_OPTIONS=--max_old_space_size=800

이 두 코드를 터미널 창에 입력해, 용량을 늘린 후 현재 용량을 다시 확인하면 499.25에서 800으로 용량이 늘어나있음을 확인할 수 있습니다.
(800은 예시일 뿐, 용량 크기는 변경해보면서 적절하게 조절하시면 더 좋습니다. 저희는 600, 700, 800 순으로 차례로 테스트해보다 800부터 에러가 해결되는 걸 확인할 수 있었습니다.)

매번 서버를 재배포할 때마다 터미널에 위 코드를 입력하는게 번거로우시다면, .bashrc 파일에 위의 용량 늘리는 코드를 그대로 추가하셔도 됩니다.

저희 서버의 경우, 이 build 에러로 인해 무중단 배포에 실패해 불편함을 겪고 있었습니다,, (400 에러로 서버가 꺼지면 수동으로 다시 직접 켰습니다,,)

빠르게 build 에러를 해결하는 게 중요했기에 [해결책 1]을 우선 사용했고, 무중단 배포를 성공할 수 있었습니다.

그러나 [해결책 1]은 근본적인 해결책이 아닙니다. 어딘가에서 메모리 누수가 발생하고 있는 것일 수 있으므로 메모리 누수를 검사해 봐야합니다.

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

NodeJS에서 메모리 누수 검사를 하는 방법은 기본 inspector 또는 추가적인 메모리 누수 관측기를 사용하는 것입니다. 이 글에선 '노드 크롬 디버거'를 사용해 메모리 누수를 관측하는 방법 두 가지를 설명드리겠습니다.

방법 1) chrome://inspect로 켜는 노드 크롬 디버거

  • 장점: 과정이 간단함
  • 단점: 활용도 낮음(다른 툴로 확장 x)
  • 요약: 터미널로 서버 키고 chrome://inspect 접속해서 디버거 켬. 디버거에서 직접 스냅샷 촬영하고 내용 확인함.

1. 터미널에서 프로젝트 루트 폴더 열기
2. 터미널에 아래 코드 입력하기 (src/index.ts는 본인 프로젝트의 시작 파일 경로로 수정하기)
-시작파일이 TypeScript인 경우:
node --loader ts-node/esm --inspect src/index.ts
-시작파일이 JavaScript인 경우:
node --inspect src/index.js 위와 같이 시작파일이 제대로 동작하는 걸 확인했다면 3으로 넘어갑니다.
3. 크롬으로 chrome://inspect 열기. 조금 기다리다보면 두 번째 사진처럼 구동 중인 서버가 뜹니다. 4. inspect 클릭해서 디버거 켜기.
5. 메모리/프로필에서 1.1kB/초와 같이 나와있는 곳 유심히 살펴 보기. 이 상태에서 메모리 부하 일으키는 작업 실행해봤을 때, 이 수치가 계속해서 크게 증가한다면 누수일 가능성이 큽니다. 6. 스냅샷 촬영 이용해서 아무것도 하지 않은 상태에서 찍은 스냅샷과 부하 일으킨 후 찍은 스냅샷 비교하기.

방법 2) heapdump를 이용해 작동하는 노드 크롬 디버거

  • 장점: heapdump의 활용도가 높음 (다른 툴에서 활용 가능)
    - ex) IBM에서 제공하는 HeapAnalyzer툴은 heapdump를 분석함
  • 단점: 과정이 복잡함
  • 요약: 프로젝트에 heapdump 추가하고 새로 만든 API 이용해서 스냅샷 촬영함. 크롬 개발자 도구 열어서 스냅샷 내용 확인함.
  1. 프로젝트에 heapdump 모듈 설치하기
npm install heapdump
yarn add heapdump

만일 설치 시 아래와 같은 error가 난다면?
[해결책] node-modules 삭제 후 yarn으로 다시 만들기. 후에 yarn add heapdump 하면 정상적으로 깔림

$ yarn add heapdump
yarn add v1.22.18
warning package-lock.json found. Your project contains lock files generated by tools other than Yarn. It is advised not to mix package managers in order to avoid resolution inconsistencies caused by unsynchronized lock files. To clear this warning, remove package-lock.json.
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
warning "firebase-admin > @firebase/database-compat > @firebase/database > @firebase/auth-interop-types@0.1.6" has unmet peer dependency "@firebase/app-types@0.x".
[4/4] Building fresh packages...
[1/3] ⢀ protobufjs
[-/3] ⢀ waiting...
error C:\Users\choo0\Documents\Repository\RecorDream-Server\node_modules\heapdump: Command failed.
Exit code: 1
Command: node-gyp rebuild
Arguments:
Directory: C:\Users\choo0\Documents\Repository\RecorDream-Server\node_modules\heapdump
Output:
C:\Users\choo0\Documents\Repository\RecorDream-Server\node_modules\heapdump>if not defined npm_config_node_gyp (node "C:\Program Files\nodejs\node_modules\npm\bin\node-gyp-bin\\..\..\node_modules\node-gyp\bin\node-gyp.js" rebuild )  else (node "" rebuild )
gyp info it worked if it ends with ok
gyp info using node-gyp@8.4.1
gyp info using node@16.14.2 | win32 | x64
gyp info find Python using Python version 3.10.4 found at "C:\Users\choo0\AppData\Local\Programs\Python\Python310\python.exe"
gyp ERR! find VS
...
  1. package.json에 heapdump 모듈 의존성 추가하기
...
"dependencies": {
	...
	"heapdump": "~0.3.7"
    ...
} ...
  1. index.ts에 아래 코드 추가하기. "C:/Users/choo0/Desktop/" 부분은 각자 본인이 희망하는 snapshot 저장 장소로 바꾸기. (중요!) (저는 제 컴퓨터(choo0)의 바탕화면으로 했습니다)
var heapdump = require("heapdump");
app.use("/heapdump", function (req, res, next) {
  var filename = "C:/Users/choo0/Desktop/" + Date.now() + ".heapsnapshot";
  heapdump.writeSnapshot(filename);
  res.send("Heapdump has been generated in " + filename);
});	
  1. http://localhost:8000/heapdump 에 접속하면 지정해둔 장소에 snapshot이 만들어짐. (8000은 본인이 설정한 포트번호로 바꾸기)
    (생성된 파일의 연결 프로그램은 중요치 않습니다. 제 컴퓨터에선 워드로 보이는데 무시하셔도 됩니다. 파일 형식만 HEAPSNAPSHOT 파일(.heapsnapshot)이면 됩니다. )
  2. 메모리 부하를 일으킨 후, 다시 http://localhost:8000/heapdump에 접속해서 snapshot 만들기. (이 snapshot은 현재 node.js가 사용중인 메모리 양이 클수록 추출하는 속도가 느려집니다.)
  3. 크롬 개발자 도구(오른쪽 상단 더보기(점 3개) > 도구 더보기 > 개발자 도구) 열기.
  4. 메모리/프로필에서 로드 버튼 눌러 앞서 만든 힙덤프 파일(snapshot) 올리기.
  5. snapshot 클릭해서 열어서 수없이 반복되는 객체가 있나 확인하기. 이를 통해 어떤 객체들이 메모리를 많이 점유해서 메모리 누수를 유발하는지 찾아낼 수 있습니다.

추가)
아래 코드 중 하나를 Controller에 추가해서 api를 여러 번 반복적으로 돌려보며 변하는 메모리 추이를 보고 간이로 누수를 체크해볼 수도 있습니다. (계속해서 크게 증가한다면 누수 가능성 큼)

console.log(process.memoryUsage().rss / 1024 / 1024 + "MB");
res.send((process.memoryUsage().rss / 1024 / 1024) + 'MB');

참고

inspect -> https://ajh322.tistory.com/243
heapdump -> https://bcho.tistory.com/1097
heapdump -> https://yarnpkg.com/package/heapdump

profile
대학생연합 IT벤처창업 동아리 SOPT 30기 SERVER 파트 기술 블로그입니다.

1개의 댓글

comment-user-thumbnail
2023년 8월 2일

환경에서 메모리 사이즈 확인해 봤더니 4144인 거예요... 이 프로젝트 사이즈가 너무 심각해서 그런건가 싶기도 하고... 정말 어이가 없는... ㅋㅋㅋㅋㅋㅋㅋㅋㅋ

프론트 힙을 5기가 정도로 잡아줘야 하는지... 이렇게 비효율적인 프로젝트 투입되어 있기도 쉽지 않은데 참... 놀랐어요... 덕분에 메모리 누수도 체크할 생각 들었네요... 감사합니다.

답글 달기