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.