Node.js - Đã vượt quá kích thước ngăn xếp cuộc gọi tối đa


80

Khi tôi chạy mã của mình, Node.js ném ra một "RangeError: Maximum call stack size exceeded"ngoại lệ do quá nhiều lệnh gọi đệ quy gây ra. Tôi đã cố gắng tăng kích thước ngăn xếp Node.js lên sudo node --stack-size=16000 app, nhưng Node.js bị treo mà không có bất kỳ thông báo lỗi nào. Khi tôi chạy lại điều này mà không có sudo, thì Node.js sẽ in 'Segmentation fault: 11'. Có khả năng nào để giải quyết vấn đề này mà không xóa các cuộc gọi đệ quy của tôi không?


3
Tại sao bạn cần đệ quy sâu như vậy ngay từ đầu?
Dan Abramov

1
Xin vui lòng, bạn có thể đăng một số mã? Segmentation fault: 11thường có nghĩa là một lỗi trong nút.
vkurchatkin

1
@Dan Abramov: Tại sao phải đệ quy sâu? Đây có thể là một vấn đề nếu bạn muốn lặp qua một mảng hoặc danh sách và thực hiện thao tác không đồng bộ trên từng mảng (ví dụ: một số thao tác cơ sở dữ liệu). Nếu bạn sử dụng lệnh gọi lại từ thao tác không đồng bộ để chuyển sang mục tiếp theo, thì sẽ có ít nhất một mức đệ quy bổ sung cho mỗi mục trong danh sách. Mô hình chống được cung cấp bởi heinob bên dưới ngăn ngăn xếp bị thổi ra.
Philip Callender

1
@PhilipCallender Tôi không nhận ra bạn đang làm những thứ không đồng bộ, cảm ơn bạn đã giải thích rõ!
Dan Abramov

@DanAbramov Không cần phải quá sâu để gặp sự cố. V8 không có cơ hội dọn dẹp những thứ được phân bổ trên ngăn xếp. Các hàm được gọi trước đó đã ngừng thực thi từ lâu có thể đã tạo ra các biến trên ngăn xếp không được tham chiếu nữa nhưng vẫn được giữ trong bộ nhớ. Nếu bạn đang thực hiện bất kỳ hoạt động tốn thời gian chuyên sâu nào theo kiểu đồng bộ và phân bổ các biến trên ngăn xếp khi đang ở đó, bạn vẫn sẽ gặp phải lỗi tương tự. Tôi đã nhận được trình phân tích cú pháp JSON đồng bộ của mình gặp sự cố ở độ sâu callstack là 9. kikobeats.com/synchronously-asynchronous
FeignMan

Câu trả lời:


114

Bạn nên gói lời gọi hàm đệ quy của mình thành một

  • setTimeout,
  • setImmediate hoặc là
  • process.nextTick

chức năng cung cấp cho node.js cơ hội để xóa ngăn xếp. Nếu bạn không làm điều đó và có nhiều vòng lặp mà không có bất kỳ lệnh gọi hàm không đồng bộ thực sự nào hoặc nếu bạn không đợi lệnh gọi lại, bạn RangeError: Maximum call stack size exceededsẽ không thể tránh khỏi .

Có rất nhiều bài báo liên quan đến "Vòng lặp không đồng bộ tiềm năng". Đây là một .

Bây giờ một số mã ví dụ khác:

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

Đúng rồi đó:

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

Bây giờ, vòng lặp của bạn có thể trở nên quá chậm, vì chúng tôi mất một ít thời gian (một vòng trình duyệt) mỗi vòng. Nhưng bạn không cần phải gọi setTimeouttrong mỗi vòng. Thông thường, nó là ok để làm điều đó mỗi lần thứ 1000. Nhưng điều này có thể khác nhau tùy thuộc vào kích thước ngăn xếp của bạn:

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
Có một số điểm tốt và điểm xấu trong câu trả lời của bạn. Tôi thực sự thích rằng bạn đã đề cập đến setTimeout () et al. Nhưng không cần sử dụng setTimeout (fn, 1), vì setTimeout (fn, 0) hoàn toàn ổn (vì vậy chúng tôi không cần setTimeout (fn, 1) mỗi lần hack% 1000). Nó cho phép máy ảo JavaScript xóa ngăn xếp và tiếp tục thực thi ngay lập tức. Trong node.js process.nextTick () tốt hơn một chút vì nó cho phép node.js thực hiện một số công việc khác (I / O IIRC) trước khi cho phép gọi lại của bạn tiếp tục.
joonas.fi

2
Tôi sẽ nói rằng tốt hơn nên sử dụng setIm Instant thay vì setTimeout trong những trường hợp này.
BaNz

4
@ joonas.fi: Việc "hack"% 1000 của tôi là cần thiết. Thực hiện setIm Instant / setTimeout (ngay cả với 0) trên mỗi vòng lặp chậm hơn đáng kể.
heinob

