高負荷下で実行されているノード環境で問題が発生しており、その原因を突き止めることができませんでした。
ちょっとした背景: http フレームワークに Express を使用して、クラスター化されたノード アプリケーションを実行しています。現在、それぞれに 8 個の CPU コアを搭載した 3 つのボックスがあり、各ボックスは 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 つのワーカー プロセスに減らしましたが、問題は引き続き発生します。
- この問題は、高負荷時にのみ発生します。私たちのトラフィックは約です。1 秒あたり 1500 ページであり、トラフィックが多い時間帯には 1 秒あたり 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();
}
nodeWorker コード。 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 つのワーカー プロセスと、そのうちの 1 つのエラーを確認できます。
[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 を信じていない人に、これが素晴らしい製品であることを示したいと思います!