it-swarm.com.de

Node.js - Maximale Aufrufstackgröße überschritten

Wenn ich meinen Code ausführt, löst Node.js eine "RangeError: Maximum call stack size exceeded"-Ausnahme aus, die durch zu viele Rekursionsaufrufe verursacht wird. Ich habe versucht, die Stackgröße von Node.js um Sudo node --stack-size=16000 app zu erhöhen, aber Node.js stürzt ohne Fehlermeldung ab. Wenn ich dieses Programm erneut ohne Sudo starte, drucke Node.js 'Segmentation fault: 11'. Gibt es eine Möglichkeit, dieses Problem zu lösen, ohne den Rekursionsaufruf zu entfernen? 

Vielen Dank

64
user1518183

Sie sollten Ihren rekursiven Funktionsaufruf in a einschließen 

  • setTimeout,
  • setImmediate oder 
  • process.nextTick 

funktion, um node.js die Chance zu geben, den Stack zu löschen. Wenn Sie dies nicht tun und es viele Schleifen ohne real async-Funktionsaufruf gibt oder wenn Sie nicht auf den Rückruf warten, wird Ihr RangeError: Maximum call stack size exceededunvermeidlich.

Es gibt viele Artikel über "Potential Async Loop". Hier ist eine

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 kann Ihre Schleife zu langsam werden, weil wir pro Runde ein wenig Zeit verlieren (einen Browser-Roundtrip). Sie müssen jedoch nicht in jeder Runde setTimeout aufrufen. Normalerweise ist es o.k. es jedes tausendste Mal zu tun. Dies kann jedoch abhängig von Ihrer Stackgröße variieren:

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

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"

Erhöhen Sie einfach das Call-Stack-Limit. Ich denke, dass dies nicht für Produktionscode geeignet ist, aber ich brauchte es für ein Skript, das nur einmal ausgeführt wird.

20
user1518183

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, so dass kein maximaler Stack-Stack-Fehler vorliegt.

Aber in Javascript unterstützen die aktuellen Engines dies nicht, es ist für die neue Version der Sprache Ecmascript 6 vorgesehen.

Node.js verfügt über einige Flags zum Aktivieren von ES6-Funktionen, aber der Rückruf ist noch nicht verfügbar.

Sie können also Ihren Code umwandeln, um eine Technik namens Trampolining zu implementieren, oder einen Refaktor, um Rekursion in eine Schleife umzuwandeln .

5

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

1
weakish

Ich hatte ein ähnliches Problem wie dieses . Ich hatte ein Problem mit der Verwendung mehrerer Array.map () in einer Reihe (etwa 8 Maps auf einmal) Ich erhielt den Fehler maximum_call_stack_exceeded . Ich habe das Problem gelöst dies durch Ändern der Karte in 'for' Schleifen

Wenn Sie viele Kartenaufrufe verwenden, kann das Problem dadurch behoben werden, dass Sie sie in for-Schleifen ändern

Bearbeiten

Nur aus Gründen der Klarheit und wahrscheinlich-nicht-benötigt-aber-gut-weiß-wissen-Informationen bewirkt die Verwendung von .map(), dass das Array vorbereitet wird (Auflösung von Gettern usw.) und der Callback zwischengespeichert wird. Außerdem wird intern ein Index des Arrays beibehalten (so wird der Rückruf mit dem richtigen Index/Wert geliefert). Dies wird bei jedem verschachtelten Aufruf gestapelt. Wenn auch nicht verschachtelt, ist Vorsicht geboten, da die nächste .map() aufgerufen werden könnte, bevor das erste Array (wenn überhaupt) Speicherbereinigung ist. 

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:

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 habe nicht die beste Art mit Worten) und hilft einigen, den Kopf zu kratzen, den ich durchgemacht habe

Wenn jemand interessiert ist, hier ist auch ein Leistungstest, der die Karte und die Schleifen vergleicht (nicht meine Arbeit).

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

Denn Schleifen sind in der Regel besser als Map, jedoch nicht reduzieren, filtern oder suchen

1
Werlious

Ich habe mir einen anderen Ansatz überlegt, bei dem Funktionsreferenzen verwendet werden, um die Größe des Aufrufstapels zu begrenzen, ohne setTimeout() (Node.js, v10.16.0) zu verwenden:

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

Bitte überprüfen Sie, ob die zu importierende Funktion und die von Ihnen in derselben Datei deklarierte Funktion nicht denselben Namen haben.

Ich werde Ihnen ein Beispiel für diesen Fehler geben. Berücksichtigen 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 infamous RangeError: Maximale Aufrufstackgröße überschritten error, da die Funktion sich so oft aufruft, dass der maximale Aufrufstapel aufgebraucht ist. 

Meistens liegt der Fehler im Code (wie oben). Eine andere Möglichkeit der Auflösung besteht darin, den Aufrufstapel manuell zu erhöhen. Das funktioniert zwar für bestimmte Extremfälle, wird aber nicht empfohlen.

Hoffe, meine Antwort hat dir geholfen.

0
Abhay Shiro

In Bezug auf das Erhöhen der maximalen Stapelgröße betragen die Standardbelegungsspeicher für V8 auf 32-Bit- und 64-Bit-Maschinen 700 MB bzw. 1400 MB. In neueren Versionen von V8 werden Speichergrenzen auf 64-Bit-Systemen nicht mehr von V8 festgelegt, was theoretisch keine Begrenzung bedeutet. Das Betriebssystem (Betriebssystem), auf dem Node ausgeführt wird, kann jedoch immer die Speichermenge begrenzen, die V8 beanspruchen kann. Daher kann die tatsächliche Grenze eines bestimmten Prozesses nicht allgemein angegeben werden.

V8 stellt zwar die Option --max_old_space_size zur Verfügung, mit der die für einen Prozess zur Verfügung stehende 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 Node-Prozess starten.

Es ist oft eine ausgezeichnete Strategie, die verfügbare Speicherzuordnung für eine bestimmte Knoteninstanz zu reduzieren, insbesondere wenn viele Instanzen ausgeführt werden. Berücksichtigen Sie wie bei den Stack-Grenzwerten, ob massiver Speicherbedarf besser an eine dedizierte Speicherebene delegiert wird, z. B. eine In-Memory-Datenbank oder ähnliches.

0
serkan