node.js / Express在高负载下抛出'RangeError:超出最大调用堆栈大小'

我们在高负载下运行的节点环境出现了一个问题,我们无法find源。

有一点背景:我们使用Express来为http框架运行一个集群节点应用程序。 目前有3个盒子,每个盒子有8个CPU核心,每个盒子都有6个节点的工作人员。 设置似乎很好,我研究了所有build议的方法,我相信这个设置是可靠的。 我们使用Express 2.5.11和XMLHttpRequest 1.4.2运行node.js 0.8.1。

问题在于:我们正在对该产品进行“黑暗启动”testing(即浏览器客户端代码在后台调用了API ajax API,但未在页面上使用或向用户显示)。 几分钟运行成功后,系统抛出:

[RangeError: Maximum call stack size exceeded] 

我们通过集群控制器中的“uncaughtException”事件捕获错误(启动每个工作者),但是在该级别没有可用的堆栈跟踪。 我已经在这个问题上进行了广泛的研究,似乎无法find有类似错误的人。 在梳理系统中的每一行代码之后,下面是我所知道的:

  • 我找不到任何recursion或循环引用。 (我读过这个错误并不总是意味着recursion问题,但是我们已经检查过;实际上我们已经通过去除大部分代码来运行testing,它仍然会发生,见下文)。
  • 我已经下了一个工作进程每箱试图消除集群作为一个问题 – 问题仍然存在;
  • 这个问题只发生在高负荷下。 我们的stream量是约。 每秒1500页,在交通繁忙的时候,可以达到每秒15000页(我们无法在开发环境中复制)。
  • 发现错误的时间不同,但通常在15分钟内。
  • 错误似乎不影响操作! 我的意思是没有腐败的反应,除了偶尔的超时之外,系统不会崩溃;
  • 陷阱错误的工作进程恢复并在几秒钟后重新开始服务请求;
  • 我得到了最基本的devise上的错误 – 没有额外的API被称为。 只需要一个请求,并用简单的json响应来回应。 这是最奇怪的部分。 看起来系统在我的任何代码中都没有失败 – 没有实例化任何类来做真正的工作,这是失败的。 很显然,我从更多的代码开始,但是慢慢地拿出了一些代码,直到在一个简单的设置下仍然失败。

我相信最明显的症状是错误总是在请求被完全服务之后发生。 也就是说,服务器接受请求,find正确的Express路由,调用res.send,并完成。 这真的感觉就像垃圾收集给我! 我已经读过V8引擎有一个非常好的GC引擎,但是我想知道我们的重负载影响了多less。

正如我所说,即使在基本devise上,代码也会抛出错误。 取出了我们大部分的自定义代码,这是设置的基础。 对不起,我在这里切,所以不是所有的variables声明等将被包括,但代码的工作,所有的东西都在真正的代码:

群集控制器。 这是在命令行上启动的清理版本。

 cluster = require('cluster'); path = require('path'); fs = require('fs'); app = require('./nodeApi'); _ = require('underscore'); nodeUtil = require(./nodeUtil); process.on('uncaughtException', function(err) { var stamp; stamp = new Date(); console.log("***************************** Exception Caught, " + stamp); return console.log("Exception is:", err); }); if (cluster.isMaster) { if ((nodeUtil.isLiveServer() || nodeUtil.isCluster()) && process.env.IS_CLUSTER !== '0') { numCPUs = require("os").cpus().length - 2; if (numCPUs <= 0) { numCPUs = 1; } } else { numCPUs = 1; } console.log("Forking " + numCPUs + " workers..."); for (i = _i = 1; 1 <= numCPUs ? _i <= numCPUs : _i >= numCPUs; i = 1 <= numCPUs ? ++_i : --_i) { worker = cluster.fork(); } } else { app.start(); } 

