로컬환경에서 개발을 할 경우 별 생각없이 도커 환경에서 작업을 많이들 합니다.
도커와 노드를 더 이해하기 위해 이 글을 작성합니다.
도커 컨테이너 내에 노드를 실행하는 경우 전형적인 메모리 튜닝은 기대처럼 항상 동작하지는 않을 것 입니다.
그 이유를 알아보고 컨테이너 환경에서의 노드실행의 best practices와 권고사항을 알아 보겠습니다.
노드를 컨테이너에서 실행 할 때
--max-old-space-size
를 사용 하는 경우 컨테이너의 limit 보다 작게 설정해야 합니다.
기본적으로 컨테이너는 자원에 제한이 없고 호스트 OS가 허락하는 한도 내의 메모리 자원을 사용 할 수 있습니다.
다음 명령어로 도커는 메모리 또는 cpu 한도를 설정 할 수 있습니다.
docker run --memory <x><y> --interactive --tty <imagename> bash
docker run --memory 1000000b --interactive --tty <imagename> bash
컨테이너 내에서 다음 명령어로 메모리 한도를 확인 할 수 있습니다.
cat /sys/fs/cgroup/memory/memory.limit_in_bytes
node의 --max_old_space_size
플래그를 사용하여 컨테이너를 살펴볼 수 있습니다.
Old space
란 Node의 v8엔진에 의해 관리되는 heap으로 --max_old_space_size
를 사용하여 최대 heap 사이즈를 제어 할 수 있습니다.
보통 컨테이너 보다 더 큰 메모리를 Node가 사용하게 된다면 Node는 강제로 종료가 됩니다.
다음 코드는 0.1초 마다 record
를 push 하여 heap을 성장 시키게 합니다.
'use strict';
const list = [];
setInterval(()=> {
const record = new MyRecord();
list.push(record);
},10);
function MyRecord() {
var x='hii';
this.name = x.repeat(10000000);
this.id = x.repeat(10000000);
this.account = x.repeat(10000000);
}
setInterval(()=> {
console.log(process.memoryUsage())
},100);
docker run --memory 512m --interactive --tty node-image bash
node --max_old_space_size=1024 test-fatal-error.js
//위 명령어는 node-image에 명시해야 합니다.
기본으로는 Node v11의 경우 32bit에서는 700mb 64bit에서는 1400mb의 최대 heap 사이즈를 갖습니다.
최신 버전에서는 1.7gb를 갖는걸로 알고있습니다.
노드가 컨테이너 메모리 limit보다 2배를 설정 했기 때문에 OOM에 의해 예상한 대로라면 종료가 되어야 합니다.
--max-old-space-size
로 최대 heap을 설정 하였다 하더라도 처음부터 최대로 설정은 되지 않습니다.
대신 늘어나는 수요에 맞춰 점차적으로 heap을 키워나갑니다.
최대 heap에 도달하더라도 Node가 종료가 되지 않았습니다.
컨테이너가 호스트된 애플리케이션에서 추적하는 중요한 것은 RSS입니다.
RSS
란 애플리케이션의 가상 메모리의 일부를 나타냅니다.
게다가 애플리케이션에 할당된 메모리
의 일부도 나타내고 현재 활성중인 애플리케이션의 할당된 메모리
의 일부로 표현됩니다.
애플리케이션 내에서 할당된 모든 메모리가 활성 상태인 것은 아닙니다. 할당된 메모리
는 프로세스가 실제로
그것을 사용하기 시작할 때까지 할당되지 않기 때문입니다.
다음 코드는 버퍼를 메모리로 할당한 것 처럼 보입니다.
const buf = Buffer.alloc(+process.argv[2] * 1024 * 1024)
console.log(Math.round(buf.length / (1024 * 1024)))
console.log(Math.round(process.memoryUsage().rss / (1024 * 1024)))
하지만 결과는 종료되지 않습니다.
$ node buffer_example 2000
2000
16
그 이유는 메모리가 끝까지 차지 않았기 때문입니다.
rss 값이 매우 낮아 컨테이너 메모리 limit을 넘지 못했기 때문입니다.
// 'x'가 데이터
const buf = Buffer.alloc(+process.argv[2] * 1024 * 1024, 'x')
console.log(Math.round(buf.length / (1024 * 1024)))
console.log(Math.round(process.memoryUsage().rss / (1024 * 1024)))
$ node buffer_example_fill.js 2000
2000
984
하지만 이래도 종료가 되지 않네요 왜 그럴가요?
도커는 기본적으로 --momory
플래그로 설정된 메모리 만큼의 스왑 메모리를 갖기 때문입니다.
즉, 위의 1gb의 메모리 설정으로 인해 1gb의 스왑 메모리
가 추가로 생겨, 총 2gb의 메모리를 갖게 되기 때문입니다.
예제2의 js 코드를 그대로 사용하여 다음 도커 명령어를 사용합니다.
docker run --memory 1024m --memory-swap=1024m --memory-swappiness=0 --interactive --tty ravali1906/dockermemory bash
$ node buffer_example_fill.js 2000
Killed
결과는 원하는 대로 Node가 종료가 되었습니다.
--memory-swap
의 값이 --memory
값과 같으면 추가적인 스왑공간을 컨테이너가 사용하지 못하게 됩니다.
또한 기본적으로 호스트 커널은 컨테이너에서 사용하는 익명 페이지의 일정 비율을 교체할 수 있으므로 값 0을 --memory-swappiness
플래그에 전달하여 비활성화합니다.
따라서 컨테이너 내부에서 스와핑이 발생하지 않기 때문에 rss가 컨테이너 제한을 초과하여 프로세스가 종료됩니다.
Node의 --max-old-space-size
가 컨테이너 limit보다 높아도 종료가 되지않는 것을 보면 컨테이너를 무시하는게 아닌 JS의 heap이 최대에 도달하지 않았기 때문인 것을 알 수 있었습니다.
https://developer.ibm.com/articles/nodejs-memory-management-in-container-environments/