ホームページ  >  記事  >  ウェブフロントエンド  >  NodeJS におけるプロセス管理の詳細な分析

NodeJS におけるプロセス管理の詳細な分析

青灯夜游
青灯夜游転載
2022-07-14 20:56:052195ブラウズ

NodeJS におけるプロセス管理の詳細な分析

js に詳しい友人は、js がシングルスレッド であることを知っています。Node では、マルチプロセス シングルスレッド のモデルが使用されます。が採用されています。 JavaScript のシングルスレッドの制限により、マルチコア サーバーでは、サーバーのパフォーマンスを最大化するために複数のプロセスを開始する必要があることがよくあります。

Node.js プロセス クラスタリングを使用すると、アプリケーション スレッド間でワークロードを分散できる複数の Node.js インスタンスを実行できます。プロセスの分離が必要ない場合は、代わりに worker_threads モジュールを使用します。これにより、単一の Node.js インスタンス内で複数のアプリケーション スレッドを実行できるようになります。

ゼロ、NodeJS マルチプロセス

  • プロセスの総数、メインプロセスの 1 つ、CPU の数 x CPU コアの数、子プロセス
  • 子プロセスやクラスターに関係なく、それはマルチスレッド モデルではなく、マルチプロセス モデルです
  • #シングルスレッドの問題に対処するには、通常、マルチプロセスを使用してマルチスレッドをシミュレートします
1. コア モジュール クラスター クラスター

Node は、バージョン V0.8 以降

クラスター モジュール を導入し、1 つのマスター プロセス (マスター) 管理によるクラスタリングを実装します。複数の子プロセス (ワーカー)。 クラスター モジュールは、サーバー ポートを共有する子プロセスを簡単に作成できます。

クラスターの最下層は child_process モジュールです。通常のメッセージの送信に加えて、基礎となるオブジェクト
TCP

UDP なども送信できます。 , cluster このモジュールは、child_process モジュールと net モジュールを組み合わせたアプリケーションです。 クラスターが開始すると、TCP サーバーが内部で開始され、TCP サーバー側ソケットのファイル記述子がワーカー プロセスに送信されます。

cluster

モジュール アプリケーションでは、メイン プロセスはワーカー プロセスのグループのみを管理できます、その動作モードには がありません。 child_process モジュール 非常に柔軟ですが、より安定しています:

NodeJS におけるプロセス管理の詳細な分析1.クラスター構成の詳細

1.1 クラスターの概要

const cluster = require('cluster')复

1.2共通のクラスター属性

    .isMaster
  • はメイン プロセス、Node
  • .isPrimary
  • はメイン プロセス、Node>16 を識別します
  • .isWorker
  • 子プロセスを識別します
  • .worker
  • [子プロセス内の] 現在のワーカー プロセス オブジェクトへの参照
  • .workers
  • ストレージ id フィールドをキーとする、アクティブなワーカー プロセス オブジェクトのハッシュ。これにより、すべてのワーカー プロセスを簡単にループできるようになります。メインプロセスでのみ使用できます。 cluster.wokers[id] ===workers[メインプロセス内]
  • .settings
  • 読み取り専用のクラスター構成項目。 .setupPrimary() または .fork() メソッドを呼び出した後、この設定オブジェクトにはデフォルト値を含む設定が含まれます。以前は空のオブジェクトでした。このオブジェクトは手動で変更または設定しないでください。
cluster.settings

