web-dev-qa-db-ja.com

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'を出力します。再帰呼び出しを削除せずにこれを解決する可能性はありますか?

ありがとう

66
user1518183

再帰関数呼び出しをラップする必要があります

  • setTimeout
  • setImmediateまたは
  • process.nextTick

node.jsにスタックをクリアする機会を与える関数。それを行わず、real非同期関数呼び出しのないループが多い場合、またはコールバックを待たない場合は、RangeError: Maximum call stack size exceeded不可になります。

「潜在的な非同期ループ」に関する多くの記事があります。 ここに1つ

次に、さらにいくつかのサンプルコードを示します。

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

ループごとに少し時間がかかる(ブラウザーの往復)ため、ループが遅くなりすぎる可能性があります。ただし、すべてのラウンドでsetTimeoutを呼び出す必要はありません。通常はOKです1000回ごとに実行します。ただし、これはスタックサイズによって異なる場合があります。

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

私は汚い解決策を見つけました:

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

呼び出しスタックの制限を増やすだけです。これは本番コードには適していないと思いますが、一度だけ実行されるスクリプトには必要でした。

20
user1518183

一部の言語では、これは末尾呼び出しの最適化で解決できます。この場合、再帰呼び出しは内部でループに変換されるため、最大スタックサイズに到達したというエラーは発生しません。

しかし、javascriptでは、現在のエンジンはこれをサポートしていません。新しいバージョンの言語 Ecmascript 6 で予測されています。

Node.jsにはES6機能を有効にするためのフラグがいくつかありますが、テールコールはまだ利用できません。

そのため、コードをリファクタリングして トランポリン と呼ばれる手法を実装するか、リファクタリングして 再帰をループに変換 にすることができます。

5

これと同様の問題がありました。複数のArray.map()を連続して使用すると問題が発生し(一度に約8マップ)、maximum_call_stack_exceededエラーが発生していました。これを解決するために、マップを「for」ループに変更しました

したがって、多くのマップ呼び出しを使用している場合、それらをforループに変更すると問題が解決する場合があります

編集

明確にするために、おそらく必要ではないが知っておくとよい情報のために、.map()を使用すると、配列が準備され(ゲッターなどの解決)、コールバックがキャッシュされ、内部的にも配列のインデックス(コールバックには正しいインデックス/値が提供されます)。これはネストされた呼び出しごとにスタックします。ネストされていない場合も注意してください。最初の配列がガベージコレクションされる前に次の.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
}

私はこれが何らかの意味をなして(私は言葉で最善の方法を持っていない)、私が経験した頭の傷を防ぐためにいくつかを助けることを願っています

誰かが興味があるなら、マップとforループを比較するパフォーマンステストもあります(私の仕事ではありません)。

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

通常、forループはmapよりも優れていますが、reduce、filter、findはありません。

2
Werlious

独自のラッパーを実装したくない場合は、キューシステムを使用できます。 async.queuequeue

1
weakish

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
Jeff Lowery

インポートする関数と同じファイルで宣言した関数の名前が同じでないことを確認してください。

このエラーの例を示します。エクスプレスJS(ES6を使用)で、次のシナリオを検討します。

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

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

上記のシナリオでは、悪名高いRangeError:Maximum call stack size exceededエラーが発生します。これは、関数が何度も自分自身を呼び出し続け、最大呼び出しスタックを使い果たすためです。

ほとんどの場合、エラーはコード内にあります(上記のような)。他の解決方法は、コールスタックを手動で増やすことです。まあ、これは特定の極端な場合に機能しますが、推奨されません。

私の答えがお役に立てば幸いです。

0
Abhay Shiro

最大スタックサイズの増加に関して、32ビットおよび64ビットマシンでは、V8のメモリ割り当てのデフォルトは、それぞれ700 MBおよび1400 MBです。 V8の新しいバージョンでは、64ビットシステムのメモリ制限はV8によって設定されなくなり、理論的には制限がないことを示しています。ただし、Nodeが実行されているOS(オペレーティングシステム)は、V8が使用できるメモリ量を常に制限する可能性があるため、特定のプロセスの真の制限を一般的に述べることはできません。

V8では--max_old_space_sizeオプションが使用可能になります。これにより、プロセスで使用可能なメモリ量を制御でき、MB単位の値を受け入れます。メモリ割り当てを増やす必要がある場合は、Nodeプロセスを生成するときにこのオプションに必要な値を渡すだけです。

多くの場合、特に多くのインスタンスを実行している場合、特定のNodeインスタンスの使用可能なメモリ割り当てを減らすことは、多くの場合優れた戦略です。スタックの制限と同様に、大量のメモリニーズは、インメモリデータベースなどの専用ストレージレイヤーにより適切に委任されるかどうかを検討してください。

0
serkan