溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

NodeJS中的進程管理怎么實現(xiàn)

發(fā)布時間:2022-09-05 16:55:04 來源:億速云 閱讀:202 作者:iii 欄目:web開發(fā)

這篇文章主要介紹“NodeJS中的進程管理怎么實現(xiàn)”,在日常操作中,相信很多人在NodeJS中的進程管理怎么實現(xiàn)問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”NodeJS中的進程管理怎么實現(xiàn)”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!

熟悉 js 的朋友都知道,js 是單線程的,在 Node 中,采用的是 多進程單線程 的模型。由于javascript單線程的限制,在多核服務器上,我們往往需要啟動多個進程才能最大化服務器性能。

Node.js 進程集群可用于運行多個 Node.js 實例,這些實例可以在其應用程序線程之間分配工作負載。 當不需要進程隔離時,請改用 worker_threads 模塊,它允許在單個 Node.js 實例中運行多個應用程序線程。

零、NodeJS多進程

  • 進程總數(shù),其中一個主進程,cpu 個數(shù) x cpu 核數(shù) 個 子進程

  • 無論 child_process 還是 cluster,都不是多線程模型,而是多進程模型

  • 應對單線程問題,通常使用多進程的方式來模擬多線程

一、核心模塊cluster集群

Node 在 V0.8 版本之后引入了 cluster模塊,通過一個主進程 (master) 管理多個子進程 (worker) 的方式實現(xiàn)集群。

集群模塊可以輕松創(chuàng)建共享服務器端口的子進程。

cluster 底層是 child_process 模塊,除了可以發(fā)送普通消息,還可以發(fā)送底層對象 TCP、UDP 等, cluster 模塊是 child_process 模塊和 net 模塊的組合應用。 cluster 啟動時,內(nèi)部會啟動 TCP 服務器,將這個 TCP 服務器端 socket 的文件描述符發(fā)給工作進程。

cluster 模塊應用中,一個主進程只能管理一組工作進程,其運作模式?jīng)]有 child_process 模塊那么靈活,但是更加穩(wěn)定:

NodeJS中的進程管理怎么實現(xiàn)

1.cluster配置詳情

1.1 引入cluster

const cluster = require('cluster')復

1.2 cluster常用屬性

  • .isMaster 標識主進程, Node<16

  • .isPrimary 標識主進程, Node>16

  • .isWorker 標識子進程

  • .worker 對當前工作進程對象的引用【子進程中】

  • .workers 存儲活動工作進程對象的哈希,以 id 字段為鍵。 這樣可以很容易地遍歷所有工作進程。 它僅在主進程中可用。cluster.wokers[id] === worker【主進程中】

  • .settings 只讀, cluster配置項。在調(diào)用 .setupPrimary()或.fork()方法之后,此設置對象將包含設置,包括默認值。之前為空對象。此對象不應手動更改或設置。

cluster.settings配置項詳情:
- `execArgv` <string[]>傳給 Node.js 可執(zhí)行文件的字符串參數(shù)列表。 **默認值:**  `process.execArgv`。
- `exec` <string> 工作進程文件的文件路徑。 **默認值:** `process.argv[1]`。
- `args` <string[]> 傳給工作進程的字符串參數(shù)。 **默認值:**`process.argv.slice(2)`。
- `cwd` <string>工作進程的當前工作目錄。 **默認值:**  `undefined` (從父進程繼承)。
- `serialization` <string>指定用于在進程之間發(fā)送消息的序列化類型。 可能的值為 `'json'` 和 `'advanced'`。  **默認值:**  `false`。
- `silent` <boolean>是否將輸出發(fā)送到父進程的標準輸入輸出。 **默認值:**  `false`。
- `stdio` <Array>配置衍生進程的標準輸入輸出。 由于集群模塊依賴 IPC 來運行,因此此配置必須包含 `'ipc'` 條目。 提供此選項時,它會覆蓋 `silent`。
- `uid` <number>設置進程的用戶標識。 
- `gid` <number>設置進程的群組標識。
- `inspectPort` <number> | <Function> 設置工作進程的檢查器端口。 這可以是數(shù)字,也可以是不帶參數(shù)并返回數(shù)字的函數(shù)。 默認情況下,每個工作進程都有自己的端口,從主進程的 `process.debugPort` 開始遞增。
- `windowsHide` <boolean> 隱藏通常在 Windows 系統(tǒng)上創(chuàng)建的衍生進程控制臺窗口。 **默認值:**  `false`。

