Node.js-최대 호출 스택 크기 초과


80

내 코드를 실행할 때 Node.js는 "RangeError: Maximum call stack size exceeded"너무 많은 재귀 호출로 인해 예외를 발생시킵니다. Node.js 스택 크기를으로 늘리려 고 sudo node --stack-size=16000 app했지만 Node.js가 오류 메시지없이 충돌합니다. sudo없이 이것을 다시 실행하면 Node.js는 'Segmentation fault: 11'. 재귀 호출을 제거하지 않고이 문제를 해결할 수 있습니까?


3
애초에 왜 그렇게 깊은 재귀가 필요합니까?
Dan Abramov 2014 년

1
코드를 게시 할 수 있습니까? Segmentation fault: 11일반적으로 노드의 버그를 의미합니다.
vkurchatkin 2014 년

1
@Dan Abramov : 왜 깊은 재귀? 배열 또는 목록을 반복하고 각각에 대해 비동기 작업을 수행하려는 경우 (예 : 일부 데이터베이스 작업) 문제가 될 수 있습니다. 비동기 작업의 콜백을 사용하여 다음 항목으로 이동하면 목록의 각 항목에 대해 하나 이상의 추가 재귀 수준이 있습니다. 아래의 heinob에서 제공하는 안티 패턴은 스택이 날아가는 것을 방지합니다.
Philip Callender

1
@PhilipCallender 설명해 주셔서 감사합니다.
Dan Abramov

@DanAbramov 크래시하기 위해 깊을 필요도 없습니다. V8은 스택에 할당 된 항목을 정리할 기회를 얻지 못합니다. 실행이 중지 된 지 오래 된 이전에 호출 된 함수는 더 이상 참조되지 않지만 여전히 메모리에 유지되는 변수를 스택에 생성했을 수 있습니다. 동기식으로 시간이 많이 걸리는 작업을 수행하고 스택에 변수를 할당하는 동안에도 동일한 오류가 발생하여 충돌이 발생합니다. 나는 9의 호출 스택의 깊이에 충돌 내 동기 JSON 파서를 가지고 kikobeats.com/synchronously-asynchronous
FeignMan

답변:


114

재귀 함수 호출을

  • setTimeout,
  • setImmediate 또는
  • process.nextTick

node.js에게 스택을 지울 수있는 기회를 제공하는 함수입니다. 당신은 그렇게하지 않고 거기에 많은 루프는없는 경우 실제 비동기 함수 호출이나 콜백을 기다리는하지 않는 경우, 당신이 RangeError: Maximum call stack size exceeded될 것입니다 피할 수 .

"잠재적 비동기 루프"에 관한 많은 기사가 있습니다. 여기 하나가 있습니다.

이제 더 많은 예제 코드 :

// ANTI-PATTERN
// THIS WILL CRASH

var condition = false, // potential means "maybe never"
    max = 1000000;

function potAsyncLoop( i, resume ) {
    if( i < max ) {
        if( condition ) { 
            someAsyncFunc( function( err, result ) { 
                potAsyncLoop( i+1, callback );
            });
        } else {
            // this will crash after some rounds with
            // "stack exceed", because control is never given back
            // to the browser 
            // -> no GC and browser "dead" ... "VERY BAD"
            potAsyncLoop( i+1, resume ); 
        }
    } else {
        resume();
    }
}
potAsyncLoop( 0, function() {
    // code after the loop
    ...
});

이것은 맞습니다 :

var condition = false, // potential means "maybe never"
    max = 1000000;

function potAsyncLoop( i, resume ) {
    if( i < max ) {
        if( condition ) { 
            someAsyncFunc( function( err, result ) { 
                potAsyncLoop( i+1, callback );
            });
        } else {
            // Now the browser gets the chance to clear the stack
            // after every round by getting the control back.
            // Afterwards the loop continues
            setTimeout( function() {
                potAsyncLoop( i+1, resume ); 
            }, 0 );
        }
    } else {
        resume();
    }
}
potAsyncLoop( 0, function() {
    // code after the loop
    ...
});

이제 라운드 당 약간의 시간 (브라우저 왕복 1 회)이 느슨해 지므로 루프가 너무 느려질 수 있습니다. 하지만 setTimeout매 라운드마다 콜할 필요는 없습니다 . 일반적으로 1,000 회마다 수행하는 것이 좋습니다. 그러나 이것은 스택 크기에 따라 다를 수 있습니다.

