Node.js - Przekroczono maksymalny rozmiar stosu wywołań

80

Kiedy uruchamiam kod, Node.js zgłasza "RangeError: Maximum call stack size exceeded"wyjątek spowodowany zbyt dużą liczbą wywołań rekurencyjnych. Próbowałem zwiększyć rozmiar stosu Node.js o sudo node --stack-size=16000 app, ale Node.js ulega awarii bez żadnego komunikatu o błędzie. Gdy uruchomię to ponownie bez sudo, a następnie node.js wydruki 'Segmentation fault: 11'. Czy istnieje możliwość rozwiązania tego problemu bez usuwania wywołań rekurencyjnych?

user1518183
źródło
3
Dlaczego w pierwszej kolejności potrzebujesz tak głębokiej rekursji?
Dan Abramov,
1
Czy możesz wysłać kod? Segmentation fault: 11zwykle oznacza błąd w węźle.
vkurchatkin
1
@Dan Abramov: Dlaczego głęboka rekurencja? Może to stanowić problem, jeśli chcesz wykonać iterację po tablicy lub liście i wykonać na nich operację asynchroniczną (np. Niektóre operacje na bazie danych). Jeśli użyjesz wywołania zwrotnego z operacji asynchronicznej, aby przejść do następnego elementu, będzie co najmniej jeden dodatkowy poziom rekurencji dla każdego elementu na liście. Anty-wzór dostarczony przez heinob poniżej zapobiega wydmuchiwaniu stosu.
Philip Callender,
1
@PhilipCallender Nie wiedziałem, że robisz rzeczy asynchroniczne, dzięki za wyjaśnienie!
Dan Abramov,
@DanAbramov Nie musi też być głęboko, żeby się zawiesić. V8 nie ma szansy na wyczyszczenie rzeczy przydzielonych na stosie. Funkcje wywołane wcześniej, które już dawno przestały działać, mogły tworzyć zmienne na stosie, do których nie ma już odwołań, ale nadal są przechowywane w pamięci. Jeśli wykonujesz jakąś intensywną, czasochłonną operację w sposób synchroniczny i przydzielasz zmienne na stosie, gdy jesteś przy tym, nadal będziesz się zawieszać z tym samym błędem. Mój synchroniczny parser JSON zawiesił się
FeignMan

Odpowiedzi:

114

Powinieneś zawinąć wywołanie funkcji rekurencyjnej w plik

  • setTimeout,
  • setImmediate lub
  • process.nextTick

funkcja, aby dać node.js szansę na wyczyszczenie stosu. Jeśli tego nie zrobisz i istnieje wiele pętli bez prawdziwego wywołania funkcji asynchronicznej lub jeśli nie będziesz czekać na wywołanie zwrotne, RangeError: Maximum call stack size exceededbędzie to nieuniknione .

Istnieje wiele artykułów dotyczących „Potencjalnej pętli asynchronicznej”. Oto jeden .

Teraz trochę więcej przykładowego kodu:

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

To prawda:

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

Teraz Twoja pętla może stać się zbyt wolna, ponieważ tracimy trochę czasu (jedna podróż w obie strony przeglądarki) na rundę. Ale nie musisz sprawdzać setTimeoutw każdej rundzie. Zwykle można to robić co tysięczny raz. Ale może się to różnić w zależności od rozmiaru twojego stosu:

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
źródło
6
Twoja odpowiedź zawierała dobre i złe strony. Bardzo podobało mi się, że wspomniałeś o setTimeout () i in. Ale nie ma potrzeby używania setTimeout (fn, 1), ponieważ setTimeout (fn, 0) jest całkowicie w porządku (więc nie potrzebujemy setTimeout (fn, 1) co% 1000 włamań). Umożliwia maszynie wirtualnej JavaScript wyczyszczenie stosu i natychmiastowe wznowienie wykonywania. W node.js metoda process.nextTick () jest nieco lepsza, ponieważ pozwala node.js na wykonanie innych czynności (I / O IIRC) również przed wznowieniem wywołania zwrotnego.
joonas.fi
2
Powiedziałbym, że w takich przypadkach lepiej jest użyć setImmediate zamiast setTimeout.
BaNz,
4
@ joonas.fi: Mój "hack" z% 1000 jest konieczny. Wykonywanie setImmediate / setTimeout (nawet z 0) w każdej pętli jest znacznie wolniejsze.
heinob
3
Chcesz zaktualizować swoje niemieckie komentarze w kodzie z tłumaczeniem na język angielski ...? :) Rozumiem, ale inni mogą nie mieć tyle szczęścia.
Robert Rossmann
dziękuję
Angelos Kyriakopoulos
30