1.3 cluster常用方法

  • .fork([env]) 衍生新的工作進程【主進程中】

  • .setupPrimary([settings]) Node>16

  • .setupMaster([settings]) 用于更改默認的 'fork' 行為,用后設置將出現(xiàn)在 cluster.settings 中。任何設置更改只會影響未來對 .fork()的調(diào)用,而不會影響已經(jīng)運行的工作進程。上述默認值僅適用于第一次調(diào)用。Node 小于 16【主進程中】

  • .disconnect([callback]) 當所有工作進程斷開連接并關閉句柄時調(diào)用【主進程中】

1.4 cluster常用事件

為了讓集群更加穩(wěn)定和健壯,cluster 模塊也暴露了許多事件:

  • 'message' 事件, 當集群主進程接收到來自任何工作進程的消息時觸發(fā)。

  • 'exit' 事件, 當任何工作進程死亡時,則集群模塊將觸發(fā) 'exit' 事件。

cluster.on('exit', (worker, code, signal) => {
  console.log('worker %d died (%s). restarting...',
              worker.process.pid, signal || code);
  cluster.fork();
});
  • 'listening'事件,從工作進程調(diào)用 listen() 后,當服務器上觸發(fā) 'listening' 事件時,則主進程中的 cluster 也將觸發(fā) 'listening' 事件。

cluster.on('listening', (worker, address) => {
  console.log(
    `A worker is now connected to ${address.address}:${address.port}`);
});
  • 'fork' 事件,當新的工作進程被衍生時,則集群模塊將觸發(fā) 'fork' 事件。

cluster.on('fork', (worker) => {
  timeouts[worker.id] = setTimeout(errorMsg, 2000);
});
  • 'setup' 事件,每次調(diào)用 .setupPrimary()時觸發(fā)。

  • disconnect事件,在工作進程 IPC 通道斷開連接后觸發(fā)。 當工作進程正常退出、被殺死、或手動斷開連接時

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() 創(chuàng)建,此函數(shù)返回的對象存儲為 .process。 在工作進程中,存儲了全局的 process。

1.5.2 worker常用方法
  • .send(message[, sendHandle[, options]][, callback]) 向工作進程或主進程發(fā)送消息,可選擇使用句柄。在主進程中,這會向特定的工作進程發(fā)送消息。 它與 ChildProcess.send()相同。在工作進程中,這會向主進程發(fā)送消息。 它與 process.send() 相同。

  • .destroy()

  • .kill([signal])此函數(shù)會殺死工作進程。kill() 函數(shù)在不等待正常斷開連接的情況下殺死工作進程,它與 worker.process.kill() 具有相同的行為。為了向后兼容,此方法別名為 worker.destroy()。

  • .disconnect([callback])發(fā)送給工作進程,使其調(diào)用自身的 .disconnect()將關閉所有服務器,等待那些服務器上的 'close' 事件,然后斷開 IPC 通道。

  • .isConnect() 如果工作進程通過其 IPC 通道連接到其主進程,則此函數(shù)返回 true,否則返回 false。 工作進程在創(chuàng)建后連接到其主進程。

  • .isDead()如果工作進程已終止(由于退出或收到信號),則此函數(shù)返回 true。 否則,它返回 false。

1.5.3 worker常用事件

為了讓集群更加穩(wěn)定和健壯,cluster 模塊也暴露了許多事件:

  • 'message' 事件, 在工作進程中。

cluster.workers[id].on('message', messageHandler);
  • 'exit' 事件, 當任何工作進程死亡時,則當前worker工作進程對象將觸發(fā) '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'事件,從工作進程調(diào)用 listen() ,對當前工作進程進行監(jiān)聽。

cluster.fork().on('listening', (address) => {
  // 工作進程正在監(jiān)聽
});
  • disconnect事件,在工作進程 IPC 通道斷開連接后觸發(fā)。 當工作進程正常退出、被殺死、或手動斷開連接時

cluster.fork().on('disconnect', () => {
  //限定于當前worker對象觸發(fā)
});

2. 進程通信

