這篇文章主要介紹“NodeJS中的進(jìn)程管理怎么實(shí)現(xiàn)”,在日常操作中,相信很多人在NodeJS中的進(jìn)程管理怎么實(shí)現(xiàn)問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”NodeJS中的進(jìn)程管理怎么實(shí)現(xiàn)”的疑惑有所幫助!接下來,請跟著小編一起來學(xué)習(xí)吧!
在墊江等地區(qū),都構(gòu)建了全面的區(qū)域性戰(zhàn)略布局,加強(qiáng)發(fā)展的系統(tǒng)性、市場前瞻性、產(chǎn)品創(chuàng)新能力,以專注、極致的服務(wù)理念,為客戶提供網(wǎng)站設(shè)計(jì)、成都網(wǎng)站建設(shè) 網(wǎng)站設(shè)計(jì)制作按需規(guī)劃網(wǎng)站,公司網(wǎng)站建設(shè),企業(yè)網(wǎng)站建設(shè),成都品牌網(wǎng)站建設(shè),成都營銷網(wǎng)站建設(shè),成都外貿(mào)網(wǎng)站制作,墊江網(wǎng)站建設(shè)費(fèi)用合理。
熟悉 js 的朋友都知道,js 是單線程
的,在 Node 中,采用的是 多進(jìn)程單線程的模型。由于javascript單線程的限制,在多核服務(wù)器上,我們往往需要啟動(dòng)多個(gè)進(jìn)程才能最大化服務(wù)器性能。
Node.js 進(jìn)程集群可用于運(yùn)行多個(gè) Node.js 實(shí)例,這些實(shí)例可以在其應(yīng)用程序線程之間分配工作負(fù)載。 當(dāng)不需要進(jìn)程隔離時(shí),請改用 worker_threads
模塊,它允許在單個(gè) Node.js 實(shí)例中運(yùn)行多個(gè)應(yīng)用程序線程。
進(jìn)程總數(shù),其中一個(gè)主進(jìn)程,cpu 個(gè)數(shù) x cpu 核數(shù) 個(gè) 子進(jìn)程
無論 child_process 還是 cluster,都不是多線程模型,而是多進(jìn)程模型
應(yīng)對單線程問題,通常使用多進(jìn)程的方式來模擬多線程
Node 在 V0.8 版本之后引入了 cluster模塊,通過一個(gè)主進(jìn)程 (master) 管理多個(gè)子進(jìn)程 (worker) 的方式實(shí)現(xiàn)集群
。
集群模塊可以輕松創(chuàng)建共享服務(wù)器端口的子進(jìn)程。
cluster 底層是 child_process 模塊,除了可以發(fā)送普通消息,還可以發(fā)送底層對象
TCP
、UDP
等,cluster
模塊是child_process
模塊和net
模塊的組合應(yīng)用。 cluster 啟動(dòng)時(shí),內(nèi)部會(huì)啟動(dòng) TCP 服務(wù)器,將這個(gè) TCP 服務(wù)器端 socket 的文件描述符發(fā)給工作進(jìn)程。
在 cluster
模塊應(yīng)用中,一個(gè)主進(jìn)程只能管理一組工作進(jìn)程
,其運(yùn)作模式?jīng)]有 child_process
模塊那么靈活,但是更加穩(wěn)定:
const cluster = require('cluster')復(fù)
.isMaster
標(biāo)識(shí)主進(jìn)程, Node<16
.isPrimary
標(biāo)識(shí)主進(jìn)程, Node>16
.isWorker
標(biāo)識(shí)子進(jìn)程
.worker
對當(dāng)前工作進(jìn)程對象的引用【子進(jìn)程中】
.workers
存儲(chǔ)活動(dòng)工作進(jìn)程對象的哈希,以 id
字段為鍵。 這樣可以很容易地遍歷所有工作進(jìn)程。 它僅在主進(jìn)程中可用。cluster.wokers[id] === worker
【主進(jìn)程中】
.settings
只讀, cluster配置項(xiàng)。在調(diào)用 .setupPrimary()或.fork()方法之后,此設(shè)置對象將包含設(shè)置,包括默認(rèn)值。之前為空對象。此對象不應(yīng)手動(dòng)更改或設(shè)置。
cluster.settings
配置項(xiàng)詳情:- `execArgv`傳給 Node.js 可執(zhí)行文件的字符串參數(shù)列表。 **默認(rèn)值:** `process.execArgv`。 - `exec` 工作進(jìn)程文件的文件路徑。 **默認(rèn)值:** `process.argv[1]`。 - `args` 傳給工作進(jìn)程的字符串參數(shù)。 **默認(rèn)值:**`process.argv.slice(2)`。 - `cwd` 工作進(jìn)程的當(dāng)前工作目錄。 **默認(rèn)值:** `undefined` (從父進(jìn)程繼承)。 - `serialization` 指定用于在進(jìn)程之間發(fā)送消息的序列化類型。 可能的值為 `'json'` 和 `'advanced'`。 **默認(rèn)值:** `false`。 - `silent` 是否將輸出發(fā)送到父進(jìn)程的標(biāo)準(zhǔn)輸入輸出。 **默認(rèn)值:** `false`。 - `stdio` 配置衍生進(jìn)程的標(biāo)準(zhǔn)輸入輸出。 由于集群模塊依賴 IPC 來運(yùn)行,因此此配置必須包含 `'ipc'` 條目。 提供此選項(xiàng)時(shí),它會(huì)覆蓋 `silent`。 - `uid` 設(shè)置進(jìn)程的用戶標(biāo)識(shí)。 - `gid` 設(shè)置進(jìn)程的群組標(biāo)識(shí)。 - `inspectPort` | 設(shè)置工作進(jìn)程的檢查器端口。 這可以是數(shù)字,也可以是不帶參數(shù)并返回?cái)?shù)字的函數(shù)。 默認(rèn)情況下,每個(gè)工作進(jìn)程都有自己的端口,從主進(jìn)程的 `process.debugPort` 開始遞增。 - `windowsHide` 隱藏通常在 Windows 系統(tǒng)上創(chuàng)建的衍生進(jìn)程控制臺(tái)窗口。 **默認(rèn)值:** `false`。
.fork([env])
衍生新的工作進(jìn)程【主進(jìn)程中】
.setupPrimary([settings])
Node>16
.setupMaster([settings])
用于更改默認(rèn)的 'fork' 行為,用后設(shè)置將出現(xiàn)在 cluster.settings
中。任何設(shè)置更改只會(huì)影響未來對 .fork()
的調(diào)用,而不會(huì)影響已經(jīng)運(yùn)行的工作進(jìn)程。上述默認(rèn)值僅適用于第一次調(diào)用。Node 小于 16【主進(jìn)程中】
.disconnect([callback])
當(dāng)所有工作進(jìn)程斷開連接并關(guān)閉句柄時(shí)調(diào)用【主進(jìn)程中】
為了讓集群更加穩(wěn)定和健壯,cluster
模塊也暴露了許多事件:
'message'
事件, 當(dāng)集群主進(jìn)程接收到來自任何工作進(jìn)程的消息時(shí)觸發(fā)。
'exit'
事件, 當(dāng)任何工作進(jìn)程死亡時(shí),則集群模塊將觸發(fā) 'exit'
事件。
cluster.on('exit', (worker, code, signal) => { console.log('worker %d died (%s). restarting...', worker.process.pid, signal || code); cluster.fork(); });
'listening'
事件,從工作進(jìn)程調(diào)用 listen()
后,當(dāng)服務(wù)器上觸發(fā) 'listening'
事件時(shí),則主進(jìn)程中的 cluster
也將觸發(fā) 'listening'
事件。
cluster.on('listening', (worker, address) => { console.log( `A worker is now connected to ${address.address}:${address.port}`); });
'fork'
事件,當(dāng)新的工作進(jìn)程被衍生時(shí),則集群模塊將觸發(fā) 'fork'
事件。
cluster.on('fork', (worker) => { timeouts[worker.id] = setTimeout(errorMsg, 2000); });
'setup'
事件,每次調(diào)用 .setupPrimary()
時(shí)觸發(fā)。
disconnect
事件,在工作進(jìn)程 IPC 通道斷開連接后觸發(fā)。 當(dāng)工作進(jìn)程正常退出、被殺死、或手動(dòng)斷開連接時(shí)
cluster.on('disconnect', (worker) => { console.log(`The worker #${worker.id} has disconnected`); });
Worker
對象包含了工作進(jìn)程的所有公共的信息和方法。 在主進(jìn)程中,可以使用 cluster.workers
來獲取它。 在工作進(jìn)程中,可以使用 cluster.worker
來獲取它。
.id
工作進(jìn)程標(biāo)識(shí),每個(gè)新的工作進(jìn)程都被賦予了自己唯一的 id,此 id 存儲(chǔ)在 id
。當(dāng)工作進(jìn)程存活時(shí),這是在 cluster.workers
中索引它的鍵。
.process
所有工作進(jìn)程都是使用 child_process.fork()
創(chuàng)建,此函數(shù)返回的對象存儲(chǔ)為 .process
。 在工作進(jìn)程中,存儲(chǔ)了全局的 process
。
.send(message[, sendHandle[, options]][, callback])
向工作進(jìn)程或主進(jìn)程發(fā)送消息,可選擇使用句柄。在主進(jìn)程中,這會(huì)向特定的工作進(jìn)程發(fā)送消息。 它與 ChildProcess.send()
相同。在工作進(jìn)程中,這會(huì)向主進(jìn)程發(fā)送消息。 它與 process.send()
相同。
.destroy()
.kill([signal])
此函數(shù)會(huì)殺死工作進(jìn)程。kill()
函數(shù)在不等待正常斷開連接的情況下殺死工作進(jìn)程,它與 worker.process.kill()
具有相同的行為。為了向后兼容,此方法別名為 worker.destroy()
。
.disconnect([callback])
發(fā)送給工作進(jìn)程,使其調(diào)用自身的 .disconnect()
將關(guān)閉所有服務(wù)器,等待那些服務(wù)器上的 'close'
事件,然后斷開 IPC 通道。
.isConnect()
如果工作進(jìn)程通過其 IPC 通道連接到其主進(jìn)程,則此函數(shù)返回 true
,否則返回 false
。 工作進(jìn)程在創(chuàng)建后連接到其主進(jìn)程。
.isDead()
如果工作進(jìn)程已終止(由于退出或收到信號(hào)),則此函數(shù)返回 true
。 否則,它返回 false
。
為了讓集群更加穩(wěn)定和健壯,cluster
模塊也暴露了許多事件:
'message'
事件, 在工作進(jìn)程中。
cluster.workers[id].on('message', messageHandler);
'exit'
事件, 當(dāng)任何工作進(jìn)程死亡時(shí),則當(dāng)前worker工作進(jìn)程
對象將觸發(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'
事件,從工作進(jìn)程調(diào)用 listen()
,對當(dāng)前工作進(jìn)程進(jìn)行監(jiān)聽。
cluster.fork().on('listening', (address) => { // 工作進(jìn)程正在監(jiān)聽 });
disconnect
事件,在工作進(jìn)程 IPC 通道斷開連接后觸發(fā)。 當(dāng)工作進(jìn)程正常退出、被殺死、或手動(dòng)斷開連接時(shí)
cluster.fork().on('disconnect', () => { //限定于當(dāng)前worker對象觸發(fā) });
Node中主進(jìn)程和子進(jìn)程之間通過進(jìn)程間通信(IPC) 實(shí)現(xiàn)進(jìn)程間的通信,進(jìn)程間通過 .send()
(a.send表示向a發(fā)送)方法發(fā)送消息,監(jiān)聽 message
事件收取信息,這是 cluster模塊
通過集成 EventEmitter
實(shí)現(xiàn)的。還是一個(gè)簡單的官網(wǎng)的進(jìn)程間通信例子
子進(jìn)程:process.on('message')
、process.send()
父進(jìn)程: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'); # 主進(jìn)程 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 // 開啟多進(jìn)程(cpu核心數(shù)) // 衍生工作進(jìn)程。 const numCPUs = require('os').cpus().length; for (let i = 0; i < numCPUs; i++) { console.log(i) cluster.fork(); } // cluster worker 主進(jìn)程與子進(jìn)程通信 for (const id in cluster.workers) { // ***監(jiān)聽來自子進(jìn)程的事件 cluster.workers[id].on('message', messageHandler); // ***向子進(jìn)程發(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 { # 子進(jìn)程 // 工作進(jìn)程可以共享任何 TCP 連接 // 在本示例中,其是 HTTP 服務(wù)器 // Worker processes have a http server. http.Server((req, res) => { res.writeHead(200); res.end('hello world\n'); //****** ?。。?!Notify 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 進(jìn)程之間通信只有消息傳遞,不會(huì)真正的傳遞對象。
send()
方法在發(fā)送消息前,會(huì)將消息組裝成 handle 和 message,這個(gè) message 會(huì)經(jīng)過 JSON.stringify
序列化,也就是說,傳遞句柄的時(shí)候,不會(huì)將整個(gè)對象傳遞過去,在 IPC 通道傳輸?shù)亩际亲址瑐鬏敽笸ㄟ^ JSON.parse
還原成對象。
代碼里有 app.listen(port)
在進(jìn)行 fork 時(shí),為什么多個(gè)進(jìn)程可以監(jiān)聽同一個(gè)端口呢?
原因是主進(jìn)程通過 send() 方法向多個(gè)子進(jìn)程發(fā)送屬于該主進(jìn)程的一個(gè)服務(wù)對象的句柄,所以對于每一個(gè)子進(jìn)程而言,它們在還原句柄之后,得到的服務(wù)對象是一樣的,當(dāng)網(wǎng)絡(luò)請求向服務(wù)端發(fā)起時(shí),進(jìn)程服務(wù)是搶占式的,所以監(jiān)聽相同端口時(shí)不會(huì)引起異常。
看下端口被占用的情況:
# master.js const fork = require('child_process').fork; const cpus = require('os').cpus(); for (let i=0; i # worker.js const http = require('http'); http.createServer((req, res) => { res.end('I am worker, pid: ' + process.pid + ', ppid: ' + process.ppid); }).listen(3000);以上代碼示例,控制臺(tái)執(zhí)行
node master.js
只有一個(gè) worker 可以監(jiān)聽到 3000 端口,其余將會(huì)拋出Error: listen EADDRINUSE :::3000
錯(cuò)誤。
那么多進(jìn)程模式下怎么實(shí)現(xiàn)多進(jìn)程端口監(jiān)聽呢?答案還是有的,通過句柄傳遞 Node.js v0.5.9 版本之后支持進(jìn)程間可
發(fā)送句柄
功能/** * http://nodejs.cn/api/child_process.html#child_process_subprocess_send_message_sendhandle_options_callback * message * sendHandle */ subprocess.send(message, sendHandle)當(dāng)父子進(jìn)程之間建立 IPC 通道之后,通過子進(jìn)程對象的 send 方法發(fā)送消息,第
二個(gè)參數(shù) sendHandle 就是句柄,可以是 TCP套接字、TCP服務(wù)器、UDP套接字等
,為了解決上面多進(jìn)程端口占用問題,我們將主進(jìn)程的 socket 傳遞到子進(jìn)程。# 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 // 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) }); } });驗(yàn)證一番,控制臺(tái)執(zhí)行
node master.js
2.3 進(jìn)程負(fù)載均衡
了解
cluster
的話會(huì)知道,子進(jìn)程是通過cluster.fork()
創(chuàng)建的。在 linux 中,系統(tǒng)原生提供了fork
方法,那么為什么 Node 選擇自己實(shí)現(xiàn)cluster模塊
,而不是直接使用系統(tǒng)原生的方法?主要的原因是以下兩點(diǎn):
fork的進(jìn)程監(jiān)聽同一端口會(huì)導(dǎo)致端口占用錯(cuò)誤
fork的進(jìn)程之間沒有負(fù)載均衡,容易導(dǎo)致驚群現(xiàn)象
在
cluster模塊
中,針對第一個(gè)問題,通過判斷當(dāng)前進(jìn)程是否為master進(jìn)程
,若是,則監(jiān)聽端口,若不是則表示為 fork 的worker進(jìn)程
,不監(jiān)聽端口。針對第二個(gè)問題,
cluster模塊
內(nèi)置了負(fù)載均衡功能,master進(jìn)程
負(fù)責(zé)監(jiān)聽端口接收請求,然后通過調(diào)度算法(默認(rèn)為 Round-Robin,可以通過環(huán)境變量NODE_CLUSTER_SCHED_POLICY
修改調(diào)度算法)分配給對應(yīng)的worker進(jìn)程
。3. 異常捕獲
3.1 未捕獲異常
當(dāng)代碼拋出了異常沒有被捕獲到時(shí),進(jìn)程將會(huì)退出,此時(shí) Node.js 提供了
process.on('uncaughtException', handler)
接口來捕獲它,但是當(dāng)一個(gè) Worker 進(jìn)程遇到未捕獲的異常時(shí),它已經(jīng)處于一個(gè)不確定狀態(tài),此時(shí)我們應(yīng)該讓這個(gè)進(jìn)程優(yōu)雅退出:
關(guān)閉異常 Worker 進(jìn)程所有的 TCP Server(將已有的連接快速斷開,且不再接收新的連接),斷開和 Master 的 IPC 通道,不再接受新的用戶請求。
Master 立刻 fork 一個(gè)新的 Worker 進(jìn)程,保證在線的『工人』總數(shù)不變。
異常 Worker 等待一段時(shí)間,處理完已經(jīng)接受的請求后退出。
+---------+ +---------+ | Worker | | Master | +---------+ +----+----+ | uncaughtException | +------------+ | | | | +---------+ | <----------+ | | Worker | | | +----+----+ | disconnect | fork a new worker | +-------------------------> + ---------------------> | | wait... | | | exit | | +-------------------------> | | | | | die | | | | | |3.2 OOM、系統(tǒng)異常
當(dāng)一個(gè)進(jìn)程出現(xiàn)異常導(dǎo)致 crash 或者 OOM 被系統(tǒng)殺死時(shí),不像未捕獲異常發(fā)生時(shí)我們還有機(jī)會(huì)讓進(jìn)程繼續(xù)執(zhí)行,只能夠讓當(dāng)前進(jìn)程直接退出,Master 立刻 fork 一個(gè)新的 Worker。
二、子進(jìn)程
1. child_process模塊
child_process 模塊提供了衍生子進(jìn)程的能力, 簡單來說就是
執(zhí)行cmd命令的能力
。 默認(rèn)情況下,stdin、 stdout 和 stderr 的管道會(huì)在父 Node.js 進(jìn)程和衍生的子進(jìn)程之間建立
。 這些管道具有有限的(且平臺(tái)特定的)容量。 如果子進(jìn)程寫入 stdout 時(shí)超出該限制且沒有捕獲輸出,則子進(jìn)程會(huì)阻塞并等待管道緩沖區(qū)接受更多的數(shù)據(jù)。 這與 shell 中的管道的行為相同。 如果不消費(fèi)輸出,則使用 { stdio: 'ignore' } 選項(xiàng)。1.1 引入child_process
const cp = require('child_process');1.2 基本概念
通過 API 創(chuàng)建出來的子進(jìn)程和父進(jìn)程沒有任何必然聯(lián)系
4個(gè)異步方法,創(chuàng)建子進(jìn)程:fork、exec、execFile、spawn
spawn(command, args)
:處理一些會(huì)有很多子進(jìn)程 I/O 時(shí)、進(jìn)程會(huì)有大量輸出時(shí)使用
execFile(file, args[, callback])
:只需執(zhí)行一個(gè)外部程序的時(shí)候使用,執(zhí)行速度快,處理用戶輸入相對安全
exec(command, options)
:想直接訪問線程的 shell 命令時(shí)使用,一定要注意用戶輸入
fork(modulePath, args)
:想將一個(gè) Node 進(jìn)程作為一個(gè)獨(dú)立的進(jìn)程來運(yùn)行的時(shí)候使用,使得計(jì)算處理和文件描述器脫離 Node 主進(jìn)程(復(fù)制一個(gè)子進(jìn)程)Node
非 Node
3個(gè)同步方法:
execSync
、execFileSync
、spawnSync
其他三種方法都是
spawn()
的延伸。1.2.1 fork(modulePath, args)函數(shù), 復(fù)制進(jìn)程
fork 方法會(huì)開放一個(gè) IPC 通道,不同的 Node 進(jìn)程進(jìn)行消息傳送
一個(gè)子進(jìn)程消耗 30ms 啟動(dòng)時(shí)間和 10MB 內(nèi)存
記住,衍生的 Node.js 子進(jìn)程獨(dú)立于父進(jìn)程,但兩者之間建立的 IPC 通信通道除外。 每個(gè)進(jìn)程都有自己的內(nèi)存,帶有自己的 V8 實(shí)例
舉個(gè)?
在一個(gè)目錄下新建 worker.js 和 master.js 兩個(gè)文件:
# child.js const t = JSON.parse(process.argv[2]); console.error(`子進(jìn)程 t=${JSON.stringify(t)}`); process.send({hello:`兒子pid=${process.pid} 給爸爸進(jìn)程pid=${process.ppid} 請安`}); process.on('message', (msg)=>{ console.error(`子進(jìn)程 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} 進(jìn)程id=${i}的問候`}); }通過
node parent.js
啟動(dòng) parent.js,然后通過ps aux | grep worker.js
查看進(jìn)程的數(shù)量,我們可以發(fā)現(xiàn),理想狀況下,進(jìn)程的數(shù)量等于 CPU 的核心數(shù),每個(gè)進(jìn)程各自利用一個(gè) CPU 核心。這是經(jīng)典的 Master-Worker 模式(主從模式)
實(shí)際上,fork 進(jìn)程是昂貴的,復(fù)制進(jìn)程的目的是充分利用 CPU 資源,所以 NodeJS 在單線程上使用了事件驅(qū)動(dòng)的方式來解決高并發(fā)的問題。
適用場景
一般用于比較耗時(shí)的場景,并且用node去實(shí)現(xiàn)的,比如下載文件;
fork可以實(shí)現(xiàn)多線程下載:將文件分成多塊,然后每個(gè)進(jìn)程下載一部分,最后拼起來;1.2.2 execFile(file, args[, callback])
會(huì)把輸出結(jié)果緩存好,通過回調(diào)返回最后結(jié)果或者異常信息
const cp = require('child_process'); // 第一個(gè)參數(shù),要運(yùn)行的可執(zhí)行文件的名稱或路徑。這里是echo cp.execFile('echo', ['hello', 'world'], (err, stdout, stderr) => { if (err) { console.error(err); } console.log('stdout: ', stdout); console.log('stderr: ', stderr); });適用場景
比較適合開銷小的任務(wù),更關(guān)注結(jié)果,比如ls等;1.2.3 exec(command, options)
主要用來執(zhí)行一個(gè)shell方法,其內(nèi)部還是調(diào)用了spawn ,不過他有最大緩存限制。
只有一個(gè)字符串命令
和 shell 一模一樣
const cp = require('child_process'); cp.exec(`cat ${__dirname}/messy.txt | sort | uniq`, (err, stdout, stderr) => { console.log(stdout); });適用場景
比較適合開銷小的任務(wù),更關(guān)注結(jié)果,比如ls等;1.2.4 spawn(command, args)
通過流可以使用有大量數(shù)據(jù)輸出的外部應(yīng)用,節(jié)約內(nèi)存
使用流提高數(shù)據(jù)響應(yīng)效率
spawn 方法返回一個(gè) I/O 的流接口
單一任務(wù)
const cp = require('child_process'); const child = cp.spawn('echo', ['hello', 'world']); child.on('error', console.error); # 輸出是流,輸出到主進(jìn)程stdout,控制臺(tái) child.stdout.pipe(process.stdout); child.stderr.pipe(process.stderr);多任務(wù)串聯(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是流式的,所以適合耗時(shí)任務(wù),比如執(zhí)行npm install,打印install的過程1.3 各種事件
1.3.1 close
在進(jìn)程已結(jié)束并且子進(jìn)程的標(biāo)準(zhǔn)輸入輸出流(sdtio)已關(guān)閉之后,則觸發(fā)
'close'
事件。這個(gè)事件跟exit
不同,因?yàn)槎鄠€(gè)進(jìn)程可以共享同個(gè)stdio流。參數(shù):
code(退出碼,如果子進(jìn)程是自己退出的話)
signal(結(jié)束子進(jìn)程的信號(hào))
問題:code一定是有的嗎?
(從對code的注解來看好像不是)比如用kill
殺死子進(jìn)程,那么,code是?1.3.2 exit
參數(shù):
code、signal,如果子進(jìn)程是自己退出的,那么code
就是退出碼,否則為null;
如果子進(jìn)程是通過信號(hào)結(jié)束的,那么,signal
就是結(jié)束進(jìn)程的信號(hào),否則為null。
這兩者中,一者肯定不為null。注意事項(xiàng):
exit
事件觸發(fā)時(shí),子進(jìn)程的stdio stream可能還打開著。(場景?)此外,nodejs監(jiān)聽了SIGINT和SIGTERM信號(hào),也就是說,nodejs收到這兩個(gè)信號(hào)時(shí),不會(huì)立刻退出,而是先做一些清理的工作,然后重新拋出這兩個(gè)信號(hào)。(目測此時(shí)js可以做清理工作了,比如關(guān)閉數(shù)據(jù)庫等。)
SIGINT
:interrupt,程序終止信號(hào),通常在用戶按下CTRL+C時(shí)發(fā)出,用來通知前臺(tái)進(jìn)程終止進(jìn)程。SIGTERM
:terminate,程序結(jié)束信號(hào),該信號(hào)可以被阻塞和處理,通常用來要求程序自己正常退出。shell命令kill缺省產(chǎn)生這個(gè)信號(hào)。如果信號(hào)終止不了,我們才會(huì)嘗試SIGKILL(強(qiáng)制終止)。1.3.3 error
當(dāng)發(fā)生下列事情時(shí),error就會(huì)被觸發(fā)。當(dāng)error觸發(fā)時(shí),exit可能觸發(fā),也可能不觸發(fā)。(內(nèi)心是崩潰的)
無法衍生該進(jìn)程。
進(jìn)程無法kill。
向子進(jìn)程發(fā)送消息失敗。
1.3.4 message
當(dāng)采用
process.send()
來發(fā)送消息時(shí)觸發(fā)。參數(shù):
message
,為json對象,或者primitive value;sendHandle
,net.Socket對象,或者net.Server對象(熟悉cluster的同學(xué)應(yīng)該對這個(gè)不陌生)1.4 方法
.connected:當(dāng)調(diào)用
.disconnected()
時(shí),設(shè)為false。代表是否能夠從子進(jìn)程接收消息,或者對子進(jìn)程發(fā)送消息。.disconnect():關(guān)閉父進(jìn)程、子進(jìn)程之間的IPC通道。當(dāng)這個(gè)方法被調(diào)用時(shí),
disconnect
事件就會(huì)觸發(fā)。如果子進(jìn)程是node實(shí)例(通過child_process.fork()創(chuàng)建),那么在子進(jìn)程內(nèi)部也可以主動(dòng)調(diào)用process.disconnect()
來終止IPC通道。三、NodeJS多線程
應(yīng)對單線程問題,通常使用多進(jìn)程的方式來模擬多線程
1. 單線程問題
對 cpu 利用不足
某個(gè)未捕獲的異??赡軙?huì)導(dǎo)致整個(gè)程序的退出
2. Node 線程
Node 進(jìn)程占用了 7 個(gè)線程
Node 中最核心的是 v8 引擎,在 Node 啟動(dòng)后,會(huì)創(chuàng)建 v8 的實(shí)例,這個(gè)實(shí)例是多線程的
主線程:編譯、執(zhí)行代碼
編譯/優(yōu)化線程:在主線程執(zhí)行的時(shí)候,可以優(yōu)化代碼
分析器線程:記錄分析代碼運(yùn)行時(shí)間,為 Crankshaft 優(yōu)化代碼執(zhí)行提供依據(jù)
垃圾回收的幾個(gè)線程
JavaScript 的執(zhí)行是
單線程
的,但 Javascript 的宿主環(huán)境,無論是 Node 還是瀏覽器都是多線程的。Javascript 為什么是單線程?
這個(gè)問題需要從瀏覽器說起,在瀏覽器環(huán)境中對于 DOM 的操作,試想如果多個(gè)線程來對同一個(gè) DOM 操作是不是就亂了呢,那也就意味著對于DOM的操作只能是單線程,避免 DOM 渲染沖突。在瀏覽器環(huán)境中 UI 渲染線程和 JS 執(zhí)行引擎是互斥的,一方在執(zhí)行時(shí)都會(huì)導(dǎo)致另一方被掛起,這是由 JS 引擎所決定的。3. 異步 IO
Node 中有一些 IO 操作(DNS,F(xiàn)S)和一些 CPU 密集計(jì)算(Zlib,Crypto)會(huì)啟用 Node 的線程池
線程池默認(rèn)大小為 4,可以手動(dòng)更改線程池默認(rèn)大小
process.env.UV_THREADPOOL_SIZE = 644. 真 Node 多線程
4.1 worker_threads核心模塊
Node 10.5.0 的發(fā)布,給出了一個(gè)實(shí)驗(yàn)性質(zhì)的模塊
worker_threads
給 Node 提供真正的多線程能力worker_thread 模塊中有 4 個(gè)對象和 2 個(gè)類
isMainThread: 是否是主線程,源碼中是通過 threadId === 0 進(jìn)行判斷的。
MessagePort: 用于線程之間的通信,繼承自 EventEmitter。
MessageChannel: 用于創(chuàng)建異步、雙向通信的通道實(shí)例。
threadId: 線程 ID。
Worker: 用于在主線程中創(chuàng)建子線程。第一個(gè)參數(shù)為 filename,表示子線程執(zhí)行的入口。
parentPort: 在 worker 線程里是表示父進(jìn)程的 MessagePort 類型的對象,在主線程里為 null
workerData: 用于在主進(jìn)程中向子進(jìn)程傳遞數(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(); }); }四、 多進(jìn)程 vs 多線程
進(jìn)程是資源分配的最小單位,線程是CPU調(diào)度的最小單位
五、 知識(shí)拓展
1. IPC
IPC (Inter-process communication) 即
進(jìn)程間通信
,由于每個(gè)進(jìn)程創(chuàng)建之后都有自己的獨(dú)立地址空間,實(shí)現(xiàn) IPC 的目的就是為了進(jìn)程之間資源共享訪問。實(shí)現(xiàn) IPC 的方式有多種:管道、消息隊(duì)列、信號(hào)量、Domain Socket,Node.js 通過 pipe 來實(shí)現(xiàn)。
實(shí)際上,父進(jìn)程會(huì)在創(chuàng)建子進(jìn)程之前,會(huì)先創(chuàng)建 IPC 通道并監(jiān)聽這個(gè) IPC,然后再創(chuàng)建子進(jìn)程,通過環(huán)境變量(NODE_CHANNEL_FD)告訴子進(jìn)程和 IPC 通道相關(guān)的文件描述符,子進(jìn)程啟動(dòng)的時(shí)候根據(jù)文件描述符連接 IPC 通道,從而和父進(jìn)程建立連接。
2. 句柄傳遞
句柄是一種可以用來標(biāo)識(shí)資源的引用的,它的內(nèi)部包含了指向?qū)ο蟮奈募Y源描述符。
一般情況下,當(dāng)我們想要將多個(gè)進(jìn)程監(jiān)聽到一個(gè)端口下,可能會(huì)考慮使用主進(jìn)程代理的方式處理:
然而,這種代理方案會(huì)導(dǎo)致每次請求的接收和代理轉(zhuǎn)發(fā)用掉兩個(gè)文件描述符,而系統(tǒng)的文件描述符是有限的,這種方式會(huì)影響系統(tǒng)的擴(kuò)展能力。
所以,為什么要使用句柄?原因是在實(shí)際應(yīng)用場景下,建立 IPC 通信后可能會(huì)涉及到比較復(fù)雜的數(shù)據(jù)處理場景,句柄可以作為
send()
方法的第二個(gè)可選參數(shù)傳入,也就是說可以直接將資源的標(biāo)識(shí)通過 IPC 傳輸,避免了上面所說的代理轉(zhuǎn)發(fā)造成的文件描述符的使用。以下是支持發(fā)送的句柄類型:
net.Socket
net.Server
net.Native
dgram.Socket
dgram.Native
3.孤兒進(jìn)程
父進(jìn)程創(chuàng)建子進(jìn)程之后,父進(jìn)程退出了,但是父進(jìn)程對應(yīng)的一個(gè)或多個(gè)子進(jìn)程還在運(yùn)行,這些子進(jìn)程會(huì)被系統(tǒng)的 init 進(jìn)程收養(yǎng),對應(yīng)的進(jìn)程 ppid 為 1,這就是孤兒進(jìn)程。通過以下代碼示例說明。
# worker.js const http = require('http'); const server = http.createServer((req, res) => { res.end('I am worker, pid: ' + process.pid + ', ppid: ' + process.ppid); // 記錄當(dāng)前工作進(jìn)程 pid 及父進(jìn)程 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)建子進(jìn)程之后,主進(jìn)程退出,此時(shí)創(chuàng)建的 worker 進(jìn)程會(huì)成為孤兒進(jìn)程控制臺(tái)進(jìn)行測試,輸出當(dāng)前工作進(jìn)程 pid 和 父進(jìn)程 ppid
由于在 master.js 里退出了父進(jìn)程,活動(dòng)監(jiān)視器所顯示的也就只有工作進(jìn)程。
再次驗(yàn)證,打開控制臺(tái)調(diào)用接口,可以看到工作進(jìn)程 5611 對應(yīng)的 ppid 為 1(為 init 進(jìn)程),此時(shí)已經(jīng)成為了孤兒進(jìn)程
4. 守護(hù)進(jìn)程
守護(hù)進(jìn)程運(yùn)行在后臺(tái)不受終端的影響,什么意思呢?
Node.js 開發(fā)的同學(xué)們可能熟悉,當(dāng)我們打開終端執(zhí)行node app.js
開啟一個(gè)服務(wù)進(jìn)程之后,這個(gè)終端就會(huì)一直被占用,如果關(guān)掉終端,服務(wù)就會(huì)斷掉,即前臺(tái)運(yùn)行模式
。
如果采用守護(hù)進(jìn)程進(jìn)程方式,這個(gè)終端我執(zhí)行node app.js
開啟一個(gè)服務(wù)進(jìn)程之后,我還可以在這個(gè)終端上做些別的事情,且不會(huì)相互影響。4.1 創(chuàng)建步驟
創(chuàng)建子進(jìn)程
在子進(jìn)程中創(chuàng)建新會(huì)話(調(diào)用系統(tǒng)函數(shù) setsid)
改變子進(jìn)程工作目錄(如:“/” 或 “/usr/ 等)
父進(jìn)程終止
4.2 Node.js 編寫守護(hù)進(jìn)程 Demo及測試
index.js 文件里的處理邏輯使用 spawn 創(chuàng)建子進(jìn)程完成了上面的第一步操作。
設(shè)置
options.detached
為 true 可以使子進(jìn)程在父進(jìn)程退出后繼續(xù)運(yùn)行(系統(tǒng)層會(huì)調(diào)用 setsid 方法),這是第二步操作。options.cwd 指定當(dāng)前子進(jìn)程工作目錄若不做設(shè)置默認(rèn)繼承當(dāng)前工作目錄,這是第三步操作。
運(yùn)行 daemon.unref() 退出父進(jìn)程,這是第四步操作。
// index.js const spawn = require('child_process').spawn; function startDaemon() { const daemon = spawn('node', ['daemon.js'], { cwd: '/usr', detached : true, stdio: 'ignore', }); console.log('守護(hù)進(jìn)程開啟 父進(jìn)程 pid: %s, 守護(hù)進(jìn)程 pid: %s', process.pid, daemon.pid); daemon.unref(); } startDaemon()daemon.js 文件里處理邏輯開啟一個(gè)定時(shí)器每 10 秒執(zhí)行一次,使得這個(gè)資源不會(huì)退出,同時(shí)寫入日志到子進(jìn)程當(dāng)前工作目錄下
/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);4.3 守護(hù)進(jìn)程總結(jié)
在實(shí)際工作中對于守護(hù)進(jìn)程并不陌生,例如 PM2、Egg-Cluster 等,以上只是一個(gè)簡單的 Demo 對守護(hù)進(jìn)程做了一個(gè)說明,在實(shí)際工作中對守護(hù)進(jìn)程的健壯性要求還是很高的,例如:進(jìn)程的異常監(jiān)聽、工作進(jìn)程管理調(diào)度、進(jìn)程掛掉之后重啟等等,這些還需要去不斷思考。
5. 進(jìn)程的當(dāng)前工作目錄
目錄是什么?
進(jìn)程的當(dāng)前工作目錄可以通過
process.cwd()
命令獲取,默認(rèn)為當(dāng)前啟動(dòng)的目錄,如果是創(chuàng)建子進(jìn)程則繼承于父進(jìn)程的目錄,可通過process.chdir()
命令重置,例如通過 spawn 命令創(chuàng)建的子進(jìn)程可以指定 cwd 選項(xiàng)設(shè)置子進(jìn)程的工作目錄。有什么作用?
例如,通過 fs 讀取文件,如果設(shè)置為相對路徑則相對于當(dāng)前進(jìn)程啟動(dòng)的目錄進(jìn)行查找,所以,啟動(dòng)目錄設(shè)置有誤的情況下將無法得到正確的結(jié)果。還有一種情況程序里引用第三方模塊也是根據(jù)當(dāng)前進(jìn)程啟動(dòng)的目錄來進(jìn)行查找的。
// 示例 process.chdir('/Users/may/Documents/test/') // 設(shè)置當(dāng)前進(jìn)程目錄 console.log(process.cwd()); // 獲取當(dāng)前進(jìn)程目錄到此,關(guān)于“NodeJS中的進(jìn)程管理怎么實(shí)現(xiàn)”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實(shí)踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識(shí),請繼續(xù)關(guān)注創(chuàng)新互聯(lián)網(wǎng)站,小編會(huì)繼續(xù)努力為大家?guī)砀鄬?shí)用的文章!
名稱欄目:NodeJS中的進(jìn)程管理怎么實(shí)現(xiàn)
分享地址:http://weahome.cn/article/ggihoe.html