Node.js - превышен максимальный размер стека вызовов

80

Когда я запускаю свой код, Node.js выдает "RangeError: Maximum call stack size exceeded"исключение, вызванное слишком большим количеством рекурсивных вызовов. Я попытался увеличить размер стека Node.js на sudo node --stack-size=16000 app, но Node.js вылетает без сообщения об ошибке. Когда я запускаю этот раз без Суда, то Node.js отпечатков 'Segmentation fault: 11'. Есть ли возможность решить эту проблему, не удаляя мои рекурсивные вызовы?

user1518183
источник
3
Зачем вообще нужна такая глубокая рекурсия?
Дан Абрамов
1
Пожалуйста, не могли бы вы разместить код? Segmentation fault: 11обычно означает ошибку в узле.
vkurchatkin 06
1
@ Дан Абрамов: Почему именно глубокая рекурсия? Это может быть проблемой, если вы хотите перебрать массив или список и выполнить асинхронную операцию для каждого (например, некоторую операцию с базой данных). Если вы используете обратный вызов из асинхронной операции для перехода к следующему элементу, то для каждого элемента в списке будет как минимум один дополнительный уровень рекурсии. Анти-шаблон, предоставленный heinob ниже, останавливает стек от выдувания.
Филип Каллендер
1
@PhilipCallender Я не знал, что вы делаете асинхронные вещи, спасибо за разъяснения!
Дэн Абрамов
@DanAbramov Не обязательно быть глубоким, чтобы разбиться. V8 не имеет возможности очистить содержимое стека. Вызванные ранее функции, которые давно перестали выполняться, могли создавать переменные в стеке, на которые больше не ссылаются, но которые все еще хранятся в памяти. Если вы выполняете какую-либо интенсивную трудоемкую операцию в синхронном режиме и выделяете переменные в стеке, пока вы в ней, вы все равно столкнетесь с той же ошибкой. У меня синхронный синтаксический анализатор JSON аварийно завершил работу при глубине стека
вызовов

Ответы:

114

Вы должны заключить рекурсивный вызов функции в

  • setTimeout,
  • setImmediate или же
  • process.nextTick

чтобы дать node.js возможность очистить стек. Если вы этого не сделаете и есть много циклов без какого-либо реального вызова асинхронной функции или если вы не дождетесь обратного вызова, RangeError: Maximum call stack size exceededэто будет неизбежно .

Есть много статей о «Возможном асинхронном цикле». Вот один .

А теперь еще несколько примеров кода:

// 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в каждом раунде. Обычно это нормально делать это каждые 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
    ...
});
Heinob
источник
6
В вашем ответе было несколько хороших и плохих моментов. Мне очень понравилось, что вы упомянули setTimeout () и др. Но нет необходимости использовать setTimeout (fn, 1), поскольку setTimeout (fn, 0) отлично подходит (так что нам не нужен setTimeout (fn, 1) каждый хак% 1000). Это позволяет виртуальной машине JavaScript очищать стек и немедленно возобновлять выполнение. В node.js process.nextTick () немного лучше, потому что он позволяет node.js делать некоторые другие вещи (I / O IIRC), прежде чем возобновить ваш обратный вызов.
joonas.fi 01
2
Я бы сказал, что в этих случаях лучше использовать setImmediate вместо setTimeout.
BaNz
4
@ joonas.fi: Нужен мой "хак" с% 1000. Выполнение setImmediate / setTimeout (даже с 0) в каждом цикле значительно медленнее.
heinob
3
Не забудьте обновить свои комментарии в коде на немецком языке с переводом на английский ...? :) Я понимаю, но другим может не повезти.
Роберт Россманн
спасибо
Ангелос Кириакопулос
30

Нашел грязное решение:

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

Это просто увеличивает лимит стека вызовов. Я думаю, что это не подходит для производственного кода, но мне это нужно для скрипта, который запускается только один раз.

user1518183
источник
Классный трюк, хотя лично я бы посоветовал использовать правильные практики, чтобы избежать ошибок и создать более продуманное решение.
decoder7283
Для меня это было решение для разблокировки. У меня был сценарий, когда я запускал сторонний сценарий обновления базы данных и получал ошибку диапазона. Я не собирался переписывать сторонний пакет, но мне нужно было обновить базу данных → это исправило.
Тим Кок
7

В некоторых языках это может быть решено с помощью оптимизации хвостового вызова, когда рекурсивный вызов преобразуется под капотом в цикл, поэтому ошибки достижения максимального размера стека не существует.

