node.js로 만든 서버 프로세스가 간헐적으로 아래와 같은 에러메시지와 함께 죽는 경우가 발생

FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - process out of memory





원인은 V8의 heap memory 할당 한계 사이즈를 넘어서는 메모리 할당이 일어났기 때문이었다.


해결방법은 간단하다.

--max_old_space_size 옵션값을 크게 설정해 heap memory 할당 한계 사이즈를 늘여주면 된다.

// 8G로 heap memory 할당 한계 사이즈 늘여서 실행. 옵션값 단위는 MB

node --max_old_space_size=8192 index.js




--max_old_space_size 값을 지정하지 않고 그냥 실행했을 경우 기본값은 64bit 기준으로 1.4G 정도.








여기서부터 나오는 내용은 개인의 궁금증 해소를 위한 기록임.


기본 old_space_size가 궁금해서 https://github.com/nodejs/node 의 소스코드를 뒤져보고 아래와 같은 결론을 얻었다.


node/deps/v8/src/heap/heap.cc 파일을 보면 기본 사이즈 지정하는 부분이 있다.

max_old_generation_size_(700ul * (kPointerSize / 4) * MB),



계산에 필요한 kPointerSize와 MB는 node/deps/v8/src/globals.h 에 정의되어 있다.

// 64bit에서 아래 값은 8 bytes
const int kPointerSize   = sizeof(void*);
 
...
...
  
const int KB = 1024;
// MB 값은 1024 x 1024
const int MB = KB * KB;


계산해보면 대략 1.4G

700 x (8 / 4) x 1024 x 1024 = 1,460,006,400 = 1,400 MB





그리고 --max_old_space_size 값에 따라, 정말 죽는 시점이 달라지는지 실험을 해봤음.

heap memory 할당량을 뻥튀기 해야되는데, 단순히 object 키 값을 늘이거나 array.push를 많이 할 경우,

memory allocation 에러가 아니라 hash table 혹은 array max length 관련 에러로 먼저 죽어버리므로 코드를 잘 짜야함.


--max_old_space_size 값을 128, 256, 512, 1024, 2048, 4096, 8192로 바꿔가면서 top을 통한 RES 사이즈 체크와 동시에 파일에 process.memoryUsage의 반환값 중 rss, heapTotal, heapUsed 값을 기록하게 했다.

그러므로 파일에 기록된 값은 죽기 직전의 값이다.



 --max_old_space_size 값 (MB)

 rss (MB)

 heapTotal (MB) 

 heapUsed (MB) 

 128

 176 

 164 

 145 

 256 

 305 

 292 

 273 

 512 

 570 

 553 

 534 

 1024 

 1088 

 1061 

 1043 

 no option 

 1462 

 1430 

 1405 

 2048 

 2138 

 2097 

 2066 

 4096 

 4199 

 30 

 6 

 8192 

 8474 

 91 

 67 



rss 값을 보면 거의 설정한 수치 근처에서 죽는걸 확인할 수 있다.


근데 보다보면 굉장히 이상한 수치가 있는데,

--max_old_space_size 값을 4096, 8192로 설정했을 경우 heapTotal, heapUsed 값이 말도 안되게 작은 수치가 나오는 걸 볼 수 있다.


원인을 알아보기 위해 메모리를 할당하는 루프 구간에서 heapTotal이 드라마틱하게 줄어드는 시점의 직전 heapTotal 값을 출력하도록 코드를 추가하고 동일한 실험을 수행해서 아래와 같은 결과를 얻었다.


(단위는 byte)

--max_old_space_size를 4096으로 했을 때 heapTotal 값 4292467712 이후 사이즈가 확 줄어들었고, (1회 발생)

8192로 했을 때는 heapTotal 값이 4294861312 이후 사이즈가 확 줄었다가 서서히 증가하다가, 다시 4294345472 이후 사이즈가 확 줄어들었다. (2회 발생)


궁금해서 대충 뒤져보니 heapTotal, heapUsed 값을 반환하는 함수의 반환값 데이터 타입이 size_t라서 저런 이상한 값이 나오는 것 같다.

node/deps/v8/include/v8.h 에 보면 HeapStatistics 라는 class가 하나 있는데 total_heap_size()를 비롯하여 모든 method의 반환값 형태가 size_t 이다.


/**
 * Collection of V8 heap information.
 *
 * Instances of this class can be passed to v8::V8::HeapStatistics to
 * get heap statistics from V8.
 */
class V8_EXPORT HeapStatistics {
 public:
  HeapStatistics();
  size_t total_heap_size() { return total_heap_size_; }
  size_t total_heap_size_executable() { return total_heap_size_executable_; }
  size_t total_physical_size() { return total_physical_size_; }
  size_t total_available_size() { return total_available_size_; }
  size_t used_heap_size() { return used_heap_size_; }
  size_t heap_size_limit() { return heap_size_limit_; }
  size_t malloced_memory() { return malloced_memory_; }
  size_t peak_malloced_memory() { return peak_malloced_memory_; }
  size_t does_zap_garbage() { return does_zap_garbage_; }
 
 private:
  size_t total_heap_size_;
  size_t total_heap_size_executable_;
  size_t total_physical_size_;
  size_t total_available_size_;
  size_t used_heap_size_;
  size_t heap_size_limit_;
  size_t malloced_memory_;
  size_t peak_malloced_memory_;
  bool does_zap_garbage_;
 
  friend class V8;
  friend class Isolate;
};



size_t가 unsigned integer라면 최대 수치는 4294967295 (0xffffffff) 이다.

위에서 출력된 3개의 값이 모두 4294967295 보다 작은 값이지만 근사한 값인 것으로 미루어 볼 때, 

저 값을 넘어서면서 사이즈 값이 한바퀴 돌아서 처음부터 다시 올라가는 것이 아닐까 추측된다.

소스코드를 하나씩 따라가면서 확인하거나 컴파일해보고 내린 결론이 아니라 소스코드에서 대충 그럴듯한 내용을 검색해서 추측한 내용이라 정확한 원인인지는 모르겠지만.




.

Posted by bloodguy
,