Node.js-最大コールスタックサイズを超えました


80

コードを実行すると、Node.jsは"RangeError: Maximum call stack size exceeded"再帰呼び出しが多すぎるために例外をスローします。Node.jsスタックサイズを増やしようとしましたが、Node.jssudo node --stack-size=16000 appがエラーメッセージなしでクラッシュします。sudoを使用せずにこれを再度実行すると、Node.jsはを出力し'Segmentation fault: 11'ます。再帰呼び出しを削除せずにこれを解決する可能性はありますか?


3
そもそもなぜこんなに深い再帰が必要なのですか?
ダンアブラモフ2014年

1
コードを投稿していただけますか?Segmentation fault: 11通常、ノードのバグを意味します。
vkurchatkin 2014年

1
@Dan Abramov:なぜ深い再帰なのですか?これは、配列またはリストを反復処理して、それぞれに対して非同期操作(データベース操作など)を実行する場合に問題になる可能性があります。非同期操作からのコールバックを使用して次のアイテムに移動する場合、リスト内のアイテムごとに少なくとも1つの追加レベルの再帰があります。以下のheinobによって提供されるアンチパターンは、スタックが吹き飛ばされるのを防ぎます。
フィリップカレンダー2014

1
@PhilipCallender説明してくれてありがとう、あなたが非同期のことをしていることに気づかなかった!
ダン・アブラモフ2014

@DanAbramovクラッシュするために深くする必要はありません。V8は、スタックに割り当てられたものを一掃する機会を得ません。実行を停止してから長い間、以前に呼び出された関数が、スタック上に変数を作成し、参照されなくなったが、メモリに保持されている可能性があります。集中的に時間のかかる操作を同期的に実行し、スタックに変数を割り当てている場合でも、同じエラーでクラッシュします。同期JSONパーサーがコールスタックの
asynchronous

回答:


114

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

  • setTimeout
  • setImmediate または
  • process.nextTick

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

「PotentialAsyncLoop」に関する記事はたくさんあります。これが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
    ...
});

これで、ラウンドごとに少しの時間(1つのブラウザーラウンドトリップ)が失われるため、ループが遅くなりすぎる可能性があります。ただしsetTimeout、すべてのラウンドで電話をかける必要はありません。通常、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
    ...
});

6
あなたの答えにはいくつかの良い点と悪い点がありました。setTimeout()などについておっしゃっていたのが本当に気に入りました。ただし、setTimeout(fn、0)は完全に問題ないため、setTimeout(fn、1)を使用する必要はありません(したがって、%1000ハックごとにsetTimeout(fn、1)は必要ありません)。これにより、JavaScript VMがスタックをクリアし、すぐに実行を再開できます。node.jsでは、process.nextTick()が少し優れています。これにより、node.jsは、コールバックを再開する前に、他の処理(I / O IIRC)を実行できるようになります。
joonas.fi 2014年

2
このような場合は、setTimeoutの代わりにsetImmediateを使用する方がよいと思います。
BaNz 2014年

4
@ joonas.fi:%1000での「ハック」が必要です。すべてのループでsetImmediate / setTimeout(0でも)を実行すると、劇的に遅くなります。
heinob 2014年

3
コード内のドイツ語のコメントを英語に翻訳して更新してください...?:)わかりましたが、他の人はそれほど幸運ではないかもしれません。
ロバートロスマン2015年


30

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

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

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


個人的には、間違いを避け、より丸みのあるソリューションを作成するために正しい方法を使用することをお勧めしますが、クールなトリックです。
デコーダー7283 2018年

私にとって、これはブロックを解除するソリューションでした。データベースのサードパーティのアップグレードスクリプトを実行していて、範囲エラーが発生するシナリオがありました。サードパーティのパッケージを書き直すつもりはありませんでしたが、データベースをアップグレードする必要がありました→これで修正されました。
TimKock20年

7

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

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

Node.jsには、ES6機能を有効にするためのいくつかのフラグがありますが、末尾呼び出しはまだ利用できません。

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


ありがとうございました。再帰呼び出しが値を返さないので、関数を呼び出して結果を待たない方法はありますか?
user1518183 2014年

そして、それが配列のようないくつかのデータを変更する機能、それは機能を何をするのか、入力/出力は何ですか?
Angular University

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ループはマップよりも優れていますが、削減、フィルタリング、または検索はできません。


数ヶ月前にあなたの回答を読んだとき、私はあなたがあなたの回答に持っている金を知りませんでした。私は最近、これとまったく同じことを自分自身で発見しました。それは、私が持っているすべてのものを学びたくなくなりました。イテレータの形で考えるのは難しい場合があります。これがお役に立てば幸いです::ループの一部としてpromiseを含み、次に進む前に応答を待つ方法を示す追加の例を作成しました。例: gist.github.com/gngenius02/...
上cigol

私はあなたがそこでしたことを愛しています(そして私が私のツールボックスのためにそれを手に入れても気にしないことを願っています)。私は主に同期コードを使用します。そのため、私は通常ループを好みます。しかし、それはあなたがそこに
たどり着い

2

事前:

私にとって、Maxコールスタックを備えたプログラムは、私のコードが原因ではありませんでした。それは、アプリケーションのフローの混雑を引き起こした別の問題であることになりました。構成の可能性なしにmongoDBにあまりにも多くのアイテムを追加しようとしたため、コールスタックの問題が発生し、何が起こっているのかを理解するのに数日かかりました。


@Jeff Loweryが答えたもののフォローアップ:私はこの答えをとても楽しんだし、それは私がやっていたことのプロセスを少なくとも10倍スピードアップした。

私はプログラミングに不慣れですが、答えをモジュール化しようとしました。また、エラーがスローされるのが気に入らなかったので、代わりにdowhileループでラップしました。私がしたことが間違っている場合は、遠慮なく訂正してください。

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によって設定されなくなり、理論的には制限がないことを示しています。ただし、ノードが実行されているOS(オペレーティングシステム)は、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

ループforを使用できます。

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.