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