Node.js - Maximale Aufrufstapelgröße überschritten

80

Wenn ich meinen Code ausführe, löst Node.js eine "RangeError: Maximum call stack size exceeded"Ausnahme aus, die durch zu viele rekursive Aufrufe verursacht wird. Ich habe versucht, die Stapelgröße von sudo node --stack-size=16000 appNode.j um zu erhöhen , aber Node.js stürzt ohne Fehlermeldung ab. Wenn ich dies erneut ohne sudo ausführe, wird Node.js gedruckt 'Segmentation fault: 11'. Gibt es eine Möglichkeit, dies zu lösen, ohne meine rekursiven Aufrufe zu entfernen?

user1518183
quelle
3
Warum brauchen Sie überhaupt eine so tiefe Rekursion?
Dan Abramov
Können Sie bitte einen Code posten? Segmentation fault: 11bedeutet normalerweise einen Fehler im Knoten.
vkurchatkin
1
@ Dan Abramov: Warum tiefe Rekursion? Dies kann ein Problem sein, wenn Sie ein Array oder eine Liste durchlaufen und für jedes Array eine asynchrone Operation ausführen möchten (z. B. eine Datenbankoperation). Wenn Sie den Rückruf von der asynchronen Operation verwenden, um zum nächsten Element überzugehen, gibt es für jedes Element in der Liste mindestens eine zusätzliche Rekursionsstufe. Das von heinob unten bereitgestellte Anti-Muster verhindert das Ausblasen des Stapels.
Philip Callender
1
@PhilipCallender Ich wusste nicht, dass du asynchrone Sachen machst, danke für die Klarstellung!
Dan Abramov
@DanAbramov Muss auch nicht tief sein, um abzustürzen. V8 hat keine Chance, auf dem Stapel zugewiesene Inhalte zu bereinigen. Zuvor aufgerufene Funktionen, deren Ausführung längst eingestellt wurde, haben möglicherweise Variablen auf dem Stapel erstellt, auf die nicht mehr verwiesen wird, die jedoch noch im Speicher gespeichert sind. Wenn Sie einen intensiven zeitaufwändigen Vorgang synchron ausführen und Variablen auf dem Stapel zuweisen, während Sie gerade dabei sind, stürzen Sie immer noch mit demselben Fehler ab. Ich habe meinen synchronen JSON-Parser bei einer Callstack-Tiefe von 9 zum Absturz gebracht. Kikobeats.com/synchronously-asynchronous
FeignMan

Antworten:

113

Sie sollten Ihren rekursiven Funktionsaufruf in a einschließen

  • setTimeout,
  • setImmediate oder
  • process.nextTick

Funktion, um node.js die Möglichkeit zu geben, den Stapel zu löschen. Wenn Sie dies nicht tun und es viele Schleifen ohne echten asynchronen Funktionsaufruf gibt oder wenn Sie nicht auf den Rückruf warten, RangeError: Maximum call stack size exceededist dies unvermeidlich .

Es gibt viele Artikel zum Thema "Potential Async Loop". Hier ist einer .

Nun noch ein Beispielcode:

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

Dies ist richtig:

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

Jetzt wird Ihre Schleife möglicherweise zu langsam, da wir pro Runde etwas Zeit verlieren (ein Browser-Roundtrip). Sie müssen aber nicht setTimeoutin jeder Runde anrufen . Normalerweise ist es in Ordnung, dies alle 1000. Mal zu tun. Dies kann jedoch je nach Stapelgröße unterschiedlich sein:

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
    ...
});
heinob
quelle
6
Ihre Antwort enthielt einige gute und schlechte Punkte. Es hat mir sehr gut gefallen, dass Sie setTimeout () et al. SetTimeout (fn, 1) muss jedoch nicht verwendet werden, da setTimeout (fn, 0) vollkommen in Ordnung ist (daher benötigen wir setTimeout (fn, 1) nicht alle% 1000 Hack). Dadurch kann die JavaScript-VM den Stapel löschen und die Ausführung sofort fortsetzen. In node.js ist process.nextTick () etwas besser, da node.js auch andere Aufgaben (I / O IIRC) ausführen kann, bevor Ihr Rückruf fortgesetzt wird.
joonas.fi
2
Ich würde sagen, dass es in diesen Fällen besser ist, setImmediate anstelle von setTimeout zu verwenden.
BaNz
4
@ joonas.fi: Mein "Hack" mit% 1000 ist notwendig. Das Ausführen eines setImmediate / setTimeout (auch mit 0) in jeder Schleife ist erheblich langsamer.
Heinob
3
Möchten Sie Ihre deutschen In-Code-Kommentare mit englischer Übersetzung aktualisieren ...? :) Ich verstehe, aber andere haben möglicherweise nicht so viel Glück.
Robert Rossmann
Vielen Dank
Angelos Kyriakopoulos
29

