Node.JS プロセスは単一の物理コア上でのみ実行されるため、スケーラブルなサーバーを開発する場合は特別な注意を払う必要があります。
プロセスを管理するための API の安定したセットとネイティブ拡張機能の開発があるため、並列化できる Node.JS アプリケーションを設計するさまざまな方法があります。このブログ投稿では、これらの考えられるアーキテクチャを比較します。
この記事では、compute-cluster モジュールについても紹介します。これは、プロセスを簡単に管理し、第 2 ラインの分散コンピューティングを実装するために使用できる小さな Node.JS ライブラリです。
問題が発生しました
Mozilla ペルソナ プロジェクトでは、さまざまな特性を持つ多数のリクエストを処理できる必要があるため、Node.JS の使用を試みました。
ユーザー エクスペリエンスに影響を与えないように、私たちが設計した「インタラクティブ」リクエストは軽量なコンピューティング消費のみを必要としますが、UI が滞りなく感じられるよう、より高速な応答時間を提供します。比較すると、「バッチ」操作の処理には約 0.5 秒かかりますが、他の理由により遅延が長くなる可能性があります。
より良い設計を実現するために、私たちは現在のニーズを満たす多くのソリューションを見つけました。
スケーラビリティとコストを考慮して、次の主要な要件をリストします:
上記のポイントにより、明確かつ目的を持ってフィルタリングできます
オプション 1: メインスレッドで直接処理します。
メインスレッドがデータを直接処理すると、結果は非常に悪くなります:
対話型のリクエスト/レスポンスでは、現在のリクエスト (またはレスポンス) が処理されるまで待つ必要があり、マルチコア CPU を最大限に活用することはできません。
このソリューションの唯一の利点は、非常にシンプルであることです
function myRequestHandler(request, response) [ // Let's bring everything to a grinding halt for half a second. var results = doComputationWorkSync(request.somesuch); }
Node.JS プログラムで、複数のリクエストを同時に処理し、それらを同期的に処理したい場合、問題が発生します。
方法 2: 非同期処理を使用するかどうか。
バックグラウンドで非同期メソッドを使用すると、パフォーマンスが大幅に向上しますか?
答えは必ずしもそうではありません。バックグラウンドで実行することが意味があるかどうかによって異なります。
たとえば、次の状況では、メインスレッドで JavaScript またはローカル コードを使用して計算を実行するときに、パフォーマンスが同期処理よりも優れていない場合は、<🎜 の処理にバックグラウンドで非同期メソッドを使用する必要は必ずしもありません。 >
次のコードをお読みください
function doComputationWork(input, callback) { // Because the internal implementation of this asynchronous // function is itself synchronously run on the main thread, // you still starve the entire process. var output = doComputationWorkSync(input); process.nextTick(function() { callback(null, output); }); } function myRequestHandler(request, response) [ // Even though this *looks* better, we're still bringing everything // to a grinding halt. doComputationWork(request.somesuch, function(err, results) { // ... do something with results ... });
}
关键点就在于NodeJS异步API的使用并不依赖于多进程的应用
方案三:用线程库来实现异步处理。
只要实现得当,使用本地代码实现的库,在 NodeJS 调用的时候是可以突破限制从而实现多线程功能的。
有很多这样的例子, Nick Campbell 编写的 bcrypt library 就是其中优秀的一个。
如果你在4核机器上拿这个库来作一个测试,你将看到神奇的一幕:4倍于平时的吞吐量,并且耗尽了几乎所有的资源!但是如果你在24核机器上测试,结果将不会有太大变化:有4个核心的使用率基本达到100%,但其他的核心基本上都处于空闲状态。
问题出在这个库使用了NodeJS内部的线程池,而这个线程池并不适合用来进行此类的计算。另外,这个线程池上限写死了,最多只能运行4个线程。
除了写死了上限,这个问题更深层的原因是:
内建线程机制的组件库在这种情况下并不能有效地利用多核的优势,这降低了程序的响应能力,并且随着负载的加大,程序表现越来越差。
方案四:使用 NodeJS 的 cluster 模块
NodeJS 0.6.x 以上的版本提供了一个cluster模块 ,允许创建“共享同一个socket”的一组进程,用来分担负载压力。
假如你采用了上面的方案,又同时使用 cluster 模块,情况会怎样呢?
这样得出的方案将同样具有同步处理或者内建线程池一样的缺点:响应缓慢,毫无优雅可言。
有时候,仅仅添加新运行实例并不能解决问题。
方案五:引入 compute-cluster 模块
在 Persona 中,我们的解决方案是,维护一组功能单一(但各不相同)的计算进程。
在这个过程中,我们编写了 compute-cluster 库。
这个库会自动按需启动和管理子进程,这样你就可以通过代码的方式来使用一个本地子进程的集群来处理数据。
使用例子:
const computecluster = require('compute-cluster'); // allocate a compute cluster var cc = new computecluster({ module: './worker.js' }); // run work in parallel cc.enqueue({ input: "foo" }, function (error, result) { console.log("foo done", result); }); cc.enqueue({ input: "bar" }, function (error, result) { console.log("bar done", result); });
fileworker.js 中响应了 message 事件,对传入的请求进行处理:
process.on('message', function(m) { var output; // do lots of work here, and we don't care that we're blocking the // main thread because this process is intended to do one thing at a time. var output = doComputationWorkSync(m.input); process.send(output); });
呼び出しコードを変更せずに、compute-cluster モジュールを既存の非同期 API と統合できるため、最小限のコードで真のマルチコア並列処理を実現できます。
このソリューションのパフォーマンスを 4 つの側面から見てみましょう。
マルチコア並列機能: 子プロセスはすべてのコアを使用します。
応答性: コア管理プロセスは子プロセスの開始とメッセージの配信のみを担当するため、ほとんどの時間はアイドル状態であり、より対話的なリクエストを処理できます。
マシンに大きな負荷がかかっている場合でも、オペレーティング システムのスケジューラを使用して、コア管理プロセスの優先順位を上げることができます。
シンプルさ: 非同期 API を使用して、特定の実装の詳細を隠すことができ、呼び出しコードを変更することなく、このモジュールを現在のプロジェクトに簡単に統合できます。
では、負荷が急激に増加した場合でも、システムの効率が異常に低下しないようにする方法を見つけてみましょう。
もちろん、圧力が急増した場合でも、システムが効率的に動作し、できるだけ多くのリクエストを処理できることが最善の目標に変わりはありません。
優れたソリューションの実装を支援するために、compute-cluster は子プロセスを管理してメッセージを渡すだけでなく、他の情報も管理します。
現在実行中の子プロセスの数と、各子プロセスが完了するまでにかかる平均時間を記録します。
これらの記録を使用して、子プロセスが開始されるまでにどれくらいの時間がかかるかを予測できます。
これによれば、ユーザーが設定したパラメーター (max_request_time) と組み合わせることで、処理せずにタイムアウトする可能性のあるリクエストを直接閉じることができます。
この機能により、ユーザー エクスペリエンスに基づいたコードを簡単に作成できます。たとえば、「ユーザーはログインするまで 10 秒以上待機しないでください。」これは、max_request_time を 7 秒に設定するのとほぼ同じです (ネットワーク送信時間を考慮する必要があります)。
ペルソナ サービスのストレス テストを行った結果、非常に満足のいく結果が得られました。
非常にプレッシャーの高い状況下でも、認証されたユーザーにサービスを提供することができ、一部の非認証ユーザーをブロックし、関連するエラー メッセージを表示することもできました。