Node中主進程和子進程之間通過進程間通信 (IPC) 實現(xiàn)進程間的通信,進程間通過 .send()(a.send表示向a發(fā)送)方法發(fā)送消息,監(jiān)聽 message 事件收取信息,這是 cluster模塊 通過集成 EventEmitter 實現(xiàn)的。還是一個簡單的官網(wǎng)的進程間通信例子

  • 子進程: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核心數(shù))
  // 衍生工作進程。
  const numCPUs = require('os').cpus().length;
  for (let i = 0; i < numCPUs; i++) {
    console.log(i)
    cluster.fork();
  }

// cluster worker 主進程與子進程通信
  for (const id in cluster.workers) {
    // ***監(jiān)聽來自子進程的事件
    cluster.workers[id].on('message', messageHandler); 
    
    // ***向子進程發(fā)送
    cluster.workers[id].send({                         
        type: 'masterToWorker',
        from: 'master',
        data: {
            number: Math.floor(Math.random() * 50)
        }
    });
  }
  cluster.on('exit', (worker, code, signal) => {
    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');

    //****** ?。。。otify master about the request ?。。。。?!*******
    //****** 向process發(fā)送
    process.send({ cmd: 'notifyRequest' }); 
    
    //****** 監(jiān)聽從process來的
    process.on('message', function(message) { 
        // xxxxxxx
    })
  }).listen(8000);
  console.log(`Worker ${process.pid} started`);
}

NodeJS中的進程管理怎么實現(xiàn)

2.1 句柄發(fā)送與還原

NodeJS 進程之間通信只有消息傳遞,不會真正的傳遞對象。

send() 方法在發(fā)送消息前,會將消息組裝成 handle 和 message,這個 message 會經(jīng)過 JSON.stringify 序列化,也就是說,傳遞句柄的時候,不會將整個對象傳遞過去,在 IPC 通道傳輸?shù)亩际亲址?,傳輸后通過 JSON.parse 還原成對象。

2.2 監(jiān)聽共同端口

代碼里有 app.listen(port) 在進行 fork 時,為什么多個進程可以監(jiān)聽同一個端口呢?

原因是主進程通過 send() 方法向多個子進程發(fā)送屬于該主進程的一個服務對象的句柄,所以對于每一個子進程而言,它們在還原句柄之后,得到的服務對象是一樣的,當網(wǎng)絡請求向服務端發(fā)起時,進程服務是搶占式的,所以監(jiān)聽相同端口時不會引起異常。

  • 看下端口被占用的情況:

# master.js

const fork = require('child_process').fork;
const cpus = require('os').cpus();

for (let i=0; i<cpus.length; i++) {
    const worker = fork('worker.js');
    console.log('worker process created, pid: %s ppid: %s', worker.pid, process.pid);
}
# worker.js

const http = require('http');
http.createServer((req, res) => {
	res.end('I am worker, pid: ' + process.pid + ', ppid: ' + process.ppid);
}).listen(3000);

以上代碼示例,控制臺執(zhí)行 node master.js 只有一個 worker 可以監(jiān)聽到 3000 端口,其余將會拋出 Error: listen EADDRINUSE :::3000 錯誤。

  • 那么多進程模式下怎么實現(xiàn)多進程端口監(jiān)聽呢?答案還是有的,通過句柄傳遞 Node.js v0.5.9 版本之后支持進程間可發(fā)送句柄功能

/**
 * http://nodejs.cn/api/child_process.html#child_process_subprocess_send_message_sendhandle_options_callback
 * message
 * sendHandle
 */
subprocess.send(message, sendHandle)

當父子進程之間建立 IPC 通道之后,通過子進程對象的 send 方法發(fā)送消息,第二個參數(shù) 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; i++) {
    const worker = fork('worker.js');
    
    # 句柄傳遞
    worker.send('server', server);
    console.log('worker process created, pid: %s ppid: %s', worker.pid, process.pid);
}
// 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)
    });
  }
});

驗證一番,控制臺執(zhí)行 node master.js

NodeJS中的進程管理怎么實現(xiàn)

NodeJS中的進程管理怎么實現(xiàn)

NodeJS中的進程管理怎么實現(xiàn)

2.3 進程負載均衡

了解 cluster 的話會知道,子進程是通過 cluster.fork() 創(chuàng)建的。在 linux 中,系統(tǒng)原生提供了 fork 方法,那么為什么 Node 選擇自己實現(xiàn) cluster模塊 ,而不是直接使用系統(tǒng)原生的方法?主要的原因是以下兩點:

  • fork的進程監(jiān)聽同一端口會導致端口占用錯誤

  • fork的進程之間沒有負載均衡,容易導致驚群現(xiàn)象

