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

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



답변

재귀 함수 호출을

  • 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
    ...
});


답변

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

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

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


답변

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

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

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

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


답변

나는 이것과 비슷한 문제가 있었다. 한 행에 여러 개의 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 루프는 일반적으로 맵보다 좋지만 축소, 필터링 또는 찾기는 아닙니다.


답변

사전 :

나에게 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


답변

자체 래퍼를 구현하지 않으려면 async.queue , queue 와 같은 대기열 시스템을 사용할 수 있습니다 .


답변

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)