設定項目の詳細: <pre class="brush:php;toolbar:false">- `execArgv` &lt;string&gt;传给 Node.js 可执行文件的字符串参数列表。 **默认值:**  `process.execArgv`。 - `exec` &lt;string&gt; 工作进程文件的文件路径。 **默认值:** `process.argv[1]`。 - `args` &lt;string&gt; 传给工作进程的字符串参数。 **默认值:**`process.argv.slice(2)`。 - `cwd` &lt;string&gt;工作进程的当前工作目录。 **默认值:**  `undefined` (从父进程继承)。 - `serialization` &lt;string&gt;指定用于在进程之间发送消息的序列化类型。 可能的值为 `'json'` 和 `'advanced'`。  **默认值:**  `false`。 - `silent` &lt;boolean&gt;是否将输出发送到父进程的标准输入输出。 **默认值:**  `false`。 - `stdio` &lt;array&gt;配置衍生进程的标准输入输出。 由于集群模块依赖 IPC 来运行,因此此配置必须包含 `'ipc'` 条目。 提供此选项时,它会覆盖 `silent`。 - `uid` &lt;number&gt;设置进程的用户标识。  - `gid` &lt;number&gt;设置进程的群组标识。 - `inspectPort` &lt;number&gt; | &lt;function&gt; 设置工作进程的检查器端口。 这可以是数字,也可以是不带参数并返回数字的函数。 默认情况下,每个工作进程都有自己的端口,从主进程的 `process.debugPort` 开始递增。 - `windowsHide` &lt;boolean&gt; 隐藏通常在 Windows 系统上创建的衍生进程控制台窗口。 **默认值:**  `false`。&lt;/boolean&gt;&lt;/function&gt;&lt;/number&gt;&lt;/number&gt;&lt;/number&gt;&lt;/array&gt;&lt;/boolean&gt;&lt;/string&gt;&lt;/string&gt;&lt;/string&gt;&lt;/string&gt;&lt;/string&gt;</pre>1.3 クラスターの共通メソッド

    #.fork([env ] )
  • 新しいワーカー プロセスを [メイン プロセス内で] 生成します
  • .setupPrimary([settings])
  • Node>16
  • .setupMaster([ settings])
  • は、デフォルトの「フォーク」動作を変更するために使用されます。使用後、設定は cluster.settings に表示されます。設定の変更は、今後の .fork() への呼び出しにのみ影響し、既に実行されているワーカー プロセスには影響しません。上記のデフォルト値は最初の呼び出しにのみ適用されます。ノードは 16 未満です [メイン プロセス内]
  • .disconnect([callback])
  • すべてのワーカー プロセスが切断してハンドルを閉じるときに [メイン プロセス内で] 呼び出されます
  • 1.4 一般的なクラスター イベント

クラスターをより安定して堅牢にするために、

cluster