cluster模塊 中,針對第一個問題,通過判斷當前進程是否為 master進程,若是,則監(jiān)聽端口,若不是則表示為 fork 的 worker進程,不監(jiān)聽端口。

針對第二個問題,cluster模塊 內(nèi)置了負載均衡功能, master進程 負責監(jiān)聽端口接收請求,然后通過調(diào)度算法(默認為 Round-Robin,可以通過環(huán)境變量 NODE_CLUSTER_SCHED_POLICY 修改調(diào)度算法)分配給對應的 worker進程。

3. 異常捕獲

3.1 未捕獲異常

當代碼拋出了異常沒有被捕獲到時,進程將會退出,此時 Node.js 提供了 process.on('uncaughtException', handler) 接口來捕獲它,但是當一個 Worker 進程遇到未捕獲的異常時,它已經(jīng)處于一個不確定狀態(tài),此時我們應該讓這個進程優(yōu)雅退出:

  • 關閉異常 Worker 進程所有的 TCP Server(將已有的連接快速斷開,且不再接收新的連接),斷開和 Master 的 IPC 通道,不再接受新的用戶請求。

  • Master 立刻 fork 一個新的 Worker 進程,保證在線的『工人』總數(shù)不變。

  • 異常 Worker 等待一段時間,處理完已經(jīng)接受的請求后退出。

+---------+                 +---------+
|  Worker |                 |  Master |
+---------+                 +----+----+
     | uncaughtException         |
     +------------+              |
     |            |              |                   +---------+
     | <----------+              |                   |  Worker |
     |                           |                   +----+----+
     |        disconnect         |   fork a new worker    |
     +-------------------------> + ---------------------> |
     |         wait...           |                        |
     |          exit             |                        |
     +-------------------------> |                        |
     |                           |                        |
    die                          |                        |
                                 |                        |
                                 |                        |

3.2 OOM、系統(tǒng)異常

當一個進程出現(xiàn)異常導致 crash 或者 OOM 被系統(tǒng)殺死時,不像未捕獲異常發(fā)生時我們還有機會讓進程繼續(xù)執(zhí)行,只能夠讓當前進程直接退出,Master 立刻 fork 一個新的 Worker。


二、子進程

1. child_process模塊

child_process 模塊提供了衍生子進程的能力, 簡單來說就是執(zhí)行cmd命令的能力。 默認情況下, stdin、 stdout 和 stderr 的管道會在父 Node.js 進程和衍生的子進程之間建立。 這些管道具有有限的(且平臺特定的)容量。 如果子進程寫入 stdout 時超出該限制且沒有捕獲輸出,則子進程會阻塞并等待管道緩沖區(qū)接受更多的數(shù)據(jù)。 這與 shell 中的管道的行為相同。 如果不消費輸出,則使用 { stdio: 'ignore' } 選項。

1.1 引入child_process

const cp = require('child_process');

1.2 基本概念

通過 API 創(chuàng)建出來的子進程和父進程沒有任何必然聯(lián)系

  • 4個異步方法,創(chuàng)建子進程:fork、exec、execFile、spawn

    • spawn(command, args):處理一些會有很多子進程 I/O 時、進程會有大量輸出時使用

    • execFile(file, args[, callback]):只需執(zhí)行一個外部程序的時候使用,執(zhí)行速度快,處理用戶輸入相對安全

    • exec(command, options):想直接訪問線程的 shell 命令時使用,一定要注意用戶輸入

    • fork(modulePath, args):想將一個 Node 進程作為一個獨立的進程來運行的時候使用,使得計算處理和文件描述器脫離 Node 主進程(復制一個子進程)

    • Node

    • 非 Node

  • 3個同步方法:execSync、execFileSync、spawnSync

NodeJS中的進程管理怎么實現(xiàn)

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

1.2.1 fork(modulePath, args)函數(shù), 復制進程
  • fork 方法會開放一個 IPC 通道,不同的 Node 進程進行消息傳送

  • 一個子進程消耗 30ms 啟動時間和 10MB 內(nèi)存

記住,衍生的 Node.js 子進程獨立于父進程,但兩者之間建立的 IPC 通信通道除外。 每個進程都有自己的內(nèi)存,帶有自己的 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 < 3; i++){
    const p = fork('./child.js', [JSON.stringify({id:1,name:1})]);
    p.on('message', (msg) => {
        console.log(`messsgae from child msg=${JSON.stringify(msg)}`, );
    });
    p.send({hello:`來自爸爸${process.pid} 進程id=${i}的問候`});
}