Ich habe eine schmutzige Lösung gefunden:

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

Es erhöht nur das Call-Stack-Limit. Ich denke, dass dies nicht für Produktionscode geeignet ist, aber ich brauchte es für Skripte, die nur einmal ausgeführt werden.

user1518183
quelle
Cooler Trick, obwohl ich persönlich vorschlagen würde, korrekte Praktiken anzuwenden, um Fehler zu vermeiden und eine rundere Lösung zu schaffen.
decoder7283
Für mich war dies eine Lösung zum Entsperren. Ich hatte ein Szenario, in dem ich ein Upgrade-Skript eines Drittanbieters für eine Datenbank ausführte und den Bereichsfehler erhielt. Ich wollte das Paket eines Drittanbieters nicht neu schreiben, musste aber die Datenbank aktualisieren → dies hat das Problem behoben.
Tim Kock
7

In einigen Sprachen kann dies mit der Tail-Call-Optimierung gelöst werden, bei der der Rekursionsaufruf unter der Haube in eine Schleife umgewandelt wird, sodass kein Fehler beim Erreichen der maximalen Stapelgröße vorliegt.

In Javascript unterstützen die aktuellen Engines dies jedoch nicht. Dies ist für eine neue Version der Sprache Ecmascript 6 vorgesehen .

Node.js verfügt über einige Flags, um ES6-Funktionen zu aktivieren, aber ein Tail Call ist noch nicht verfügbar.

Sie können also Ihren Code umgestalten, um eine Technik namens Trampolin zu implementieren , oder umgestalten, um die Rekursion in eine Schleife umzuwandeln .

Angular University
quelle
Vielen Dank. Mein Rekursionsaufruf gibt keinen Wert zurück. Gibt es also eine Möglichkeit, die Funktion aufzurufen und nicht auf das Ergebnis zu warten?
user1518183
Und ändert die Funktion einige Daten, wie ein Array, was bewirkt sie, was sind die Ein- / Ausgänge?
Angular University
5

Ich hatte ein ähnliches Problem wie dieses. Ich hatte ein Problem mit der Verwendung mehrerer Array.map () in einer Reihe (ungefähr 8 Karten gleichzeitig) und bekam den Fehler "maximum_call_stack_exceeded". Ich habe dieses Problem gelöst, indem ich die Karte in 'for'-Schleifen geändert habe

Wenn Sie also viele Kartenaufrufe verwenden, kann das Problem durch Ändern in for-Schleifen behoben werden

Bearbeiten

Nur aus Gründen der Klarheit und wahrscheinlich nicht benötigten, aber gut zu informierenden Informationen .map()bewirkt die Verwendung, dass das Array vorbereitet wird (Auflösen von Gettern usw.) und der Rückruf zwischengespeichert wird, und dass auch intern ein Index des Arrays gespeichert wird ( Daher wird der Rückruf mit dem richtigen Index / Wert versehen. Dies wird bei jedem verschachtelten Aufruf gestapelt, und Vorsicht ist geboten, wenn es nicht ebenfalls verschachtelt ist, da das nächste .map()aufgerufen werden kann, bevor das erste Array (wenn überhaupt) mit Müll gesammelt wird.

Nehmen Sie dieses Beispiel:

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
}) 

Wenn wir dies ändern in:

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
}

Ich hoffe, das macht Sinn (ich kann nicht gut mit Worten umgehen) und hilft einigen, das Kratzen des Kopfes zu verhindern, das ich durchgemacht habe

Wenn jemand interessiert ist, ist hier auch ein Leistungstest zum Vergleichen von Karte und für Schleifen (nicht meine Arbeit).

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

For-Schleifen sind normalerweise besser als Map, aber nicht reduzieren, filtern oder finden

Werlious
quelle
Als ich vor ein paar Monaten Ihre Antwort las, hatte ich keine Ahnung, welches Gold Sie in Ihrer Antwort hatten. Ich habe kürzlich genau dasselbe für mich entdeckt und es hat mich wirklich dazu gebracht, alles zu verlernen, was ich habe. Manchmal ist es nur schwer, in Form von Iteratoren zu denken. Hoffe das hilft :: Ich habe ein zusätzliches Beispiel geschrieben, das Versprechen als Teil der Schleife enthält und zeigt, wie man auf die Antwort wartet, bevor man weitermacht. Beispiel: gist.github.com/gngenius02/…
cigol am
Ich liebe, was du dort getan hast (und hoffe, es macht dir nichts aus, wenn ich mir das schnippte, das ich für meinen Werkzeugkasten geschnitten habe). Ich benutze meistens synchronen Code, weshalb ich normalerweise Schleifen bevorzuge. Aber das ist auch ein Juwel, das Sie dort gefunden haben und das höchstwahrscheinlich den Weg auf den nächsten Server finden wird, an dem ich arbeite
Werlious
2