モジュールは多くのイベントも公開します:<ul> <li> <code>'message' 事件, 当集群主进程接收到来自任何工作进程的消息时触发。

  • 'exit' 事件, 当任何工作进程死亡时,则集群模块将触发 'exit' 事件。
  • cluster.on('exit', (worker, code, signal) => {
      console.log('worker %d died (%s). restarting...',
                  worker.process.pid, signal || code);
      cluster.fork();
    });
    • 'listening'事件,从工作进程调用 listen() 后,当服务器上触发 'listening' 事件时,则主进程中的 cluster 也将触发 'listening' 事件。
    cluster.on('listening', (worker, address) => {
      console.log(
        `A worker is now connected to ${address.address}:${address.port}`);
    });
    • 'fork' 事件,当新的工作进程被衍生时,则集群模块将触发 'fork' 事件。
    cluster.on('fork', (worker) => {
      timeouts[worker.id] = setTimeout(errorMsg, 2000);
    });
    • 'setup' 事件,每次调用 .setupPrimary()时触发。
    • disconnect事件,在工作进程 IPC 通道断开连接后触发。 当工作进程正常退出、被杀死、或手动断开连接时
    cluster.on('disconnect', (worker) => {
      console.log(`The worker #${worker.id} has disconnected`);
    });

    1.5 Worker类

    Worker 对象包含了工作进程的所有公共的信息和方法。 在主进程中,可以使用 cluster.workers 来获取它。 在工作进程中,可以使用 cluster.worker 来获取它。

    1.5.1 worker常用属性

    • .id 工作进程标识,每个新的工作进程都被赋予了自己唯一的 id,此 id 存储在 id。当工作进程存活时,这是在 cluster.workers 中索引它的键。
    • .process 所有工作进程都是使用 child_process.fork() 创建,此函数返回的对象存储为 .process。 在工作进程中,存储了全局的 process

    1.5.2 worker常用方法

    • .send(message[, sendHandle[, options]][, callback]) 向工作进程或主进程发送消息,可选择使用句柄。在主进程中,这会向特定的工作进程发送消息。 它与 ChildProcess.send()相同。在工作进程中,这会向主进程发送消息。 它与 process.send() 相同。
    • .destroy()
    • .kill([signal])此函数会杀死工作进程。kill() 函数在不等待正常断开连接的情况下杀死工作进程,它与 worker.process.kill() 具有相同的行为。为了向后兼容,此方法别名为 worker.destroy()
    • .disconnect([callback])发送给工作进程,使其调用自身的 .disconnect()将关闭所有服务器,等待那些服务器上的 'close' 事件,然后断开 IPC 通道。
    • .isConnect() 如果工作进程通过其 IPC 通道连接到其主进程,则此函数返回 true,否则返回 false。 工作进程在创建后连接到其主进程。
    • .isDead()如果工作进程已终止(由于退出或收到信号),则此函数返回 true。 否则,它返回 false

    1.5.3 worker常用事件

    为了让集群更加稳定和健壮,cluster 模块也暴露了许多事件:

    • 'message' 事件, 在工作进程中。
    cluster.workers[id].on('message', messageHandler);
    • 'exit' 事件, 当任何工作进程死亡时,则当前worker工作进程对象将触发 'exit' 事件。
    if (cluster.isPrimary) {
      const worker = cluster.fork();
      worker.on('exit', (code, signal) => {
        if (signal) {
          console.log(`worker was killed by signal: ${signal}`);
        } else if (code !== 0) {
          console.log(`worker exited with error code: ${code}`);
        } else {
          console.log('worker success!');
        }
      });
    }
    • 'listening'事件,从工作进程调用 listen() ,对当前工作进程进行监听。
    cluster.fork().on('listening', (address) => {
      // 工作进程正在监听
    });
    • disconnect事件,在工作进程 IPC 通道断开连接后触发。 当工作进程正常退出、被杀死、或手动断开连接时
    cluster.fork().on('disconnect', () => {
      //限定于当前worker对象触发
    });

    2. 进程通信

    Node中主进程和子进程之间通过进程间通信 (IPC) 实现进程间的通信,进程间通过 .send()(a.send表示向a发送)方法发送消息,监听 message 事件收取信息,这是 cluster模块 通过集成 EventEmitter 实现的。还是一个简单的官网的进程间通信例子

    • 子进程:process.on('message')process.send()
    • 父进程:child.on('message')child.send()
    # cluster.isMaster
    # cluster.fork()
    # cluster.workers
    # cluster.workers[id].on('message', messageHandler);
    # cluster.workers[id].send();
    # process.on('message', messageHandler); 
    # process.send();
    
    
    const cluster = require('cluster');
    const http = require('http');
    
    # 主进程
    if (cluster.isMaster) {
      // Keep track of http requests
      console.log(`Primary ${process.pid} is running`);
      let numReqs = 0;
      
      // Count requests
      function messageHandler(msg) {
        if (msg.cmd && msg.cmd === 'notifyRequest') {
          numReqs += 1;
        }
      }
    
      // Start workers and listen for messages containing notifyRequest
      // 开启多进程(cpu核心数)
      // 衍生工作进程。
      const numCPUs = require('os').cpus().length;
      for (let i = 0; i  {
        console.log(`worker ${worker.process.pid} died`);
      });
    
    } else {
    
      # 子进程
    
      // 工作进程可以共享任何 TCP 连接
      // 在本示例中,其是 HTTP 服务器
      // Worker processes have a http server.
      http.Server((req, res) => {
        res.writeHead(200);
        res.end('hello world\n');
    
        //****** !!!!Notify master about the request !!!!!!*******
        //****** 向process发送
        process.send({ cmd: 'notifyRequest' }); 
        
        //****** 监听从process来的
        process.on('message', function(message) { 
            // xxxxxxx
        })
      }).listen(8000);
      console.log(`Worker ${process.pid} started`);
    }

    NodeJS におけるプロセス管理の詳細な分析

    2.1 句柄发送与还原

    NodeJS 进程之间通信只有消息传递,不会真正的传递对象。

    send() 方法在发送消息前,会将消息组装成 handle 和 message,这个 message 会经过 JSON.stringify 序列化,也就是说,传递句柄的时候,不会将整个对象传递过去,在 IPC 通道传输的都是字符串,传输后通过 JSON.parse 还原成对象。

    2.2 监听共同端口

    代码里有 app.listen(port) 在进行 fork 时,为什么多个进程可以监听同一个端口呢?

    原因是主进程通过 send() 方法向多个子进程发送属于该主进程的一个服务对象的句柄,所以对于每一个子进程而言,它们在还原句柄之后,得到的服务对象是一样的,当网络请求向服务端发起时,进程服务是抢占式的,所以监听相同端口时不会引起异常。

    • 看下端口被占用的情况:
    # master.js
    
    const fork = require('child_process').fork;
    const cpus = require('os').cpus();
    
    for (let i=0; i<cpus.length><pre class="brush:php;toolbar:false"># worker.js
    
    const http = require('http');
    http.createServer((req, res) => {
    	res.end('I am worker, pid: ' + process.pid + ', ppid: ' + process.ppid);
    }).listen(3000);

    以上代码示例,控制台执行 node master.js 只有一个 worker 可以监听到 3000 端口,其余将会抛出 Error: listen EADDRINUSE :::3000 错误。

    • 那么多进程模式下怎么实现多进程端口监听呢?答案还是有的,通过句柄传递 Node.js v0.5.9 版本之后支持进程间可发送句柄功能
    /**
     * http://nodejs.cn/api/child_process.html#child_process_subprocess_send_message_sendhandle_options_callback
     * message
     * sendHandle
     */
    subprocess.send(message, sendHandle)

    当父子进程之间建立 IPC 通道之后,通过子进程对象的 send 方法发送消息,第二个参数 sendHandle 就是句柄,可以是 TCP套接字、TCP服务器、UDP套接字等,为了解决上面多进程端口占用问题,我们将主进程的 socket 传递到子进程。

    # master.js
    
    const fork = require('child_process').fork;
    const cpus = require('os').cpus();
    const server = require('net').createServer();
    server.listen(3000);
    process.title = 'node-master'
    
    for (let i=0; i<cpus.length><pre class="brush:php;toolbar:false">// worker.js
    let worker;
    process.title = 'node-worker'
    process.on('message', function (message, sendHandle) {
      if (message === 'server') {
        worker = sendHandle;
        worker.on('connection', function (socket) {
          console.log('I am worker, pid: ' + process.pid + ', ppid: ' + process.ppid)
        });
      }
    });

    验证一番,控制台执行 node master.js 

    NodeJS におけるプロセス管理の詳細な分析

    NodeJS におけるプロセス管理の詳細な分析

    NodeJS におけるプロセス管理の詳細な分析

    2.3 进程负载均衡

    了解 cluster 的话会知道,子进程是通过 cluster.fork() 创建的。在 linux 中,系统原生提供了 fork 方法,那么为什么 Node 选择自己实现 cluster模块 ,而不是直接使用系统原生的方法?主要的原因是以下两点:

    • fork的进程监听同一端口会导致端口占用错误

    • fork的进程之间没有负载均衡,容易导致惊群现象

    在 cluster模块 中,针对第一个问题,通过判断当前进程是否为 master进程,若是,则监听端口,若不是则表示为 fork 的 worker进程,不监听端口。

    针对第二个问题,cluster模块 内置了负载均衡功能, master进程 负责监听端口接收请求,然后通过调度算法(默认为 Round-Robin,可以通过环境变量 NODE_CLUSTER_SCHED_POLICY 修改调度算法)分配给对应的 worker进程

    3. 异常捕获

    3.1 未捕获异常

    当代码抛出了异常没有被捕获到时,进程将会退出,此时 Node.js 提供了 process.on('uncaughtException', handler) 接口来捕获它,但是当一个 Worker 进程遇到未捕获的异常时,它已经处于一个不确定状态,此时我们应该让这个进程优雅退出:

    • 关闭异常 Worker 进程所有的 TCP Server(将已有的连接快速断开,且不再接收新的连接),断开和 Master 的 IPC 通道,不再接受新的用户请求。
    • Master 立刻 fork 一个新的 Worker 进程,保证在线的『工人』总数不变。
    • 异常 Worker 等待一段时间,处理完已经接受的请求后退出。
    +---------+                 +---------+
    |  Worker |                 |  Master |
    +---------+                 +----+----+
         | uncaughtException         |
         +------------+              |
         |            |              |                   +---------+
         |  + ---------------------> |
         |         wait...           |                        |
         |          exit             |                        |
         +-------------------------> |                        |
         |                           |                        |
        die                          |                        |
                                     |                        |
                                     |                        |

    3.2 OOM、系统异常

    当一个进程出现异常导致 crash 或者 OOM 被系统杀死时,不像未捕获异常发生时我们还有机会让进程继续执行,只能够让当前进程直接退出,Master 立刻 fork 一个新的 Worker。


    二、子进程

    1. child_process模块

    child_process 模块提供了衍生子进程的能力, 简单来说就是执行cmd命令的能力。 默认情况下, stdin、 stdout 和 stderr 的管道会在父 Node.js 进程和衍生的子进程之间建立。 这些管道具有有限的(且平台特定的)容量。 如果子进程写入 stdout 时超出该限制且没有捕获输出,则子进程会阻塞并等待管道缓冲区接受更多的数据。 这与 shell 中的管道的行为相同。 如果不消费输出,则使用 { stdio: 'ignore' } 选项。

    1.1 引入child_process

    const cp = require('child_process');

    1.2 基本概念

    通过 API 创建出来的子进程和父进程没有任何必然联系

    • 4个异步方法,创建子进程:fork、exec、execFile、spawn

      • Node

        • fork(modulePath, args):想将一个 Node 进程作为一个独立的进程来运行的时候使用,使得计算处理和文件描述器脱离 Node 主进程(复制一个子进程)
      • 非 Node

        • spawn(command, args):处理一些会有很多子进程 I/O 时、进程会有大量输出时使用
        • execFile(file, args[, callback]):只需执行一个外部程序的时候使用,执行速度快,处理用户输入相对安全
        • exec(command, options):想直接访问线程的 shell 命令时使用,一定要注意用户输入
    • 3个同步方法:execSyncexecFileSyncspawnSync

    NodeJS におけるプロセス管理の詳細な分析

    其他三种方法都是 spawn() 的延伸。

    1.2.1 fork(modulePath, args)函数, 复制进程

    • fork 方法会开放一个 IPC 通道,不同的 Node 进程进行消息传送
    • 一个子进程消耗 30ms 启动时间和 10MB 内存

    记住,衍生的 Node.js 子进程独立于父进程,但两者之间建立的 IPC 通信通道除外。 每个进程都有自己的内存,带有自己的 V8 实例

    举个?

    在一个目录下新建 worker.js 和 master.js 两个文件:

    # child.js
    
    const t = JSON.parse(process.argv[2]);
    console.error(`子进程 t=${JSON.stringify(t)}`);
    process.send({hello:`儿子pid=${process.pid} 给爸爸进程pid=${process.ppid} 请安`});
    process.on('message', (msg)=>{
        console.error(`子进程 msg=${JSON.stringify(msg)}`);
    });
    # parent.js
    
    const {fork} = require('child_process');
    for(let i = 0; i  {
            console.log(`messsgae from child msg=${JSON.stringify(msg)}`, );
        });
        p.send({hello:`来自爸爸${process.pid} 进程id=${i}的问候`});
    }

    NodeJS におけるプロセス管理の詳細な分析

    通过 node parent.js 启动 parent.js,然后通过 ps aux | grep worker.js 查看进程的数量,我们可以发现,理想状况下,进程的数量等于 CPU 的核心数,每个进程各自利用一个 CPU 核心。

    这是经典的 Master-Worker 模式(主从模式)

    NodeJS におけるプロセス管理の詳細な分析

    实际上,fork 进程是昂贵的,复制进程的目的是充分利用 CPU 资源,所以 NodeJS 在单线程上使用了事件驱动的方式来解决高并发的问题。

    适用场景
    一般用于比较耗时的场景,并且用node去实现的,比如下载文件;
    fork可以实现多线程下载:将文件分成多块,然后每个进程下载一部分,最后拼起来;

    1.2.2 execFile(file, args[, callback])

    • 会把输出结果缓存好,通过回调返回最后结果或者异常信息
    const cp = require('child_process');
    // 第一个参数,要运行的可执行文件的名称或路径。这里是echo
    cp.execFile('echo', ['hello', 'world'], (err, stdout, stderr) => {
      if (err) { console.error(err); }
      console.log('stdout: ', stdout);
      console.log('stderr: ', stderr);
    });

    适用场景
    比较适合开销小的任务,更关注结果,比如ls等;

    1.2.3 exec(command, options)

    主要用来执行一个shell方法,其内部还是调用了spawn ,不过他有最大缓存限制。

    • 只有一个字符串命令
    • 和 shell 一模一样
    const cp = require('child_process');
    
    cp.exec(`cat ${__dirname}/messy.txt | sort | uniq`, (err, stdout, stderr) => {
      console.log(stdout);
    });

    适用场景
    比较适合开销小的任务,更关注结果,比如ls等;

    1.2.4 spawn(command, args)

    • 通过流可以使用有大量数据输出的外部应用,节约内存
    • 使用流提高数据响应效率
    • spawn 方法返回一个 I/O 的流接口

    单一任务

    const cp = require('child_process');
    
    const child = cp.spawn('echo', ['hello', 'world']);
    child.on('error', console.error);
    
    # 输出是流,输出到主进程stdout,控制台
    child.stdout.pipe(process.stdout);
    child.stderr.pipe(process.stderr);

    多任务串联

    const cp = require('child_process');
    const path = require('path');
    
    const cat = cp.spawn('cat', [path.resolve(__dirname, 'messy.txt')]);
    const sort = cp.spawn('sort');
    const uniq = cp.spawn('uniq');
    
    # 输出是流
    cat.stdout.pipe(sort.stdin);
    sort.stdout.pipe(uniq.stdin);
    uniq.stdout.pipe(process.stdout);

    适用场景
    spawn是流式的,所以适合耗时任务,比如执行npm install,打印install的过程

    1.3 各种事件

    1.3.1 close

    在进程已结束并且子进程的标准输入输出流(sdtio)已关闭之后,则触发 'close' 事件。这个事件跟exit不同,因为多个进程可以共享同个stdio流。

    参数:

    • code (子プロセスが単独で終了する場合の終了コード)
    • signal (子プロセスを終了するシグナル)

    質問: コードは次のとおりです。何かありますか?
    (コードのコメントからではないようです) たとえば、子プロセスを強制終了するには kill を使用します。

    1.3.2 exit

    パラメータ:
    code、signal、子プロセスが単独で終了する場合、code が終了になります。 code 、それ以外の場合は null;
    子プロセスがシグナルによって終了する場合、 signal はプロセスを終了するシグナルであり、それ以外の場合は null です。
    2 つのうち、1 つは null であってはなりません。

    Note:
    exitイベントがトリガーされたとき、子プロセスの stdio ストリームがまだ開いている可能性があります。 (シナリオ?) さらに、nodejs は SIGINT および SIGTERM シグナルをリッスンします。つまり、nodejs がこれら 2 つのシグナルを受信して​​も、すぐに終了せず、最初にいくつかのクリーンアップ作業を行ってから、これら 2 つのシグナルを再スローします。 (視覚的には、この時点で js はデータベースを閉じるなどのクリーンアップ作業を行うことができます。)

    SIGINT: 割り込み、プログラム終了信号。通常はユーザーが CTRL C を押したときに発行されます。通知に使用されます。フォアグラウンド プロセスがプロセスを終了します。
    SIGTERM: 終了、プログラム終了信号。この信号はブロックして処理することができ、通常はプログラムを正常に終了するように要求するために使用されます。シェル コマンド kill は、デフォルトでこのシグナルを生成します。シグナルを終了できない場合は、SIGKILL(強制終了)を試します。

    1.3.3 エラー

    以下のような場合にエラーが発生します。エラーがトリガーされると、終了がトリガーされる場合とトリガーされない場合があります。 (心が折れました)

    • プロセスを生成できません。
    • プロセスを強制終了できません。
    • 子プロセスへのメッセージの送信に失敗しました。

    1.3.4 message

    process.send() を使用してメッセージを送信するとトリガーされます。

    Parameters:
    message (json オブジェクトまたはプリミティブ値)、sendHandle (net.Socket オブジェクトまたはnet.Server オブジェクト (クラスターに詳しい学生はこれに精通しているはずです)

    1.4 メソッド

    .connected: .disconnected()# を呼び出すとき##、設定は false です。子プロセスからメッセージを受信できるか、または子プロセスにメッセージを送信できるかどうかを表します。

    .disconnect(): 親プロセスと子プロセス間の IPC チャネルを閉じます。このメソッドが呼び出されると、disconnect イベントがトリガーされます。子プロセスがノード インスタンス (child_process.fork() によって作成されたもの) の場合、子プロセス内で process.disconnect() をアクティブに呼び出して IPC チャネルを終了することもできます。


    3. NodeJS マルチスレッド

    シングルスレッドの問題に対処するには、通常、マルチプロセス メソッドを使用してマルチスレッドをシミュレートします

    1. シングル-スレッドの問題

      CPU の使用率が不十分
    • #キャッチされない例外により、プログラム全体が終了する可能性があります
    • #2. ノード スレッド

    Node プロセスは 7 スレッドを占有します
    • Node のコアは v8 エンジンです。Node が開始されると、v8 のインスタンスが作成されます。このインスタンスマルチスレッドです
    • メイン スレッド: コードのコンパイルと実行

        コンパイル/最適化スレッド: メイン スレッドの実行時に、コードを最適化できます
      • アナライザー スレッド: コードの実行時間を記録して分析し、Crankshaft がコードの実行を最適化するための基礎を提供します
      • ガベージ コレクション用の複数のスレッド
      #JavaScript の実行は
    • single-threaded
    • ですが、JavaScript のホスティング環境は、ノードであれブラウザであれ、マルチスレッドです。

    JavaScript はなぜシングルスレッドなのでしょうか?

    この問題はブラウザから始める必要があります。ブラウザ環境での DOM 操作の場合、複数のスレッドが同じ DOM 上で操作すると、混乱が生じるでしょうか? つまり、DOM 操作は 1 つだけであるということです。 -DOM レンダリングの競合を避けるためにスレッド化されます。ブラウザ環境では、UIレンダリングスレッドとJS実行エンジンは相互に排他的であり、一方が実行されると他方が一時停止されますが、これはJSエンジンによって決定されます。

    3. 异步 IO

    • Node 中有一些 IO 操作(DNS,FS)和一些 CPU 密集计算(Zlib,Crypto)会启用 Node 的线程池
    • 线程池默认大小为 4,可以手动更改线程池默认大小
    process.env.UV_THREADPOOL_SIZE = 64

    4. 真 Node 多线程

    4.1 worker_threads核心模块

    • Node 10.5.0 的发布,给出了一个实验性质的模块 worker_threads 给 Node 提供真正的多线程能力
    • worker_thread 模块中有 4 个对象和 2 个类
      • isMainThread: 是否是主线程,源码中是通过 threadId === 0 进行判断的。
      • MessagePort: 用于线程之间的通信,继承自 EventEmitter。
      • MessageChannel: 用于创建异步、双向通信的通道实例。
      • threadId: 线程 ID。
      • Worker: 用于在主线程中创建子线程。第一个参数为 filename,表示子线程执行的入口。
      • parentPort: 在 worker 线程里是表示父进程的 MessagePort 类型的对象,在主线程里为 null
      • workerData: 用于在主进程中向子进程传递数据(data 副本)
    const {
      isMainThread,
      parentPort,
      workerData,
      threadId,
      MessageChannel,
      MessagePort,
      Worker
    } = require('worker_threads');
    
    function mainThread() {
      for (let i = 0; i  { console.log(`main: worker stopped with exit code ${code}`); });
        worker.on('message', msg => {
          console.log(`main: receive ${msg}`);
          worker.postMessage(msg + 1);
        });
      }
    }
    
    function workerThread() {
      console.log(`worker: workerDate ${workerData}`);
      parentPort.on('message', msg => {
        console.log(`worker: receive ${msg}`);
      }),
      parentPort.postMessage(workerData);
    }
    
    if (isMainThread) {
      mainThread();
    } else {
      workerThread();
    }

    4.2 线程通信

    const assert = require('assert');
    const {
      Worker,
      MessageChannel,
      MessagePort,
      isMainThread,
      parentPort
    } = require('worker_threads');
    if (isMainThread) {
      const worker = new Worker(__filename);
      const subChannel = new MessageChannel();
      worker.postMessage({ hereIsYourPort: subChannel.port1 }, [subChannel.port1]);
      subChannel.port2.on('message', (value) => {
        console.log('received:', value);
      });
    } else {
      parentPort.once('message', (value) => {
        assert(value.hereIsYourPort instanceof MessagePort);
        value.hereIsYourPort.postMessage('the worker is sending this');
        value.hereIsYourPort.close();
      });
    }

    四、 多进程 vs 多线程

    进程是资源分配的最小单位,线程是CPU调度的最小单位


    五、 知识拓展

    1. IPC

    IPC (Inter-process communication) 即进程间通信,由于每个进程创建之后都有自己的独立地址空间,实现 IPC 的目的就是为了进程之间资源共享访问。

    实现 IPC 的方式有多种:管道、消息队列、信号量、Domain Socket,Node.js 通过 pipe 来实现。

    NodeJS におけるプロセス管理の詳細な分析

    实际上,父进程会在创建子进程之前,会先创建 IPC 通道并监听这个 IPC,然后再创建子进程,通过环境变量(NODE_CHANNEL_FD)告诉子进程和 IPC 通道相关的文件描述符,子进程启动的时候根据文件描述符连接 IPC 通道,从而和父进程建立连接。

    NodeJS におけるプロセス管理の詳細な分析

    2. 句柄传递

    句柄是一种可以用来标识资源的引用的,它的内部包含了指向对象的文件资源描述符。

    一般情况下,当我们想要将多个进程监听到一个端口下,可能会考虑使用主进程代理的方式处理:

    1NodeJS におけるプロセス管理の詳細な分析

    然而,这种代理方案会导致每次请求的接收和代理转发用掉两个文件描述符,而系统的文件描述符是有限的,这种方式会影响系统的扩展能力。

    所以,为什么要使用句柄?原因是在实际应用场景下,建立 IPC 通信后可能会涉及到比较复杂的数据处理场景,句柄可以作为 send() 方法的第二个可选参数传入,也就是说可以直接将资源的标识通过 IPC 传输,避免了上面所说的代理转发造成的文件描述符的使用。

    1NodeJS におけるプロセス管理の詳細な分析

    以下是支持发送的句柄类型:

    • net.Socket
    • net.Server
    • net.Native
    • dgram.Socket
    • dgram.Native

    3.孤儿进程

    父进程创建子进程之后,父进程退出了,但是父进程对应的一个或多个子进程还在运行,这些子进程会被系统的 init 进程收养,对应的进程 ppid 为 1,这就是孤儿进程。通过以下代码示例说明。

    # worker.js
    
    const http = require('http');
    const server = http.createServer((req, res) => {
    	res.end('I am worker, pid: ' + process.pid + ', ppid: ' + process.ppid); 
            // 记录当前工作进程 pid 及父进程 ppid
    });
    
    let worker;
    process.on('message', function (message, sendHandle) {
    	if (message === 'server') {
    		worker = sendHandle;
    		worker.on('connection', function(socket) {
    			server.emit('connection', socket);
    		});
    	}
    });
    # master.js
    
    const fork = require('child_process').fork;
    const server = require('net').createServer();
    server.listen(3000);
    const worker = fork('worker.js');
    
    worker.send('server', server);
    console.log('worker process created, pid: %s ppid: %s', worker.pid, process.pid);
    process.exit(0); 
    // 创建子进程之后,主进程退出,此时创建的 worker 进程会成为孤儿进程

    控制台进行测试,输出当前工作进程 pid 和 父进程 ppid

    1NodeJS におけるプロセス管理の詳細な分析

    由于在 master.js 里退出了父进程,活动监视器所显示的也就只有工作进程。

    1NodeJS におけるプロセス管理の詳細な分析

    再次验证,打开控制台调用接口,可以看到工作进程 5611 对应的 ppid 为 1(为 init 进程),此时已经成为了孤儿进程

    1NodeJS におけるプロセス管理の詳細な分析

    4. 守护进程

    守护进程运行在后台不受终端的影响,什么意思呢?
    Node.js 开发的同学们可能熟悉,当我们打开终端执行 node app.js 开启一个服务进程之后,这个终端就会一直被占用,如果关掉终端,服务就会断掉,即前台运行模式
    如果采用守护进程进程方式,这个终端我执行 node app.js 开启一个服务进程之后,我还可以在这个终端上做些别的事情,且不会相互影响。

    4.1 创建步骤

    • 创建子进程

    • 在子进程中创建新会话(调用系统函数 setsid)

    • 改变子进程工作目录(如:“/” 或 “/usr/ 等)

    • 父进程终止

    4.2 Node.js 编写守护进程 Demo及测试

    • index.js 文件里的处理逻辑使用 spawn 创建子进程完成了上面的第一步操作。
    • 设置 options.detached 为 true 可以使子进程在父进程退出后继续运行(系统层会调用 setsid 方法),这是第二步操作。
    • options.cwd 指定当前子进程工作目录若不做设置默认继承当前工作目录,这是第三步操作。
    • 运行 daemon.unref() 退出父进程,这是第四步操作。
    // index.js
    const spawn = require('child_process').spawn;
    
    function startDaemon() {
        const daemon = spawn('node', ['daemon.js'], {
            cwd: '/usr',
            detached : true,
            stdio: 'ignore',
        });
    
        console.log('守护进程开启 父进程 pid: %s, 守护进程 pid: %s', process.pid, daemon.pid);
        daemon.unref();
    }
    
    startDaemon()

    daemon.js 文件里处理逻辑开启一个定时器每 10 秒执行一次,使得这个资源不会退出,同时写入日志到子进程当前工作目录下

    /usr/daemon.js
    const fs = require('fs');
    const { Console } = require('console');
    
    // custom simple logger
    const logger = new Console(fs.createWriteStream('./stdout.log'), fs.createWriteStream('./stderr.log'));
    
    setInterval(function() {
    	logger.log('daemon pid: ', process.pid, ', ppid: ', process.ppid);
    }, 1000 * 10);

    守护进程实现 Node.js 版本 源码地址

    https://github.com/Q-Angelo/project-training/tree/master/nodejs/simple-daemon

    1NodeJS におけるプロセス管理の詳細な分析

    1NodeJS におけるプロセス管理の詳細な分析

    4.3 守护进程总结

    在实际工作中对于守护进程并不陌生,例如 PM2、Egg-Cluster 等,以上只是一个简单的 Demo 对守护进程做了一个说明,在实际工作中对守护进程的健壮性要求还是很高的,例如:进程的异常监听、工作进程管理调度、进程挂掉之后重启等等,这些还需要去不断思考。

    5. 进程的当前工作目录

    目录是什么?

    进程的当前工作目录可以通过 process.cwd() 命令获取,默认为当前启动的目录,如果是创建子进程则继承于父进程的目录,可通过 process.chdir() 命令重置,例如通过 spawn 命令创建的子进程可以指定 cwd 选项设置子进程的工作目录。

    有什么作用?

    例如,通过 fs 读取文件,如果设置为相对路径则相对于当前进程启动的目录进行查找,所以,启动目录设置有误的情况下将无法得到正确的结果。还有一种情况程序里引用第三方模块也是根据当前进程启动的目录来进行查找的。

    // 示例
    process.chdir('/Users/may/Documents/test/') // 设置当前进程目录
    
    console.log(process.cwd()); // 获取当前进程目录

    更多node相关知识,请访问:nodejs 教程

    以上がNodeJS におけるプロセス管理の詳細な分析の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

    声明:
    この記事はjuejin.cnで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。