NodeJS中的進程管理怎么實現(xiàn)

通過 node parent.js 啟動 parent.js,然后通過 ps aux | grep worker.js 查看進程的數(shù)量,我們可以發(fā)現(xiàn),理想狀況下,進程的數(shù)量等于 CPU 的核心數(shù),每個進程各自利用一個 CPU 核心。

這是經(jīng)典的 Master-Worker 模式(主從模式)

NodeJS中的進程管理怎么實現(xiàn)

實際上,fork 進程是昂貴的,復制進程的目的是充分利用 CPU 資源,所以 NodeJS 在單線程上使用了事件驅(qū)動的方式來解決高并發(fā)的問題。

適用場景
一般用于比較耗時的場景,并且用node去實現(xiàn)的,比如下載文件;
fork可以實現(xiàn)多線程下載:將文件分成多塊,然后每個進程下載一部分,最后拼起來;

1.2.2 execFile(file, args[, callback])
  • 會把輸出結果緩存好,通過回調(diào)返回最后結果或者異常信息

const cp = require('child_process');
// 第一個參數(shù),要運行的可執(zhí)行文件的名稱或路徑。這里是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)

主要用來執(zhí)行一個shell方法,其內(nèi)部還是調(diào)用了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)
  • 通過流可以使用有大量數(shù)據(jù)輸出的外部應用,節(jié)約內(nèi)存

  • 使用流提高數(shù)據(jù)響應效率

  • 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);

多任務串聯(lián)

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是流式的,所以適合耗時任務,比如執(zhí)行npm install,打印install的過程

1.3 各種事件

1.3.1 close

在進程已結束并且子進程的標準輸入輸出流(sdtio)已關閉之后,則觸發(fā) 'close' 事件。這個事件跟exit不同,因為多個進程可以共享同個stdio流。

參數(shù):

  • code(退出碼,如果子進程是自己退出的話)

  • signal(結束子進程的信號)

問題:code一定是有的嗎?
(從對code的注解來看好像不是)比如用kill殺死子進程,那么,code是?

1.3.2 exit

參數(shù):
code、signal,如果子進程是自己退出的,那么code就是退出碼,否則為null;
如果子進程是通過信號結束的,那么,signal就是結束進程的信號,否則為null。
這兩者中,一者肯定不為null。

注意事項
exit事件觸發(fā)時,子進程的stdio stream可能還打開著。(場景?)此外,nodejs監(jiān)聽了SIGINT和SIGTERM信號,也就是說,nodejs收到這兩個信號時,不會立刻退出,而是先做一些清理的工作,然后重新拋出這兩個信號。(目測此時js可以做清理工作了,比如關閉數(shù)據(jù)庫等。)

SIGINT:interrupt,程序終止信號,通常在用戶按下CTRL+C時發(fā)出,用來通知前臺進程終止進程。
SIGTERM:terminate,程序結束信號,該信號可以被阻塞和處理,通常用來要求程序自己正常退出。shell命令kill缺省產(chǎn)生這個信號。如果信號終止不了,我們才會嘗試SIGKILL(強制終止)。

1.3.3 error

當發(fā)生下列事情時,error就會被觸發(fā)。當error觸發(fā)時,exit可能觸發(fā),也可能不觸發(fā)。(內(nèi)心是崩潰的)

  • 無法衍生該進程。

  • 進程無法kill。

  • 向子進程發(fā)送消息失敗。

1.3.4 message

當采用process.send()來發(fā)送消息時觸發(fā)。

參數(shù)
message,為json對象,或者primitive value;sendHandle,net.Socket對象,或者net.Server對象(熟悉cluster的同學應該對這個不陌生)

1.4 方法

.connected:當調(diào)用.disconnected()時,設為false。代表是否能夠從子進程接收消息,或者對子進程發(fā)送消息。

.disconnect() :關閉父進程、子進程之間的IPC通道。當這個方法被調(diào)用時,disconnect事件就會觸發(fā)。如果子進程是node實例(通過child_process.fork()創(chuàng)建),那么在子進程內(nèi)部也可以主動調(diào)用process.disconnect()來終止IPC通道。


三、NodeJS多線程

應對單線程問題,通常使用多進程的方式來模擬多線程