var condition = false, // potential means "maybe never"
    max = 1000000;

function potAsyncLoop( i, resume ) {
    if( i < max ) {
        if( condition ) { 
            someAsyncFunc( function( err, result ) { 
                potAsyncLoop( i+1, callback );
            });
        } else {
            if( i % 1000 === 0 ) {
                setTimeout( function() {
                    potAsyncLoop( i+1, resume ); 
                }, 0 );
            } else {
                potAsyncLoop( i+1, resume ); 
            }
        }
    } else {
        resume();
    }
}
potAsyncLoop( 0, function() {
    // code after the loop
    ...
});

6
답변에 좋은 점과 나쁜 점이 있습니다. setTimeout () 등을 언급 한 것이 정말 마음에 들었습니다. 그러나 setTimeout (fn, 1)을 사용할 필요는 없습니다. setTimeout (fn, 0)은 완벽하게 괜찮 기 때문입니다 (따라서 우리는 % 1000 해킹마다 setTimeout (fn, 1)이 필요하지 않습니다). JavaScript VM이 스택을 지우고 즉시 실행을 재개 할 수 있습니다. node.js에서 process.nextTick ()은 콜백을 재개하기 전에 node.js가 다른 작업 (I / O IIRC)을 수행 할 수 있기 때문에 약간 더 좋습니다.
joonas.fi

2
이 경우 setTimeout 대신 setImmediate를 사용하는 것이 좋습니다.
BaNz 2014 년

4
@ joonas.fi : % 1000의 "hack"이 필요합니다. 모든 루프 에서 setImmediate / setTimeout (0이 있더라도)을 수행하는 것은 상당히 느립니다.
heinob

3
코드 내 독일어 주석을 영어 번역으로 업데이트하십시오 ...? :) 이해하지만 다른 사람들은 그렇게 운이 좋지 않을 수 있습니다.
Robert Rossmann 2015 년


30

더러운 해결책을 찾았습니다.

/bin/bash -c "ulimit -s 65500; exec /usr/local/bin/node --stack-size=65500 /path/to/app.js"

호출 스택 제한을 늘립니다. 프로덕션 코드에는 적합하지 않다고 생각하지만 한 번만 실행되는 스크립트에는 필요했습니다.


멋진 속임수이지만 개인적으로 실수를 피하고 더 둥근 솔루션을 만들기 위해 올바른 방법을 사용하는 것이 좋습니다.
디코더 7283

저에게 이것은 차단 해제 솔루션이었습니다. 데이터베이스의 타사 업그레이드 스크립트를 실행하고 범위 오류가 발생하는 시나리오가있었습니다. 타사 패키지를 다시 작성하지 않고 데이터베이스를 업그레이드해야했습니다 →이 문제가 해결되었습니다.
Tim Kock

7

일부 언어에서는 재귀 호출이 내부적으로 루프로 변환되어 최대 스택 크기에 도달 한 오류가없는 테일 호출 최적화로 해결할 수 있습니다.

그러나 자바 스크립트에서는 현재 엔진이이를 지원하지 않으며 Ecmascript 6 언어의 새 버전이 예상됩니다 .

Node.js에는 ES6 기능을 활성화하는 몇 가지 플래그가 있지만 테일 호출은 아직 사용할 수 없습니다.

따라서 코드를 리팩터링하여 trampolining 이라는 기술을 구현 하거나 재귀를 루프로 변환 하기 위해 리팩터링 할 수 있습니다. .


감사합니다. 내 재귀 호출이 값을 반환하지 않으므로 함수를 호출하고 결과를 기다리지 않는 방법이 있습니까?
user1518183

그리고 함수가 배열과 같은 일부 데이터를 변경합니까? 함수는 무엇이며 입력 / 출력은 무엇입니까?
Angular University

5

나는 이것과 비슷한 문제가 있었다. 한 행에 여러 개의 Array.map ()을 사용하는 데 문제가 있었으며 (한 번에 약 8 개의 맵) maximum_call_stack_exceeded 오류가 발생했습니다. 맵을 'for'루프로 변경하여이 문제를 해결했습니다.

따라서 맵 호출을 많이 사용하는 경우 for 루프로 변경하면 문제가 해결 될 수 있습니다.

편집하다