nodeWorker代码。 使用Express和简单的路线来提供请求。 如果使用jsonp,请求被封装在一个callback中(对于我们用ajax进行testing,这是需要的)

 (function() { var crypto, express, fs, modroot, path, staticroot, _; express = require('express'); _ = require('underscore'); fs = require('fs'); path = require('path'); module.exports.start = function() { logFile = fs.createWriteStream("" + logpath + "/access.log", { flags: 'a' }); app = express.createServer(); app.configure(function() { app.use(express.logger({ stream: logFile, format: ':remote-addr - [:date] - ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" :response-time ms' })); app.use(express.errorHandler({ dumpExceptions: true, showStack: true })); app.use(express.cookieParser()); app.use(express.bodyParser()); app.use(express.session({ secret: "ourMemStoreSecret", cookie: { domain: ".ourdomain.com" }, maxAge: new Date(Date.now() + 7200000), // The store WAS a redis store. I took it out to eliminate redis as the issue. We don't use sessions anyway. store: new require('express').session.MemoryStore({ reapInterval: 60000 * 15 }) })); app.use(express["static"](staticroot)); app.set('view engine', 'underscore'); // For our template rendering. Not used in this test. app.set('views', __dirname + '/views/src'); app.set('view options', { layout: false }); app.use(app.router); }); ignore = function(req, res, next) { if (req.params.api === 'favicon.ico') { return next('route'); } return next(); }; wrapCallback = function(req, res, next) { var callbackName; if (callbackName = req.query.callback) { req.wrapCallback = true; res._send = res.send; res.send = function(data, status) { var dataString; if (_.isObject(data)) { dataString = encodeURI(JSON.stringify(data)); res.setHeader('Content-Type', 'application/javascript'); return res._send("" + callbackName + "(\"" + dataString + "\")", status); } else { data = encodeURI(data); return res._send("" + callbackName + "(\"" + data + "\")", status); } }; } return next(); }; app.error(function(err, req, res, next) { console.log("[" + process.pid + "] Error Handler. Ok.", err); return res.send({ error: err.msg }, err.statusCode); }); // Does anyone know how to hard-code a path AND put it into a variable at the same time? // Kind of like: "/:api=MyTestAPI" ?? That's why this route is here. setAPIName = function(req, res, next) { req.params.api = 'MyTestAPI'; return next(); }; app.get("/MyTestAPI", setAPIName, wrapCallback, function(req, res) { res.send({ hello: 'world' }, 200); return console.log("[" + process.pid + "] res.send (no cacher) is done"); }); process.setMaxListeners(0); process.send({ // For IPC - the controller has a handler for this message cmd: 'isStarted' }); return app.listen(process.env.APP_PORT); }; }).call(this); 

什么错误看起来像。 基本上,我从来没有看到它发生在一个请求的中间。 错误上也没有调用堆栈 – 它只是堆栈溢出消息。 在这里你可以看到2个工作进程每个服务一个响应,然后在其中之一的错误。

 [660] res.send (no cacher) is done [654] res.send (no cacher) is done ***************************** Exception Caught, Fri Nov 02 2012 10:23:48 GMT-0400 (EDT) 

我真的很感激这个反馈。 该系统运行精美,能够处理我们与3箱巨大的交通。 盒子上的负载约为40%,嗡嗡作响。 我很想find这个问题的根源,所以其他人可以像我一样为这个系统感到自豪,并向node.js非信徒展示这是一个伟大的产品!

我在我的一个生产环境中遇到同样的问题。 在分析过程中,我发现下面的事情,可能是我错了。 但是我希望这会帮助你

这个问题基本上和Socket有关。 有一个选项打开Socket连接应该接受多less个? 可以保持一半的连接?

通常情况下,这种例外的发生只是因为您在特定的时间段内访问服务器的频率。

让我解释清楚…

  1. 假设只有两个套接字path,并且您有四个请求,每个请求都需要5秒的处理时间。

  2. 一般来说,NodeJ可以在第0秒发出2个请求,而在第6秒发送2个请求时完美地发挥作用。

  3. 而不是像这样,如果你在0秒给出4个请求,那么NodeJs只准备服务2个请求。 其余的两个请求NodeJs只是closures套接字。 注意:稍后如果您提出相同的请求,NodeJs将接受并给出响应。

  4. 有关更多信息,请通过socket.io.js实现。

而我的解决scheme是,

  1. 以服务器友好的方式创build负载平衡器。
  2. 在负载均衡器下运行NodeJs实例或集群。

或者,如果您发现任何其他简单的方法来解决这个问题,请更新此post…

我正在等待这个问题的一个很好的解决scheme。

谢谢

我想我会更新我自己的post,以解释什么是修复是我的。

在意识到自己已经完成了所有我知道该怎么做的事情之后,解决scheme就是这样做的:

安装Express版本3

核心代码需要做太多的差异和变化才能完成转换。 然而,这样做的话,我可以利用许多新的v3特性,包括.param方法将helpers附加到每个路由中的:paramvariables。 这消除了我的一些旧的“帮手”function,所以不是通过路线链接,而是使用它。

我现在对路由/中间件有了全面的了解,只要重写Express v3,我的问题就消失了!

由于这不是一个确切的答案,这些是我用来学习如何进行转换的东西:

Express v3 API参考

有关如何路线工作的信息

令人敬畏的HOWTO文档! 感谢那些家伙!