blog

マルチプロセシングとNode.jsの実装

複数のプロセスを複製することによるメモリの浪費の問題を避けるために、ウェブサーバのモデルにマルチスレッドが導入されました。1つのスレッドが1つのユーザのリクエストに応答し、スレッドはプロセスのメモリを...

Aug 24, 2020 · 8 min. read
シェア

プロセスとスレッド

プロセスとスレッドの誕生は、マルチタスクから話をする、マルチタスクは、オペレーティングシステムが同時に複数のアプリケーションを実行することができますを指し、CPUはコードを順次実行し、同時に1つのタスクを処理することができ、主流のオペレーティングシステムのシングルコア時代にマルチタスク機能を持って、主に迅速に複数のタスクの間で切り替えることによって、複数のタスクが同時に実行されている印象を与えるために



プロセスとは、オペレーティング・システムによって実行されるアプリケーションのことで、1つのプロセスには複数のサブタスク(スレッド)が同時に存在します。

Web サーバーモデル

ウェブサーバーは同時に複数のユーザーリクエストを処理し、ユーザーにレスポンス内容を返す必要があり、マルチタスクを達成するためにいくつかの異なるサーバーモデルがあります。

マルチプロセス・シングルスレッド

このサービスモデルでは、各リクエストを別々のプロセスで処理するプロセスレプリケーションによって、複数のリクエストに対する同時応答を実現していますが、オペレーティングシステムのレプリケーションプロセスでは、プロセスの内部状態を複製する必要があるため、同じ状態がメモリ上に複数存在することになり、一定のメモリオーバーヘッドが発生します。また、同時に処理できるリクエスト数とメモリサイズには正の相関があります。

複数のスレッドを持つ単一プロセス

マルチプロセッシングによるメモリ浪費問題の再現を避けるため、ウェブサーバモデルにマルチスレッドが導入されました。 スレッドはプロセスのメモリを共有できるため、メモリ浪費が発生せず、同時にスレッドはプロセスに比べてメモリオーバーヘッドが非常に小さくなります。しかし、各スレッドはそれぞれ独立したスタックを持ち、一定量のメモリ空間を占有する必要があるため、マルチプロセッシングによって引き起こされるリソースの浪費の問題を緩和するだけです。



さらに、オペレーティングシステムはスレッドを切り替える際にスレッドのコンテキストを切り替える必要があり、スレッドの数が多すぎると、コンテキスト切り替えにCPUが消費されます。同時に、スレッドのクラッシュはプロセス全体のクラッシュを引き起こす可能性があり、サーバーにかなりの安定性リスクをもたらします。

マルチプロセッシングとマルチスレッド

その名前が示すように、マルチプロセス・マルチスレッディング・モデルは、複数のプロセスと各プロセス内の複数のスレッドを有効にして、並行性の高い問題を解決するもので、マルチプロセス・モデルとマルチスレッディング・モデルの両方の利点を統合していますが、ユーザー数が十分に多い場合には、他の2つのモデルの欠点もあります。



同時実行の数が1000万に達すると、メモリの問題の良い使用が露出され、これは有名なC10k問題であり、C10k問題の本質は次のとおりです:あまりにも多くのプロセスやスレッドを作成するには、高い同時実行に対処するために、データのコピーが頻繁に、プロセス/スレッドのコンテキストスイッチングは、オペレーティングシステムのクラッシュの多くを消費します!

イベントドライバー

ためにWeb Nginxは、イベント駆動型のモデルを使用して、CPU上の単一のプロセスを使用して、高い並行性の問題を解決するために、1つのスレッドは、ユーザーのリクエストに応答するために、最も時間のかかるブロッキングタスクのI/Oタスク非同期、処理は、メインプロセスのイベント通知を通じて、ユーザーにI/Oタスクの要求を処理するために待っている次のリクエストに応答するために完了します。



このようなモデルの性能はCPUの演算能力に依存しますが、マルチプロセス・マルチスレッドモードではリソースの上限に影響されないため、I/Oが集中するWebの特性に非常に適しており、Webサーバーの主流モデルとなっています。

master-worker