1. 單線程問題

  • 對 cpu 利用不足

  • 某個未捕獲的異常可能會導致整個程序的退出

2. Node 線程

  • Node 進程占用了 7 個線程

  • Node 中最核心的是 v8 引擎,在 Node 啟動后,會創(chuàng)建 v8 的實例,這個實例是多線程的

    • 主線程:編譯、執(zhí)行代碼

    • 編譯/優(yōu)化線程:在主線程執(zhí)行的時候,可以優(yōu)化代碼

    • 分析器線程:記錄分析代碼運行時間,為 Crankshaft 優(yōu)化代碼執(zhí)行提供依據(jù)

    • 垃圾回收的幾個線程

  • JavaScript 的執(zhí)行是單線程的,但 Javascript 的宿主環(huán)境,無論是 Node 還是瀏覽器都是多線程的。

Javascript 為什么是單線程?
這個問題需要從瀏覽器說起,在瀏覽器環(huán)境中對于 DOM 的操作,試想如果多個線程來對同一個 DOM 操作是不是就亂了呢,那也就意味著對于DOM的操作只能是單線程,避免 DOM 渲染沖突。在瀏覽器環(huán)境中 UI 渲染線程和 JS 執(zhí)行引擎是互斥的,一方在執(zhí)行時都會導致另一方被掛起,這是由 JS 引擎所決定的。

3. 異步 IO

  • Node 中有一些 IO 操作(DNS,F(xiàn)S)和一些 CPU 密集計算(Zlib,Crypto)會啟用 Node 的線程池

  • 線程池默認大小為 4,可以手動更改線程池默認大小

process.env.UV_THREADPOOL_SIZE = 64

4. 真 Node 多線程

4.1 worker_threads核心模塊

  • Node 10.5.0 的發(fā)布,給出了一個實驗性質(zhì)的模塊 worker_threads 給 Node 提供真正的多線程能力

  • worker_thread 模塊中有 4 個對象和 2 個類

    • isMainThread: 是否是主線程,源碼中是通過 threadId === 0 進行判斷的。

    • MessagePort: 用于線程之間的通信,繼承自 EventEmitter。

    • MessageChannel: 用于創(chuàng)建異步、雙向通信的通道實例。

    • threadId: 線程 ID。

    • Worker: 用于在主線程中創(chuàng)建子線程。第一個參數(shù)為 filename,表示子線程執(zhí)行的入口。

    • parentPort: 在 worker 線程里是表示父進程的 MessagePort 類型的對象,在主線程里為 null

    • workerData: 用于在主進程中向子進程傳遞數(shù)據(jù)(data 副本)

const {
  isMainThread,
  parentPort,
  workerData,
  threadId,
  MessageChannel,
  MessagePort,
  Worker
} = require('worker_threads');

