20

高負荷下で実行されているノード環境で問題が発生しており、その原因を突き止めることができませんでした。

ちょっとした背景: 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 を信じていない人に、これが素晴らしい製品であることを示したいと思います!

4

2 に答える 2

3

実稼働環境の 1 つで同じ問題に直面しました。分析中に、次のことがわかりました。間違っている可能性があります。しかし、これがあなたの助けになることを願っています...

この問題は基本的にソケットに関連しています。開いているソケット接続の数を受け入れるオプションがありますか? 接続を半分開いたままにできますか?

通常、この種の例外は、特定の期間にサーバーにアクセスする頻度が高いためにのみ発生します。

分かりやすく説明して...

  1. ソケット パスが 2 つしかなく、リクエストが 4 つあり、それぞれに 5 秒の処理時間がかかるとします。

  2. 一般に、0 秒目に 2 つのリクエストを送信し、6 秒目に残りの 2 つのリクエストを送信すると、NodeJ は完全に機能します。

  3. このようにではなく、0 秒目に 4 つのリクエストを送信すると、NodeJ は 2 つのリクエストのみを処理する準備が整います。NodeJs は、残りの 2 つの要求に対して単純にソケットを閉じます。注: 後で同じリクエストを送信すると、NodeJs はそれを受け入れてレスポンスを返します。

  4. 詳細については、socket.io.js の実装を参照してください。

そして私の解決策は、

  1. サーバーフレンドリーな方法でロードバランサーを作成します。
  2. ロード バランサーで NodeJs インスタンスまたはクラスターを実行します。

または、これを解決する他の簡単な方法を見つけた場合は、この投稿を更新してください...

この問題の優れた解決策を知りたいと思っています。

ありがとう

于 2012-12-28T07:33:19.823 に答える
2

自分の投稿を更新して、修正内容を説明しようと思いました。

私が他のすべての方法を知っていることに気付いた後、ソリューションは次のようにして現れました。

Expressバージョン3をインストールします

コアコードに加える必要のある多くの違いや変更があったため、変換を行うだけで1日かかりました。ただし、そうすることで、各ルートの:param変数にヘルパーをアタッチするための.paramメソッドを含む、多くの新しいv3機能を利用することができました。これにより、古い「ヘルパー」関数のいくつかが削除されたため、ルートをチェーンする代わりに、それを使用しました。

ルート/ミドルウェアを完全に理解したので、Express v3用に書き直すだけで、問題は解決しました。

これは正確な答えではないので、これらは私が変換を行う方法を学ぶために使用したものです:

Expressv3APIリファレンス

ルートの仕組みに関する情報

素晴らしいHOWTOドキュメント!それらの人に感謝します!

于 2013-01-02T19:16:24.640 に答える