Vor:

Für mich lag das Programm mit dem Max-Aufrufstapel nicht an meinem Code. Es war ein anderes Problem, das zu einer Überlastung des Anwendungsflusses führte. Da ich versucht habe, zu viele Elemente zu mongoDB hinzuzufügen, ohne dass Konfigurationsmöglichkeiten bestehen, trat das Problem mit dem Anrufstapel auf und ich brauchte ein paar Tage, um herauszufinden, was los war.


Nachverfolgung der Antwort von @Jeff Lowery: Ich habe diese Antwort sehr genossen und sie hat den Prozess meiner Arbeit mindestens um das 10-fache beschleunigt.

Ich bin neu in der Programmierung, aber ich habe versucht, die Antwort darauf zu modularisieren. Ich mochte es auch nicht, wenn der Fehler ausgelöst wurde, also habe ich ihn stattdessen in eine do while-Schleife eingeschlossen. Wenn etwas, das ich getan habe, falsch ist, können Sie mich gerne korrigieren.

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

Schauen Sie sich diese Übersicht an, um meine Dateien zu sehen und wie Sie die Schleife aufrufen. https://gist.github.com/gngenius02/3c842e5f46d151f730b012037ecd596c

Cigol auf
quelle
1

Wenn Sie keinen eigenen Wrapper implementieren möchten, können Sie ein Warteschlangensystem verwenden, z . B. async.queue , queue .

schwach
quelle
1

Ich dachte an einen anderen Ansatz mit Funktionsreferenzen, der die Größe des Aufrufstapels ohne Verwendung begrenzt 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;
}

Ausgabe:

$ 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)
Jeff Lowery
quelle
0

In Bezug auf die Erhöhung der maximalen Stapelgröße betragen die Standardwerte für die Speicherzuweisung von V8 auf 32-Bit- und 64-Bit-Computern 700 MB bzw. 1400 MB. In neueren Versionen von V8 werden Speicherbeschränkungen auf 64-Bit-Systemen nicht mehr von V8 festgelegt, was theoretisch keine Begrenzung anzeigt. Das Betriebssystem (Betriebssystem), auf dem Node ausgeführt wird, kann jedoch immer die Speicherkapazität begrenzen, die V8 beanspruchen kann, sodass die tatsächliche Grenze eines bestimmten Prozesses nicht allgemein angegeben werden kann.

V8 stellt jedoch die --max_old_space_sizeOption zur Verfügung, mit der die für einen Prozess verfügbare Speichermenge gesteuert werden kann, wobei ein Wert in MB akzeptiert wird. Wenn Sie die Speicherzuordnung erhöhen müssen, übergeben Sie dieser Option einfach den gewünschten Wert, wenn Sie einen Knotenprozess erzeugen.

Es ist oft eine hervorragende Strategie, die verfügbare Speicherzuordnung für eine bestimmte Knoteninstanz zu reduzieren, insbesondere wenn viele Instanzen ausgeführt werden. Überlegen Sie wie bei Stapelbeschränkungen, ob der massive Speicherbedarf besser an eine dedizierte Speicherschicht delegiert werden kann, z. B. eine speicherinterne Datenbank oder ähnliches.

serkan
quelle
0

Bitte überprüfen Sie, ob die Funktion, die Sie importieren, und die Funktion, die Sie in derselben Datei deklariert haben, nicht denselben Namen haben.

Ich werde Ihnen ein Beispiel für diesen Fehler geben. Betrachten Sie in Express JS (mit ES6) das folgende Szenario:

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

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

Das obige Szenario führt zu einem berüchtigten RangeError: Die maximale Größe des Aufrufstapels hat den Fehler überschritten , da sich die Funktion so oft selbst aufruft, dass der maximale Aufrufstapel nicht mehr ausreicht.

Meistens liegt der Fehler im Code vor (wie oben). Eine andere Möglichkeit zum Auflösen besteht darin, den Aufrufstapel manuell zu erhöhen. Nun, dies funktioniert in bestimmten Extremfällen, wird jedoch nicht empfohlen.

Hoffe meine Antwort hat dir geholfen.

Abhay Shiro
quelle
-4

Sie können Schleife für verwenden.

var items = {1, 2, 3}
for(var i = 0; i < items.length; i++) {
  if(i == items.length - 1) {
    res.ok(i);
  }
}
Marcin Kamiński
quelle
2
var items = {1, 2, 3}ist keine gültige JS-Syntax. Wie hängt das überhaupt mit der Frage zusammen?
musemind