function mainThread() {
  for (let i = 0; i < 5; i++) {
    const worker = new Worker(__filename, { workerData: i });
    worker.on('exit', code => { 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調(diào)度的最小單位


五、 知識拓展

1. IPC

IPC (Inter-process communication) 即進程間通信,由于每個進程創(chuàng)建之后都有自己的獨立地址空間,實現(xiàn) IPC 的目的就是為了進程之間資源共享訪問。

實現(xiàn) IPC 的方式有多種:管道、消息隊列、信號量、Domain Socket,Node.js 通過 pipe 來實現(xiàn)。

NodeJS中的進程管理怎么實現(xiàn)

實際上,父進程會在創(chuàng)建子進程之前,會先創(chuàng)建 IPC 通道并監(jiān)聽這個 IPC,然后再創(chuàng)建子進程,通過環(huán)境變量(NODE_CHANNEL_FD)告訴子進程和 IPC 通道相關的文件描述符,子進程啟動的時候根據(jù)文件描述符連接 IPC 通道,從而和父進程建立連接。

NodeJS中的進程管理怎么實現(xiàn)

2. 句柄傳遞

句柄是一種可以用來標識資源的引用的,它的內(nèi)部包含了指向?qū)ο蟮奈募Y源描述符。

一般情況下,當我們想要將多個進程監(jiān)聽到一個端口下,可能會考慮使用主進程代理的方式處理:

NodeJS中的進程管理怎么實現(xiàn)

然而,這種代理方案會導致每次請求的接收和代理轉發(fā)用掉兩個文件描述符,而系統(tǒng)的文件描述符是有限的,這種方式會影響系統(tǒng)的擴展能力。

所以,為什么要使用句柄?原因是在實際應用場景下,建立 IPC 通信后可能會涉及到比較復雜的數(shù)據(jù)處理場景,句柄可以作為 send() 方法的第二個可選參數(shù)傳入,也就是說可以直接將資源的標識通過 IPC 傳輸,避免了上面所說的代理轉發(fā)造成的文件描述符的使用。

NodeJS中的進程管理怎么實現(xiàn)

以下是支持發(fā)送的句柄類型:

  • net.Socket

  • net.Server

  • net.Native

  • dgram.Socket

  • dgram.Native

3.孤兒進程

父進程創(chuàng)建子進程之后,父進程退出了,但是父進程對應的一個或多個子進程還在運行,這些子進程會被系統(tǒng)的 init 進程收養(yǎng),對應的進程 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); 
// 創(chuàng)建子進程之后,主進程退出,此時創(chuàng)建的 worker 進程會成為孤兒進程

控制臺進行測試,輸出當前工作進程 pid 和 父進程 ppid

NodeJS中的進程管理怎么實現(xiàn)

由于在 master.js 里退出了父進程,活動監(jiān)視器所顯示的也就只有工作進程。

NodeJS中的進程管理怎么實現(xiàn)

再次驗證,打開控制臺調(diào)用接口,可以看到工作進程 5611 對應的 ppid 為 1(為 init 進程),此時已經(jīng)成為了孤兒進程

NodeJS中的進程管理怎么實現(xiàn)

4. 守護進程

守護進程運行在后臺不受終端的影響,什么意思呢?
Node.js 開發(fā)的同學們可能熟悉,當我們打開終端執(zhí)行 node app.js 開啟一個服務進程之后,這個終端就會一直被占用,如果關掉終端,服務就會斷掉,即前臺運行模式
如果采用守護進程進程方式,這個終端我執(zhí)行 node app.js 開啟一個服務進程之后,我還可以在這個終端上做些別的事情,且不會相互影響。

4.1 創(chuàng)建步驟

  • 創(chuàng)建子進程

  • 在子進程中創(chuàng)建新會話(調(diào)用系統(tǒng)函數(shù) setsid)

  • 改變子進程工作目錄(如:“/” 或 “/usr/ 等)

  • 父進程終止

4.2 Node.js 編寫守護進程 Demo及測試

  • index.js 文件里的處理邏輯使用 spawn 創(chuàng)建子進程完成了上面的第一步操作。

  • 設置 options.detached 為 true 可以使子進程在父進程退出后繼續(xù)運行(系統(tǒng)層會調(diào)用 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 秒執(zhí)行一次,使得這個資源不會退出,同時寫入日志到子進程當前工作目錄下

/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);


NodeJS中的進程管理怎么實現(xiàn)

NodeJS中的進程管理怎么實現(xiàn)

4.3 守護進程總結

在實際工作中對于守護進程并不陌生,例如 PM2、Egg-Cluster 等,以上只是一個簡單的 Demo 對守護進程做了一個說明,在實際工作中對守護進程的健壯性要求還是很高的,例如:進程的異常監(jiān)聽、工作進程管理調(diào)度、進程掛掉之后重啟等等,這些還需要去不斷思考。

5. 進程的當前工作目錄

目錄是什么?

進程的當前工作目錄可以通過 process.cwd() 命令獲取,默認為當前啟動的目錄,如果是創(chuàng)建子進程則繼承于父進程的目錄,可通過 process.chdir() 命令重置,例如通過 spawn 命令創(chuàng)建的子進程可以指定 cwd 選項設置子進程的工作目錄。

有什么作用?

例如,通過 fs 讀取文件,如果設置為相對路徑則相對于當前進程啟動的目錄進行查找,所以,啟動目錄設置有誤的情況下將無法得到正確的結果。還有一種情況程序里引用第三方模塊也是根據(jù)當前進程啟動的目錄來進行查找的。

// 示例
process.chdir('/Users/may/Documents/test/') // 設置當前進程目錄

console.log(process.cwd()); // 獲取當前進程目錄

到此,關于“NodeJS中的進程管理怎么實現(xiàn)”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續(xù)學習更多相關知識,請繼續(xù)關注億速云網(wǎng)站,小編會繼續(xù)努力為大家?guī)砀鄬嵱玫奈恼拢?/p>

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內(nèi)容。

AI