Znalazłem brudne rozwiązanie:

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

Po prostu zwiększa limit stosu wywołań. Myślę, że to nie jest odpowiednie dla kodu produkcyjnego, ale potrzebowałem go do skryptu, który był uruchamiany tylko raz.

user1518183
źródło
Fajna sztuczka, chociaż osobiście sugerowałbym stosowanie właściwych praktyk, aby uniknąć błędów i stworzyć bardziej dopracowane rozwiązanie.
dekoder7283
Dla mnie było to rozwiązanie odblokowujące. Miałem scenariusz, w którym uruchamiałem skrypt aktualizacji bazy danych innej firmy i otrzymywałem błąd zakresu. Nie miałem zamiaru przepisywać pakietu innej firmy, ale musiałem zaktualizować bazę danych → to naprawiło.
Tim Kock
7

W niektórych językach można to rozwiązać za pomocą optymalizacji wywołań ogonowych, w której wywołanie rekurencji jest przekształcane pod maską w pętlę, więc nie występuje błąd osiągnięcia maksymalnego rozmiaru stosu.

Ale w javascript obecne silniki tego nie obsługują, jest to przewidziane dla nowej wersji języka Ecmascript 6 .

Node.js ma kilka flag włączających funkcje ES6, ale wywołanie ogonowe nie jest jeszcze dostępne.

Możesz więc refaktoryzować swój kod, aby zaimplementować technikę zwaną trampolinowaniem lub refaktoryzacją w celu przekształcenia rekursji w pętlę .

Uniwersytet Angular
źródło
Dziękuję Ci. Moje wywołanie rekurencji nie zwraca wartości, więc czy istnieje sposób na wywołanie funkcji i nie czekanie na wynik?
user1518183
I czy funkcja zmienia niektóre dane, jak tablica, co robi funkcja, jakie są wejścia / wyjścia?
Angular University,
5

Miałem podobny problem jak ten. Miałem problem z używaniem wielu Array.map () w rzędzie (około 8 map na raz) i otrzymywałem błąd maximum_call_stack_exceeded. Rozwiązałem ten problem, zmieniając mapę na pętle „for”

Więc jeśli używasz wielu wywołań map, zmiana ich na pętle for może rozwiązać problem

Edytować

Dla jasności i prawdopodobnie nie-potrzebnych-ale-dobrze-wiedzieć-informacji, użycie .map()powoduje przygotowanie tablicy (rozwiązywanie funkcji pobierających itp.), A wywołanie zwrotne jest buforowane, a także wewnętrznie zachowuje indeks tablicy ( więc wywołanie zwrotne zawiera poprawny indeks / wartość). Jest to układane w stosy z każdym wywołaniem zagnieżdżonym i zaleca się ostrożność, gdy nie jest również zagnieżdżone, ponieważ następna .map()może zostać wywołana, zanim pierwsza tablica zostanie wyrzucona do pamięci (jeśli w ogóle).

Weź ten przykład:

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

Jeśli zmienimy to na:

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
}

Mam nadzieję, że to ma jakiś sens (nie mam najlepszego sposobu na słowa) i pomaga kilku, aby zapobiec drapaniu głowy, przez które przeszedłem

Jeśli ktoś jest zainteresowany, tutaj jest również test wydajnościowy porównujący mapę i pętle (nie moja praca).

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

Pętle For są zwykle lepsze niż mapowanie, ale nie redukują, nie filtrują ani nie znajdują

Werlious
źródło
kilka miesięcy temu, kiedy przeczytałem twoją odpowiedź, nie miałem pojęcia, jakie złoto masz w odpowiedzi. Niedawno odkryłem to samo dla siebie i to naprawdę sprawiło, że chciałem oduczyć się wszystkiego, co mam, po prostu czasami trudno jest myśleć w formie iteratorów. Mam nadzieję, że to pomoże: Napisałem dodatkowy przykład, który zawiera obietnice jako część pętli i pokazuje, jak czekać na odpowiedź przed przejściem dalej. przykład: gist.github.com/gngenius02/…
cigol
Uwielbiam to, co tam zrobiłeś (i mam nadzieję, że nie masz nic przeciwko, jeśli wezmę to wycięte do mojego zestawu narzędzi). Najczęściej używam kodu synchronicznego, dlatego zazwyczaj wolę pętle. Ale to również klejnot, który tam dostałeś i najprawdopodobniej trafi na następny serwer, na którym pracuję
Werlious
2

