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'. Чи є можливість вирішити це питання, не видаляючи мої рекурсивні дзвінки?


3
Навіщо вам спочатку така глибока рекурсія?
Дан Абрамов

1
Будь ласка, можете ви розмістити якийсь код? Segmentation fault: 11зазвичай означає помилку у вузлі.
vkurchatkin

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

6
У вашій відповіді було кілька хороших і поганих моментів. Мені дуже сподобалось, що ви згадали setTimeout () та ін. Але немає необхідності використовувати setTimeout (fn, 1), оскільки setTimeout (fn, 0) цілком чудовий (тому нам не потрібен setTimeout (fn, 1) кожні% 1000 хаків). Це дозволяє JavaScript VM очистити стек і негайно відновити виконання. У node.js процес.nextTick () дещо кращий, оскільки дозволяє node.js робити деякі інші речі (I / O IIRC) також перед тим, як дозволити зворотний виклик відновити.
joonas.fi

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"

Це просто збільшує ліміт стека дзвінків. Я думаю, що це не підходить для виробничого коду, але він мені знадобився для сценарію, який запускається лише один раз.


Класний фокус, хоча особисто я б запропонував використовувати правильні практики, щоб уникнути помилок і створити більш досконале рішення.
декодер7283

Для мене це було рішення розблокування. У мене був сценарій, коли я запускав сторонній сценарій оновлення бази даних і отримував помилку діапазону. Я не збирався переписувати сторонній пакет, але мені потрібно було оновити базу даних → це виправило.
Тім Кок,

7

У деяких мовах це можна вирішити за допомогою оптимізації хвостового виклику, де рекурсійний виклик перетворюється під капот у цикл, тому не існує похибки максимального розміру стека.

Але в javascript поточні движки цього не підтримують, це передбачено для нової версії мови Ecmascript 6 .

У Node.js є кілька прапорів для ввімкнення функцій ES6, але хвіст ще не доступний.

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


Дякую. Мій виклик рекурсії не повертає значення, тож чи є спосіб викликати функцію і не чекати результату?
user1518183

І чи змінює ця функція деякі дані, як масив, що вона робить, які входи / виходи?
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

Адже цикли зазвичай кращі за карту, але не зменшують, фільтрують чи знаходять


Кілька місяців тому, коли я прочитав вашу відповідь, я не мав уявлення про те золото, яке ви мали у своїй відповіді. Нещодавно я виявив для себе це саме те саме, і це справді змусило мене навчитися всього, що маю, іноді важко подумати у формі ітераторів. Сподіваюся, це допомагає :: Я написав додатковий приклад, який включає обіцянки як частину циклу та показує, як чекати відповіді, перш ніж рухатись далі. приклад: gist.github.com/gngenius02/…
cigol 02.03.20

Мені подобається те, що ти там робив (і сподіваюся, ти не проти, якщо я схоплю те, що вирізано для моєї панелі інструментів). Я здебільшого використовую синхронний код, саме тому я зазвичай віддаю перевагу циклам. Але це дорогоцінний камінь, який ти теж там отримав, і, швидше за все, знайдеш свій шлях на наступному сервері, над яким я працюю
Werlious

2

Попередньо:

для мене програма зі стеком викликів Max була не через мій код. Це виявилося іншою проблемою, яка спричинила затори в потоці програми. Отож, оскільки я намагався додати занадто багато елементів до mongoDB, не маючи жодних шансів на конфігурацію, виникла проблема зі стеком дзвінків, і мені знадобилося кілька днів, щоб зрозуміти, що відбувається .... це сказав:


Слідом за тим, що відповів @Jeff Lowery: Мені так сподобалась ця відповідь, і це пришвидшило процес того, що я робив, принаймні в 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.

Часто це відмінна стратегія зменшити доступне виділення пам'яті для даного екземпляра 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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.