我们的节点环境在高负载下运行时遇到问题,我们无法找到其来源。
一点背景知识:我们正在使用 Express 为 http 框架运行一个集群节点应用程序。目前,有 3 个盒子,每个盒子有 8 个 CPU 内核,每个盒子运行一个由 6 个节点工作人员组成的集群。设置似乎效果很好,我研究了所有建议的方法,因此我相信设置是可靠的。我们使用 Express 2.5.11 和 XMLHttpRequest 1.4.2 运行 node.js 0.8.1。
问题是:我们正在对该产品进行“暗启动”测试(即浏览器客户端代码在后台对我们的 API 进行 javascript ajax 调用,但未在页面上使用或向用户显示)。成功运行几分钟后,系统抛出:
[RangeError: Maximum call stack size exceeded]
我们在集群控制器(启动每个工作程序)中的“uncaughtException”事件中捕获了错误,但是在该级别没有可用的堆栈跟踪。我已经对这个问题进行了广泛的研究,似乎找不到任何有类似错误的人。在梳理了系统中的每一行代码之后,这就是我所知道的:
- 我找不到任何递归或循环引用。(我读过这个错误并不总是意味着递归问题,但我们已经检查过;我们实际上已经通过删除大部分代码来运行测试,但它仍然会发生,见下文);
- 我已经将每个盒子减少到 1 个工作进程,以尝试将集群作为一个问题消除——问题仍然存在;
- 该问题仅在高负载下发生。我们的流量约为。每秒 1500 页,在交通繁忙的时候,可以达到每秒 15000 页(我们无法在开发环境中复制);
- 捕获错误的时间各不相同,但通常在 15 分钟内;
- 该错误似乎不会影响操作!我的意思是没有损坏的响应,除了偶尔的超时,系统永远不会崩溃;
- 捕获错误的工作进程恢复并在几秒钟后再次开始服务请求;
- 我在最基本的设计上发生了错误——没有调用额外的 API。只需接受请求并使用简单的 json 响应进行响应。这是最令人好奇的部分。似乎系统在我的任何代码中都没有失败——它在没有实例化任何类来完成实际工作的情况下失败了。显然,我从更多的代码开始,但慢慢地取出部分,直到它在一个简单的设置下仍然失败。
我相信,最明显的症状是错误总是在请求得到完全处理之后发生。也就是说,服务器接受一个请求,找到正确的 Express 路由,调用 res.send,然后完成。这对我来说真的感觉像是垃圾收集!我读过 V8 引擎有一个非常好的 GC 引擎,但我想知道我们的重负载对事物的影响有多大。
正如我所说,即使在基本设计上,代码也会引发错误。去掉了我们的大部分自定义代码,这是设置的基础。抱歉,我在这里删减了,因此并非所有变量声明等都将包括在内,但是代码确实有效,并且所有这些内容都在真实代码中:
集群控制器。这是在命令行上启动的内容的清理版本。
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();
}
节点工作代码。 使用 Express 和简单的路由来处理请求。如果使用 jsonp,则请求被包装在回调中(对于我们使用 ajax 进行的测试,这是必需的)
(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% 并且嗡嗡作响。我很想找到这个问题的根源,这样其他人就可以像我一样为这个系统感到自豪,并向不相信 node.js 的人展示这是一个很棒的产品!