Node.js自体はイベントドリブンモデルを採用しており、シングルスレッドによる単一プロセスでマルチコアを十分に利用できないという問題を解決するために、CPU数に応じて複数のプロセスを起動することが可能で、理想的には1つのプロセスで1つのCPUを利用することができます。



child_process.fork(modulePath) Node.js はマルチプロセッシングをサポートする child_process モジュールを提供しており、このメソッドを使用して指定されたモジュールを呼び出し、新しい Node.js プロセスを生成することができます。

worker.js
const http = require('http');
const randomPort = parseInt(Math.random() * 10000) 
http.createServer((req, res) => {
 res.end('Hello world')
}).listen(randomPort);
master.js
const { fork } = require('child_process');
const os = require('os');
for (let i = 0, len = os.cpus().length; i < len; i++) {
 fork('./worker.js');
}
undefined 5271 4931720 21584 0:00.13 /usr/local/bin/node ./worker.js
undefined 5270 4931720 21624 0:00.13 /usr/local/bin/node ./worker.js
undefined 5269 4931720 21640 0:00.13 /usr/local/bin/node ./worker.js
undefined 5268 4931720 21636 0:00.12 /usr/local/bin/node ./worker.js
undefined 5267 4931720 21616 0:00.13 /usr/local/bin/node ./worker.js
undefined 5266 4931720 21696 0:00.12 /usr/local/bin/node ./worker.js
undefined 5265 4931720 21648 0:00.13 /usr/local/bin/node ./worker.js
undefined 5264 4931720 21640 0:00.12 /usr/local/bin/node ./worker.js

これはマスター-ワーカーモデルであり、マスタープロセスがワークプロセスのスケジューリングと管理を担当し、ワーカープロセスが特定のビジネスロジックを担当します。

プロセス通信

マスタプロセスは作業プロセスを管理し、多くの場合ワーカープロセスと通信する必要があります。 child_process レプリケートプロセスを介してマスタプロセスと通信するには、WebWorker API を使用できます。

worker.js
const http = require('http');
const randomPort = parseInt(Math.random() * 10000);
http.createServer((req, res) => {
 res.end('Hello world')
}).listen(randomPort);
process.on('message', msg => {
 console.log(`worker get message: ${msg}`);
});
process.send(`${randomPort} ready`);
master.js
const { fork } = require('child_process');
const os = require('os');
for (let i = 0, len = os.cpus().length; i < len; i++) {
 const worker = fork('./worker.js');
 worker.on('message', msg => {
 console.log(`master get message: ${msg}`);
 });
 worker.send('ok');
}

ハンドルの受け渡し

上記の例では、ワーカープロセスはそれぞれランダムなポートを使っています。

Error: listen EADDRINUSE :::9527
 at Server.setupListenHandle [as _listen2] (net.js:1360:14)
 at listenInCluster (net.js:1401:12)
 at Server.listen (net.js:1485:7)

この問題は、マスターがポート 80 を listen していて、リクエストをワーカープロセスに分散することで解決できます。

上記のモデルでは、プロキシ・サービスを使用するため、各接続に対して2つのファイル記述子を使用します。 オペレーティング・システムのファイル記述子の数には限りがあるため、プロキシ・スキームは2倍のファイル記述子を無駄にし、システムのスループットに影響を与えます。



この問題を解決するために、マスターはワーカープロセスにハンドルを送ることができます。

send(message, handler);

これは、マスタープロセスがリクエストを受け取り、ワーカーに接続するために新しいソケットを作成することなく、ソケットを直接ワーカーに送信することを意味します。

master.js
const { fork } = require('child_process');
const net = require('net');
const os = require('os');
const workers = [];
for (let i = 0, len = os.cpus().length; i < len; i++) {
 const worker = fork('./worker.js');
 workers.push(worker);
}
const server = net.createServer();
server.listen(9527, () => {
 workers.forEach(worker => {
 worker.send('SERVER', server);
 });
 server.close();
});

マスタープロセスでtcpサーバーを作成し、ポート9527でリッスンし、すべてのワーカーにtcpサーバーを送信します。