Но в javascript текущие движки этого не поддерживают, это предусмотрено для новой версии языка Ecmascript 6 .

В Node.js есть несколько флагов для включения функций ES6, но хвостовой вызов пока недоступен.

Таким образом, вы можете реорганизовать свой код, чтобы реализовать технику, называемую трамплином , или рефакторинг, чтобы преобразовать рекурсию в цикл .

Угловой университет
источник
Спасибо. Мой рекурсивный вызов не возвращает значения, так есть ли способ вызвать функцию и не ждать результата?
user1518183 05
И изменяет ли функция какие-то данные, например массив, что она делает, каковы входы / выходы?
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
}

Я надеюсь, что в этом есть какой-то смысл (у меня не лучший способ использовать слова) и помогает некоторым избежать царапин на голове, через которые я прошел

Если кому-то интересно, вот еще тест производительности, сравнивающий карту и циклы (не моя работа).

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

Циклы For обычно лучше, чем map, но не сокращают, фильтруют или находят

Верлиус
источник
Пару месяцев назад, когда я читал ваш ответ, я понятия не имел, сколько золота было в вашем ответе. Я недавно обнаружил то же самое для себя, и это действительно заставило меня захотеть забыть все, что у меня есть, просто иногда трудно думать в форме итераторов. Надеюсь, это поможет: я написал дополнительный пример, который включает обещания как часть цикла и показывает, как дождаться ответа, прежде чем двигаться дальше. пример: gist.github.com/gngenius02/…
cigol 2
Мне нравится то, что вы там сделали (и надеюсь, вы не против, если я возьму это для своего ящика с инструментами). Я в основном использую синхронный код, поэтому обычно предпочитаю циклы. Но это драгоценный камень, который у вас тоже есть, и, скорее всего, он попадет на следующий сервер, над которым я работаю
Верлиус,
2

Предварительно:

для меня программа со стеком вызовов Max не была из-за моего кода. В конечном итоге это была другая проблема, которая вызвала перегрузку потока приложения. Так как я пытался добавить слишком много элементов в mongoDB без какой-либо конфигурации, скорее всего возникла проблема со стеком вызовов, и мне потребовалось несколько дней, чтобы понять, что происходит ...


В продолжение того, что ответил @Jeff Lowy: мне очень понравился этот ответ, и он ускорил процесс того, что я делал, по крайней мере, в 10 раз.

Я новичок в программировании, но я попытался разбить ответ на модули. Кроме того, мне не понравилось, что выдается ошибка, поэтому я вместо этого заключил ее в цикл do while. Если что-то, что я сделал, неправильно, пожалуйста, поправьте меня.

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

Если вы не хотите реализовывать свою собственную оболочку, вы можете использовать систему очередей, например async.queue , queue .

слабый
источник
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 составляют 700 МБ и 1400 МБ соответственно. В более новых версиях V8 ограничения памяти в 64-битных системах больше не устанавливаются V8, что теоретически означает отсутствие ограничений. Однако ОС (операционная система), в которой работает Node, всегда может ограничить объем памяти, который может занять V8, поэтому истинный предел любого данного процесса не может быть определен в общих чертах.

Хотя V8 предоставляет --max_old_space_sizeопцию, которая позволяет контролировать объем памяти, доступной процессу , принимая значение в МБ. Если вам нужно увеличить объем памяти, просто передайте этой опции желаемое значение при порождении процесса Node.

Часто это отличная стратегия для уменьшения объема доступной памяти для данного экземпляра узла, особенно при запуске множества экземпляров. Как и в случае с ограничениями стека, подумайте, не лучше ли делегировать большие потребности в памяти на выделенный уровень хранения, такой как база данных в памяти или аналогичный.

Серкан
источник
0

Убедитесь, что у функции, которую вы импортируете, и функции, которую вы объявили в одном файле, разные имена.

Я приведу вам пример этой ошибки. В экспресс-JS (с использованием ES6) рассмотрим следующий сценарий:

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

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

Вышеупомянутый сценарий вызовет печально известную ошибку RangeError: Превышен максимальный размер стека вызовов, потому что функция продолжает вызывать себя так много раз, что у нее заканчивается максимальный стек вызовов.

В большинстве случаев ошибка заключается в коде (например, выше). Другой способ решения - вручную увеличить стек вызовов. Что ж, это работает в некоторых крайних случаях, но не рекомендуется.

Надеюсь, мой ответ вам помог.

Абхай Широ
источник
-4

Вы можете использовать цикл для.

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