3
Hãy quan tâm đến việc cập nhật các bình luận bằng tiếng Đức bằng mã của bạn với bản dịch tiếng Anh ...? :) Tôi hiểu nhưng những người khác có thể không may mắn như vậy.
Robert Rossmann


30

Tôi tìm thấy một giải pháp bẩn:

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

Nó chỉ tăng giới hạn ngăn xếp cuộc gọi. Tôi nghĩ rằng điều này không phù hợp với mã sản xuất, nhưng tôi cần nó cho tập lệnh chỉ chạy một lần.


Mẹo hay, mặc dù cá nhân tôi khuyên bạn nên sử dụng các phương pháp thực hành đúng để tránh sai lầm và tạo ra một giải pháp hoàn thiện hơn.
decoder7283 19/03/18

Đối với tôi, đây là một giải pháp bỏ chặn. Tôi đã gặp một trường hợp trong đó tôi đang chạy tập lệnh nâng cấp của bên thứ ba của cơ sở dữ liệu và gặp lỗi phạm vi. Tôi sẽ không viết lại gói của bên thứ ba nhưng cần nâng cấp cơ sở dữ liệu → điều này đã sửa nó.
Tim Kock

7

Trong một số ngôn ngữ, điều này có thể được giải quyết bằng cách tối ưu hóa cuộc gọi đuôi, trong đó lệnh gọi đệ quy được chuyển dưới mui xe thành một vòng lặp để không tồn tại lỗi đạt đến kích thước ngăn xếp tối đa.

Nhưng trong javascript, các công cụ hiện tại không hỗ trợ điều này, điều này được dự đoán trước cho phiên bản mới của ngôn ngữ Ecmascript 6 .

Node.js có một số cờ để kích hoạt các tính năng của ES6 nhưng lệnh gọi đuôi vẫn chưa khả dụng.

Vì vậy, bạn có thể cấu trúc lại mã của mình để triển khai một kỹ thuật được gọi là trampolining hoặc tái cấu trúc để chuyển đổi đệ quy thành một vòng lặp .


Cảm ơn bạn. Lời gọi đệ quy của tôi không trả về giá trị, vậy có cách nào để gọi hàm mà không phải đợi kết quả không?
user1518183

Và chức năng nó có thay đổi một số dữ liệu, chẳng hạn như một mảng, nó thực hiện chức năng gì, đầu vào / đầu ra là gì?
Đại học Angular

5

Tôi đã có một vấn đề tương tự như thế này. Tôi đã gặp sự cố khi sử dụng nhiều Array.map () trong một hàng (khoảng 8 bản đồ cùng một lúc) và gặp lỗi Maximum_call_stack_exceeded. Tôi đã giải quyết vấn đề này bằng cách thay đổi bản đồ thành vòng lặp 'cho'

Vì vậy, nếu bạn đang sử dụng rất nhiều lệnh gọi bản đồ, việc thay đổi chúng thành vòng lặp for có thể khắc phục sự cố

Biên tập

Chỉ để rõ ràng và có thể-không-cần-nhưng-tốt-cần-biết-thông tin, việc sử dụng .map()khiến mảng được chuẩn bị sẵn (giải quyết getters, v.v.) và lệnh gọi lại được lưu vào bộ nhớ cache, đồng thời lưu giữ một chỉ mục của mảng ( vì vậy lệnh gọi lại được cung cấp với chỉ mục / giá trị chính xác). Điều này sẽ ngăn xếp với mỗi lệnh gọi lồng nhau và cũng nên thận trọng khi không lồng ghép, vì lệnh tiếp theo .map()có thể được gọi trước khi mảng đầu tiên được thu gom rác (nếu có).

Lấy ví dụ sau:

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

Nếu chúng tôi thay đổi điều này thành:

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
}

Tôi hy vọng điều này có ý nghĩa (tôi không có cách tốt nhất để sử dụng từ ngữ) và giúp một số người đỡ phải gãi đầu mà tôi đã trải qua

Nếu ai quan tâm, đây cũng là một bài kiểm tra hiệu suất so sánh bản đồ và vòng lặp for (không phải công việc của tôi).

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

Đối với các vòng thường tốt hơn so với bản đồ, nhưng không giảm, lọc hoặc tìm


vài tháng trước, khi tôi đọc câu trả lời của bạn, tôi không biết bạn đã có câu trả lời như thế nào. Gần đây tôi đã phát hiện ra điều này rất giống với bản thân mình và Nó thực sự khiến tôi muốn mở ra mọi thứ mà tôi có, đôi khi thật khó để suy nghĩ dưới dạng trình lặp. Hy vọng điều này sẽ giúp :: Tôi đã viết một ví dụ bổ sung bao gồm các lời hứa như một phần của vòng lặp và chỉ ra cách chờ phản hồi trước khi tiếp tục. ví dụ: gist.github.com/gngenius02/…
cigol vào ngày