worker.js
const http = require('http');
// ポート番号をリッスンせずにhttpサーバーを作成する
const httpServer = http.createServer((req, res) => {
 res.end(`Hello world by ${process.pid}
`);
});
process.on('message', (msg, tcpServer) => {
 // tcpサーバーがマスターから渡された場合
 if (msg === 'SERVER') {
 // 新しい接続が確立されたときにトリガーされる
 tcpServer.on('connection', socket => {
 // tcpサーバ接続をhttpサーバに転送する
 httpServer.emit('connection', socket);
 });
 }
});

なぜ複数のプロセスが同じポート番号をリッスンしても、これを書いた後にEADDRINUSEエラーを報告しないのですか?



Node.jsは、SO_REUSEADDRオプションを各ポートでリッスンするように設定し、異なるプロセスが同じポート番号でリッスンできるようにします。

setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

独立して起動したプロセス・サーバーのソケットのファイル・ディスクリプタは異なるので、同じポート番号でリッスンしても失敗します。一方、上記のコードのソケットはすべてマスターから送られたソケットを使用しているので、正常にリッスンできます。



複数のアプリケーションが同じポート番号をリッスンしている場合、ファイル記述子は一度に1つのプロセスによってのみ占有されます。つまり、ネットワークリクエストがサーバーに送信されると、1つのプロセスだけがリクエストに対応するためにプリエンプトされます。

安定性

マスターとワーカー間の通信メカニズムにより、マスターはワーカーを管理することができます。

worker 自動再起動

master.js
const { fork } = require('child_process');
const net = require('net');
const os = require('os');
const workers = {};
function createWorker(server) {
 const worker = fork('./worker.js');
 worker.send('SERVER', server);
 workers[worker.pid] = worker;
 console.log(`worker ${worker.pid} created`);
 worker.on('exit', () => {
 // worker プロセスが終了し、自動的に再作成される
 console.log(`worker ${worker.pid} exited`);
 delete workers[worker.pid];
 createWorker(server);
 });
}
const server = net.createServer();
server.listen(9527);
for (let i = 0, len = os.cpus().length; i < len; i++) {
 createWorker(server);
}

master shutdown ワーカーの自動シャットダウン

master.js
process.on('exit', () => {
 for (const pid in workers) {
 workers[pid].kill();
 }
});

cluster

上記は、Node.js 組み込みのモジュールクラスタで実現できます。

const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;
if (cluster.isMaster) {
 console.log(`マスター${process.pid} 実行中`);
 // ワーカープロセスを派生させる
 for (let i = 0; i < numCPUs; i++) {
 cluster.fork();
 }
 cluster.on('exit', (worker, code, signal) => {
 console.log(`ワーカープロセス${worker.process.pid}  `);
 });
} else {
 // ワーカープロセスは、任意の TCP 接続(この場合は HTTP サーバー)を共有できる。
 http.createServer((req, res) => {
 res.writeHead(200);
 res.end('ハローワールド
');
 }).listen(8000);
 console.log(`ワーカープロセス${process.pid}  `);
}

cluster

クラスタモジュールは、開発者がよりカスタマイズしやすいように、いくつかのイベントも公開しています。

  1. disconnect: ワーカープロセスの IPC パイプラインが切断された後にトリガーされます。
  2. exit: クラスタモジュールは、ワーカープロセスがシャットダウンしたときに起動されます。
  3. fork: クラスタモジュールは、新しいワーカープロセスが生成されたときに起動されます。
  4. リスニング: ワーカープロセスが listen() を呼び出すと、マスタープロセスは
  5. message: クラスタマスタプロセスが任意のワーカープロセスからメッセージを受信したときにトリガされます。
  6. online: 新しいワーカープロセスが生成されると、ワーカープロセスはオンラインメッセージで応答する必要があります。 このイベントは、マスタープロセスがオンラインメッセージを受信したときにトリガーされます。
  7. setup: .setupMaster() が呼び出されたときに発生します。





Read next

ik セグメンテーション・ホットアップデートのためのpostgresデータベースへのリモート接続

ソースコードのダウンロードは--//.....ipでソースコードをダウンロードし、解凍してideaにダンプしてパッケージのダウンロードを待ちます。pom.xmlファイルの赤字は気にしないでください。

Aug 24, 2020 · 5 min read