Przed:

dla mnie program ze stosem wywołań Max nie był spowodowany moim kodem. Skończyło się na tym, że był to inny problem, który spowodował zatory w przepływie aplikacji. Więc ponieważ próbowałem dodać zbyt wiele elementów do mongoDB bez żadnych szans na konfigurację, pojawił się problem ze stosem wywołań i zajęło mi kilka dni, aby dowiedzieć się, co się dzieje ...


Kontynuując to, co odpowiedział @Jeff Lowery: Tak bardzo podobała mi się ta odpowiedź i przyspieszyła proces tego, co robiłem, co najmniej 10x.

Jestem nowy w programowaniu, ale próbowałem modularyzować odpowiedź na to pytanie. Poza tym nie podobał mi się wyrzucany błąd, więc zamiast tego owinąłem go w pętlę do while. Jeśli cokolwiek zrobiłem, jest nieprawidłowe, prosimy o poprawienie mnie.

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

Zapoznaj się z tym streszczeniem, aby zobaczyć moje pliki i jak wywołać pętlę. https://gist.github.com/gngenius02/3c842e5f46d151f730b012037ecd596c

cigol on
źródło
1

Jeśli nie chcesz implementować własnego opakowania, możesz skorzystać z systemu kolejek, np. Async.queue , queue .

słabe
źródło
1

Pomyślałem o innym podejściu wykorzystującym odwołania do funkcji, które ograniczają rozmiar stosu wywołań bez użycia 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;
}

wynik:

$ 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
źródło
0

Jeśli chodzi o zwiększenie maksymalnego rozmiaru stosu, na komputerach 32-bitowych i 64-bitowych domyślne ustawienia alokacji pamięci V8 to odpowiednio 700 MB i 1400 MB. W nowszych wersjach V8 limity pamięci w systemach 64-bitowych nie są już ustawiane przez V8, co teoretycznie oznacza brak ograniczeń. Jednak system operacyjny (system operacyjny), na którym działa Node, może zawsze ograniczyć ilość pamięci, którą V8 może zająć, więc nie można ogólnie określić prawdziwego limitu dowolnego procesu.

Chociaż V8 udostępnia --max_old_space_sizeopcję, która pozwala kontrolować ilość pamięci dostępnej dla procesu , akceptując wartość w MB. Jeśli potrzebujesz zwiększyć przydział pamięci, po prostu przekaż tej opcji żądaną wartość podczas tworzenia procesu Node.

Często doskonałą strategią jest zmniejszenie dostępnej alokacji pamięci dla danej instancji Node, zwłaszcza w przypadku uruchamiania wielu instancji. Podobnie jak w przypadku limitów stosu, rozważ, czy ogromne zapotrzebowanie na pamięć jest lepiej delegowane do dedykowanej warstwy pamięci, takiej jak baza danych w pamięci lub podobna.

serkan
źródło
0

Sprawdź, czy funkcja, którą importujesz i ta, którą zadeklarowałeś w tym samym pliku, nie mają takiej samej nazwy.

Podam przykład tego błędu. W ekspresowym JS (używając ES6) rozważ następujący scenariusz:

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

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

Powyższy scenariusz spowoduje niesławny RangeError: Przekroczono maksymalny rozmiar stosu wywołań ponieważ funkcja wywołuje się tak wiele razy, że zabraknie jej maksymalnego stosu wywołań.

W większości przypadków błąd jest w kodzie (jak ten powyżej). Innym sposobem rozwiązania problemu jest ręczne zwiększenie stosu wywołań. Cóż, działa to w niektórych ekstremalnych przypadkach, ale nie jest zalecane.

Mam nadzieję, że moja odpowiedź ci pomogła.

Abhay Shiro
źródło
-4

Możesz użyć pętli dla.

var items = {1, 2, 3}
for(var i = 0; i < items.length; i++) {
  if(i == items.length - 1) {
    res.ok(i);
  }
}
Marcin Kamiński
źródło
2
var items = {1, 2, 3}nie jest poprawną składnią JS. jak to w ogóle ma związek z pytaniem?
musemind