Tôi yêu những gì bạn đã làm ở đó (và hy vọng bạn không phiền nếu tôi lấy nó được cắt cho hộp công cụ của tôi). Tôi chủ yếu sử dụng mã đồng bộ, đó là lý do tại sao tôi thường thích các vòng lặp. Nhưng đó cũng là một viên ngọc quý mà bạn có được ở đó, và rất có thể sẽ tìm được đường đến máy chủ tiếp theo mà tôi làm việc
Werlious

2

Trước:

đối với tôi, chương trình có ngăn xếp cuộc gọi Max không phải do mã của tôi. Cuối cùng, nó là một vấn đề khác gây ra tắc nghẽn trong luồng của ứng dụng. Vì vậy, bởi vì tôi đã cố gắng thêm quá nhiều mục vào mongoDB mà không có bất kỳ cấu hình nào nên sự cố ngăn xếp cuộc gọi đang xuất hiện và tôi phải mất vài ngày để tìm hiểu điều gì đang xảy ra .... điều đó nói:


Theo sau những gì @Jeff Lowery đã trả lời: Tôi rất thích câu trả lời này và nó đã đẩy nhanh quá trình tôi đang làm ít nhất là 10 lần.

Tôi mới học lập trình nhưng tôi đã cố gắng mô-đun hóa câu trả lời đó. Ngoài ra, không thích lỗi bị ném ra nên thay vào đó tôi đã quấn nó trong một vòng lặp do while. Nếu bất cứ điều gì tôi đã làm là không chính xác, xin vui lòng sửa chữa cho tôi.

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

Kiểm tra ý chính này để xem các tệp của tôi và cách gọi vòng lặp. https://gist.github.com/gngenius02/3c842e5f46d151f730b012037ecd596c


1

Nếu bạn không muốn triển khai trình bao bọc của riêng mình, bạn có thể sử dụng hệ thống hàng đợi, ví dụ: async.queue , queue .


1

Tôi đã nghĩ đến một cách tiếp cận khác bằng cách sử dụng tham chiếu hàm giới hạn kích thước ngăn xếp cuộc gọi mà không sử dụng 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;
}

đầu ra:

$ 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

Về việc tăng kích thước ngăn xếp tối đa, trên máy 32 bit và 64 bit, mặc định phân bổ bộ nhớ của V8 lần lượt là 700 MB và 1400 MB. Trong các phiên bản V8 mới hơn, giới hạn bộ nhớ trên hệ thống 64 bit không còn được thiết lập bởi V8, về mặt lý thuyết cho thấy không có giới hạn. Tuy nhiên, Hệ điều hành (Hệ điều hành) mà Node đang chạy trên đó luôn có thể giới hạn dung lượng bộ nhớ mà V8 có thể sử dụng, do đó không thể nói chung giới hạn thực sự của bất kỳ quá trình nhất định nào.

Mặc dù V8 cung cấp --max_old_space_sizetùy chọn có sẵn , cho phép kiểm soát lượng bộ nhớ có sẵn cho một quy trình , chấp nhận một giá trị tính bằng MB. Nếu bạn cần tăng phân bổ bộ nhớ, chỉ cần chuyển tùy chọn này giá trị mong muốn khi tạo một quy trình Node.

Nó thường là một chiến lược tuyệt vời để giảm phân bổ bộ nhớ khả dụng cho một phiên bản Node nhất định, đặc biệt là khi chạy nhiều phiên bản. Cũng như giới hạn ngăn xếp, hãy cân nhắc xem liệu nhu cầu bộ nhớ lớn có được ủy quyền tốt hơn cho lớp lưu trữ chuyên dụng, chẳng hạn như cơ sở dữ liệu trong bộ nhớ hoặc tương tự hay không.


0

Vui lòng kiểm tra xem chức năng bạn đang nhập và chức năng bạn đã khai báo trong cùng một tệp có trùng tên hay không.

Tôi sẽ cung cấp cho bạn một ví dụ cho lỗi này. Trong JS nhanh (sử dụng ES6), hãy xem xét tình huống sau:

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

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

Trường hợp trên sẽ gây ra lỗi RangeError nổi tiếng : Lỗi vượt quá kích thước ngăn xếp cuộc gọi tối đa vì hàm liên tục gọi chính nó nhiều lần đến mức hết ngăn xếp lệnh gọi tối đa.

Hầu hết các lần lỗi là trong mã (giống như ở trên). Cách giải quyết khác là tăng ngăn xếp cuộc gọi theo cách thủ công. Chà, điều này hiệu quả đối với một số trường hợp cực đoan nhất định, nhưng nó không được khuyến khích.

Hy vọng câu trả lời của tôi đã giúp bạn.


-4

Bạn có thể sử dụng vòng lặp for.

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}không có cú pháp JS hợp lệ. Làm thế nào điều này có liên quan đến câu hỏi?
musemind
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.