hmk run dev

Node.js 메모리 누수(feat. SSR) 본문

node

Node.js 메모리 누수(feat. SSR)

hmk run dev 2024. 3. 25. 21:38

이 글로 얻을 수 있는 것

- 메모리릭을 디버깅 할 수 있는 자신감!

 

- Node.js 환경이든 브라우저 환경이든

  크롬 브라우저의 Memory 탭에서 메모리 누수의 범인을 찾을 수 있음!

 

 

메모리 누수(Memory Leak)이란?

 

메모리 누수는 실제로는 필요하지 않은데,

메모리를 계속 차지하고 있는 현상

 

 

메모리 누수가 있으면 뭐가 문제일까?

 

JS를 동작시킬 메모리가 부족하니까, 성능이 좋지 않게 된다.

 

- GC의 활동이 늘어나면, CPU 사용량이 늘어난다.

- CPU intensive한 작업이 늘어나면, 이벤트 루프가 블로킹되서 연산이 느려진다.

 

 

띄워놓은 Node.js 서버가 죽는다.

 

- SIGABRT 등의 시그널로 인한 프로세스 종료

- 인스터스가 재시작되고 일부 요청에 대한 응답이 실패할 수 있음

- 로드밸런서가 HTTP Status 502(Bad Gateway) 에러를 뱉을 수 있어요

- 가용성에 문제가 있을 수 있단 뜻!

 

 

메모리 누수 해결방법은?

1. 힙메모리를 늘려주거나 => 근본적인 해결방법은 아니다!

2. 메모리 누수의 범인을 디버깅한다.

 

 

메모리 누수가 있는지 어떻게 알 수 있을까?

 

아래와 같이 터미널에서 확인할 수 있다!

실제로는 터미널을 계쏙 보고 있기보다는, 모니터링 툴을 활용한다!

 

 

 

메모리 누수 모니터링

모니터링은 크게 서버사이드와 클라이언트 사이드로 나뉠 수 있습니다.

 

서버의 경우에는 크게 아래와 같은 서비스를 이용해 모니터링을 합니다!

 

클라이언트 사이드 같은 경우에는 모니터링 도구 자체를 붙이기는 어렵다.

=> 어떤 브라우저를 쓰는지, 하드웨어가 어떤건지에 대해서 많이 달라질 수 있기때문 

 

 

 

메모리 누수가 발생하는 코드는 모니터링 도구에서 어떻게 보일까?

 

코드로 살펴 봅시다.

 

- 요청을 받으면 응답을 내려주고 200 status를 내려주는 node.js 코드

const server = http.createServer((req, res) => {
	res.writeHead(200, {'Content-Type': 'text/html'});
    res.write(`
    	<!DOCTYPE html>
        <html lang="en">
            <head>
		<title>Hello World</title>
            </head>
            <body>
            	<h1>Content</h1>
            </body>
	</html>
    `);
    
    res.end();
})

 

 

SSR 이면서 유저의 트래픽을 받는 상황을 재현하기 위해

아래와 같이 내가 띄워놓은 로컬 서버에 쉘스크립트를 이용해 요청을 보내는 방법을 재현했습니다.

 

 

메모리 누수가 없는 코드

 

 

- 백만번의 반복문을 돌려 listItems에 i 값을 push

 

- 현재 프로세스의 힙메모리 사이즈 측정

 

 

- 요청 로깅 및 메모리 측정

 

 

위와 같이 메모리 사이즈가 고만고만하고 일정한 경우엔

모니터링 도구의 메모리 사용 그래프는 아래 처럼 크게 변동이 없음!

 

 


 

메모리 누수가 있는 코드

 

메모리 누수가 없는 코드와 별반 차이는 없지만

listItems변수를 전역변수로 선언했음

 

 

메모리 사용량이 점차 올라간다!

 

 

그러다가 서버가 죽는다!

 

 

 

 

 


메모리 누수를 해결해보자!

 

1. 힙메모리 사이즈 늘리기

 

결론적으론 아니다!

힙메모리를 늘려줘도 위의 코드는 계속해서 메모리 누수를 일으킨다!

 

 

 

그전에 간략하게 V8이 메모리를 관리하는 방식을 알아보자

 

V8은 GC 알고리즘으로 Mark and Sweep 방식을 주로 사용합니다.

간단히 말해서, 사용하는 것은 Mark 사용하지 않는 것은 Sweep(치워버림) 합니다.

 

 

원시타입이 아닌(array, object, function- js는 함수도 일급객체 등등..)
경우 변수들은 힙메모리에 할당 받아서 사용하게 되는데,

 

사용하는 것은 mark 아닌것은

GC가 한번 돌면서 불필요한 객체를 수거해서 Sweep!

 

 

 

GC에 의해 정리되지 않고 계속해서 메모리가 살아남아 있다면? 

 

V8은 힙메모리를 관리하기 위해 영역을 나눠서 관리합니다.

 

기본적으로 Young Generation과 Old Generation으로 나눠져 있고,

GC도 Minor와 Major로 나눠져 있습니다.

 

 

 

처음 선언한 객체가 있다면 nursery(유아기) 영역에 해당되는 곳에 메모리가 할당되게 된다.

 

 

 

그리고 GC가 한번 돌고 해당 메모리가 살아있다면

중간기에 해당하는 intermediate로 넘어가게 됩니다.

 

 

두 번째에도 GC에서 살아남았다?

Old Generation으로 가게된다.

 

V8 문서를 살펴보게 되면 Old Generation 영역까지 살아남은 메모리는 거의없다! 라고 나와 있습니다.

 

Nursey: 처음 Object이 할당된 공간

Intermeditate: 첫 GC에서 살아남아 이동한 공간

Old Generation: 두 번째 GC에서 살아남아 이동한 공간

 

 

 

Old Generation이 꽉찬 상태가된다?

-> 서버가 죽는다! 꿱!

 

 

전역으로 선언된 listItems는

함수안에서 계속해서 참조하기 때문에 Old Generation 영역으로 이동하게 될 것 입니다.

 

요청이 올 수록 계속해서 GC에 의해 수거되지 않고 메모리 영역을 계속해서 커집니다!

 

 

그렇다면 힙 메모리를 늘려주면 메모리 누수가 사라질까? => 당근 아니요

 

단순하게 구글링을 하게되면 아래와 같이 해결법이 나온다.

 

- Node.js 메모리 부족 어떻게 해결함?

=> --max-old-space-size 고고

 

 

참고로 각각 영역의 사이즈를 늘려주는 옵션이 있습니다.

 

--max-old-space-size 는 Old Generation(Old Space) 영역

--max-semi-space-size * 3 는 Young Generation(New Space) 영역

 

 

 

계속 살아남은 객체는 Old space로,

계속 힙메모리를 차지하면 OOM((Out of Memory) 시스템이 이상 메모리를 할당할 없을 발생하는 오류)

 

 

 

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

 

1. 전역변수

 

2. 해제되지 않은 타이버(clearTimeout 등)

 

3. 클로저 - 실행컨텍스트 안에서 어떤 변수 혹은 어떤 객체를 참조하고 있을지 몰라 힙메모리 할당이 많이 필요할 수도!

 

 

고로 메모리를 늘리는 것이 근본적인 해결법이 될 순 없다!

 


메모리 누수의 범인을 디버깅

 

간단한 옵션으로 디버깅하기

 

node --inspect index.js

 

 

chrome에 inspect 옵션을 활용하는 방법도 있다.

 

 


Reference

https://www.youtube.com/watch?v=P3C7fzMqIYg&list=LL&index=12&t=343s

 

 

Comments