Подтвердить что ты не робот

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

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

4b9b3361

Ответ 1

Вы должны перенести свой вызов рекурсивной функции в

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

чтобы дать node.js шанс очистить стек. Если вы этого не сделаете и существует множество циклов без какого-либо вызова функции aync реального, или если вы не дожидаетесь обратного вызова, ваш 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 в каждом раунде. Обычно это o.k. делать это каждые 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
    ...
});

Ответ 2

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

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

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

Ответ 3

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

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

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

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

Ответ 4

У меня была похожая проблема, как эта. У меня была проблема с использованием нескольких Array.map() подряд (около 8 карт одновременно), и я получал ошибку maximum_call_stack_exceeded. Я решил это, изменив карту на "для" петель

Так что, если вы используете много вызовов карты, изменение их на цикл 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

Поскольку циклы обычно лучше, чем карта, но не уменьшают, фильтруют или находят

Ответ 5

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

Ответ 6

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

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

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

Ответ 7

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

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

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

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

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

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

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

Ответ 8

Я подумал о другом подходе, использующем ссылки на функции, которые ограничивают размер стека вызовов без использования 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)