명확성을 위해 필요하지 않지만 알아두면 좋은 정보를 제공하기 위해 using을 사용 .map()하면 배열이 준비되고 (getters 확인 등) 콜백이 캐시되고 내부적으로 배열의 인덱스가 유지됩니다 ( 따라서 콜백에 올바른 인덱스 / 값이 제공됩니다. 이는 각 중첩 된 호출과 함께 스택되며 중첩되지 않은 경우에도주의 .map()해야합니다. 첫 번째 배열이 가비지 수집되기 전에 다음 이 호출 될 수 있기 때문입니다 (아마도).

이 예를 보자 :

var cb = *some callback function*
var arr1 , arr2 , arr3 = [*some large data set]
arr1.map(v => {
    *do something
})
cb(arr1)
arr2.map(v => {
    *do something // even though v is overwritten, and the first array
                  // has been passed through, it is still in memory
                  // because of the cached calls to the callback function
}) 

이것을 다음과 같이 변경하면 :

for(var|let|const v in|of arr1) {
    *do something
}
cb(arr1)
for(var|let|const v in|of arr2) {
    *do something  // Here there is not callback function to 
                   // store a reference for, and the array has 
                   // already been passed of (gone out of scope)
                   // so the garbage collector has an opportunity
                   // to remove the array if it runs low on memory
}

나는 이것이 의미가 있기를 바랍니다 (나는 단어에 가장 좋은 방법이 없습니다) 그리고 내가 겪은 머리 긁힘을 방지하는 데 도움이되기를 바랍니다.

관심이 있으시면 map과 for 루프를 비교하는 성능 테스트도 있습니다 (내 작업이 아님).

https://github.com/dg92/Performance-Analysis-JS

For 루프는 일반적으로 맵보다 좋지만 축소, 필터링 또는 찾기는 아닙니다.


몇 달 전에 당신의 답변을 읽었을 때 당신의 대답에 당신이 가진 금을 전혀 몰랐습니다. 나는 최근에 나 자신을 위해 똑같은 것을 발견했고 그것은 내가 가진 모든 것을 배우고 싶지 않게 만들었습니다. 가끔 반복기 형태로 생각하기가 어렵습니다. 이것이 도움이되기를 바랍니다 :: 루프의 일부로 promise를 포함하고 계속 진행하기 전에 응답을 기다리는 방법을 보여주는 추가 예제를 작성했습니다. 예 : gist.github.com/gngenius02/…
cigol on

나는 당신이 거기에서 한 일을 좋아합니다 (그리고 내 도구 상자를 위해 그것을 잡아도 상관 없기를 바랍니다). 주로 동기 코드를 사용하기 때문에 일반적으로 루프를 선호합니다. 그러나 그것은 당신이뿐만 아니라이있어 보석이며, 대부분의 다음 서버 내가 작업에 그것의 방법을 찾을 것
Werlious

2

사전 :

나에게 Max 호출 스택이있는 프로그램은 내 코드 때문이 아니 었습니다. 결국 응용 프로그램 흐름의 정체를 유발하는 다른 문제가되었습니다. 그래서 구성 기회없이 mongoDB에 너무 많은 항목을 추가하려고했기 때문에 호출 스택 문제가 발생했고 무슨 일이 일어나고 있는지 파악하는 데 며칠이 걸렸습니다 ....


@Jeff Lowery가 대답 한 내용에 대한 후속 조치 :이 대답이 너무 즐거웠고 내가하고있는 작업의 프로세스 속도를 최소한 10 배나 빨랐습니다.

나는 프로그래밍에 익숙하지 않지만 대답을 모듈화하려고 시도했습니다. 또한 오류가 발생하는 것을 좋아하지 않았으므로 대신 do while 루프로 래핑했습니다. 내가 한 일이 잘못된 경우 언제든지 수정하십시오.

module.exports = function(object) {
    const { max = 1000000000n, fn } = object;
    let counter = 0;
    let running = true;
    Error.stackTraceLimit = 100;
    const A = (fn) => {
        fn();
        flipper = B;
    };
    const B = (fn) => {
        fn();
        flipper = A;
    };
    let flipper = B;
    const then = process.hrtime.bigint();
    do {
        counter++;
        if (counter > max) {
            const now = process.hrtime.bigint();
            const nanos = now - then;
            console.log({ 'runtime(sec)': Number(nanos) / 1000000000.0 });
            running = false;
        }
        flipper(fn);
        continue;
    } while (running);
};

이 요점을 확인하여 내 파일과 루프를 호출하는 방법을 확인하십시오. https://gist.github.com/gngenius02/3c842e5f46d151f730b012037ecd596c



1

setTimeout() (Node.js, v10.16.0) 을 사용하지 않고 호출 스택 크기를 제한하는 함수 참조를 사용하는 다른 접근 방식을 생각했습니다 .

testLoop.js

let counter = 0;
const max = 1000000000n  // 'n' signifies BigInteger
Error.stackTraceLimit = 100;

const A = () => {
  fp = B;
}

const B = () => {
  fp = A;
}

let fp = B;

const then = process.hrtime.bigint();

for(;;) {
  counter++;
  if (counter > max) {
    const now = process.hrtime.bigint();
    const nanos = now - then;

    console.log({ "runtime(sec)": Number(nanos) / (1000000000.0) })
    throw Error('exit')
  }
  fp()
  continue;
}

산출:

$ node testLoop.js
{ 'runtime(sec)': 18.947094799 }
C:\Users\jlowe\Documents\Projects\clearStack\testLoop.js:25
    throw Error('exit')
    ^

Error: exit
    at Object.<anonymous> (C:\Users\jlowe\Documents\Projects\clearStack\testLoop.js:25:11)
    at Module._compile (internal/modules/cjs/loader.js:776:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:787:10)
    at Module.load (internal/modules/cjs/loader.js:653:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:593:12)
    at Function.Module._load (internal/modules/cjs/loader.js:585:3)
    at Function.Module.runMain (internal/modules/cjs/loader.js:829:12)
    at startup (internal/bootstrap/node.js:283:19)
    at bootstrapNodeJSCore (internal/bootstrap/node.js:622:3)

0

최대 스택 크기 증가와 관련하여 32 비트 및 64 비트 시스템에서 V8의 메모리 할당 기본값은 각각 700MB 및 1400MB입니다. 최신 버전의 V8에서 64 비트 시스템의 메모리 제한은 더 이상 V8에 의해 설정되지 않으며 이론적으로 제한이 없음을 나타냅니다. 그러나 Node가 실행중인 OS (Operating System)는 V8이 사용할 수있는 메모리 양을 항상 제한 할 수 있으므로 주어진 프로세스의 실제 제한을 일반적으로 지정할 수 없습니다.

V8에서는 --max_old_space_size옵션 을 사용할 수 있지만 프로세스에서 사용할 수있는 메모리 양을 제어 할 수 있습니다. MB 단위의 값을 허용합니다. 메모리 할당을 늘려야하는 경우 노드 프로세스를 생성 할 때이 옵션을 원하는 값으로 전달하면됩니다.

특히 많은 인스턴스를 실행할 때 특정 노드 인스턴스에 대해 사용 가능한 메모리 할당을 줄이는 것은 종종 훌륭한 전략입니다. 스택 제한과 마찬가지로 대용량 메모리 요구 사항이 인 메모리 데이터베이스 등의 전용 스토리지 계층에 더 잘 위임되는지 고려하십시오.


0

가져 오는 함수와 같은 파일에서 선언 한 함수의 이름이 같지 않은지 확인하십시오.

이 오류에 대한 예를 들어 보겠습니다. Express JS (ES6 사용)에서 다음 시나리오를 고려하십시오.

import {getAllCall} from '../../services/calls';

let getAllCall = () => {
   return getAllCall().then(res => {
      //do something here
   })
}
module.exports = {
getAllCall
}

위의 시나리오는 악명 높은 RangeError : 최대 호출 스택 크기를 초과했습니다. 오류를 발생시킵니다. 함수가 정도로 자신을 계속 호출하기 때문입니다.

대부분의 경우 오류는 위와 같은 코드에 있습니다. 해결하는 다른 방법은 수동으로 호출 스택을 늘리는 것입니다. 음, 이것은 특정 극단적 인 경우에 작동하지만 권장되지 않습니다.

내 대답이 도움이 되었기를 바랍니다.


-4

루프를 사용할 수 있습니다.

var items = {1, 2, 3}
for(var i = 0; i < items.length; i++) {
  if(i == items.length - 1) {
    res.ok(i);
  }
}

2
var items = {1, 2, 3}유효한 JS 구문이 아닙니다. 이것이 질문과 어떤 관련이 있습니까?
musemind
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.