標籤:version 模式 close 傳遞 核心數 log rgs span 經驗
NodeJs是一個單進程的語言,不能像Java那樣可以建立多線程來並發執行。當然在大部分情況下,NodeJs是不需要並發執行的,因為它是事件驅動性永不阻塞。但單進程也有個問題就是不能充分利用CPU的多核機制,根據前人的經驗,可以通過建立多個進程來充分利用CPU多核,並且Node通過了child_process模組來建立完成多進程的操作。
child_process模組給予node任意建立子進程的能力,node官方文檔對於child_proces模組給出了四種方法,映射到作業系統其實都是建立子進程。但對於開發人員而已,這幾種方法的api有點不同
child_process.exec(command[, options][, callback]) 啟動
子進程來執行shell命令,可以通過回調參數來擷取指令碼shell執行結果
const { exec } = require(‘child_process‘);exec(‘cat *.js bad_file | wc -l‘, (error, stdout, stderr) => { if (error) { console.error(`exec error: ${error}`); return; } console.log(`stdout: ${stdout}`); console.log(`stderr: ${stderr}`);});
child_process.execfile(file[, args][, options][, callback])
與exec類型不同的是,不衍生一個 shell,而是,指定的可執行檔 file 被直接衍生為一個新進程,這使得它比 child_process.exec() 更高效。 由於沒有衍生 shell,因此不支援像 I/O 重新導向和檔案尋找這樣的行為。
const { execFile } = require(‘child_process‘);const child = execFile(‘node‘, [‘--version‘], (error, stdout, stderr) => { if (error) { throw error; } console.log(stdout);});
與另外另個命令不同的是接受一個函數,如果提供了一個 callback 函數,則它被調用時會帶上參數 (error, stdout, stderr)。 當成功時,error 會是 null。 當失敗時,error 會是一個 Error執行個體。 error.code 屬性會是子進程的退出碼,error.signal 會被設為終止進程的訊號。 除 0 以外的任何退出碼都被認為是一個錯誤。
exec()與execfile()在建立的時候可以指定timeout屬性設定逾時時間,一旦逾時會被殺死
如果使用execfile()執行可執行檔,那麼頭部一定是#!/usr/bin/env node
child_process.spawn(command[, args][, options])
僅僅執行一個shell命令,不需要擷取執行結果
const { spawn } = require(‘child_process‘);const ls = spawn(‘ls‘, [‘-lh‘, ‘/usr‘]);ls.stdout.on(‘data‘, (data) => { console.log(`stdout: ${data}`);});ls.stderr.on(‘data‘, (data) => { console.log(`stderr: ${data}`);});ls.on(‘close‘, (code) => { console.log(`子進程退出碼:${code}`);});// 例子,一種執行 ‘ps ax | grep ssh‘ 的方法:const { spawn } = require(‘child_process‘);const ps = spawn(‘ps‘, [‘ax‘]);const grep = spawn(‘grep‘, [‘ssh‘]);ps.stdout.on(‘data‘, (data) => { grep.stdin.write(data);});ps.stderr.on(‘data‘, (data) => { console.log(`ps stderr: ${data}`);});ps.on(‘close‘, (code) => { if (code !== 0) { console.log(`ps 進程退出碼:${code}`); } grep.stdin.end();});grep.stdout.on(‘data‘, (data) => { console.log(data.toString());});grep.stderr.on(‘data‘, (data) => { console.log(`grep stderr: ${data}`);});grep.on(‘close‘, (code) => { if (code !== 0) { console.log(`grep 進程退出碼:${code}`); }});
child_process.fork(modulePath[, args][, options])
與另外三個不同的是它開啟的是一個node進程,執行的只能是js檔案。並通過建立 IPC 通訊通道來調用指定的模組,該通道允許父進程與子進程之間相互發送資訊。
處理序間通訊
node 與 子進程之間的通訊是使用IPC管道機制完成。如果子進程
也是node進程(使用fork),則可以使用監聽message事件和使用send()來通訊。
main.js
var cp = require(‘child_process‘);//只有使用fork才可以使用message事件和send()方法var n = cp.fork(‘./child.js‘);n.on(‘message‘,function(m){ console.log(m);})n.send({"message":"hello"});
child.js
var cp = require(‘child_process‘);process.on(‘message‘,function(m){ console.log(m);})process.send({"message":"hello I am child"})
父子進程之間會建立IPC通道,message事件和send()便利用IPC通道通訊.
控制代碼傳遞
學會如何建立子進程後,我們建立一個HTTP服務並啟動多個進程來共同
做到充分利用CPU多核。
worker.js
var http = require(‘http‘);http.createServer(function(req,res){ res.end(‘Hello,World‘); //監聽隨機連接埠}).listen(Math.round((1+Math.random())*1000),‘127.0.0.1‘);
main.js
var fork = require(‘child_process‘).fork;var cpus = require(‘os‘).cpus();for(var i=0;i<cpus.length;i++){ fork(‘./worker.js‘);}
上述代碼會根據你的cpu核心數來建立對應數量的fork進程,每個進程監聽一個隨機連接埠來提供HTTP服務。
上述就完成了一個典型的Master-Worker主從複製模式。在分布式應用中用於平行處理業務,具備良好的收縮性和穩定性。這裡需要注意,fork一個進程代價是昂貴的,node單進程事件驅動具有很好的效能。此例的多個fork進程是為了充分利用CPU的核,並非解決並發問題.
上述樣本有個不太好的地方就是佔有了太多連接埠,那麼能不能對於多個子進程全部使用同一個連接埠從而對外提供http服務也只是使用這一個連接埠。嘗試將上述的連接埠隨機數改為8080,啟動會發現拋出如下異常。
events.js:72 throw er;//Unhandled ‘error‘ eventError:listen EADDRINUSEXXXX
拋出連接埠被佔有的異常,這意味著只有一個worker.js才能監聽8080連接埠,而其餘的會拋出異常。
如果要解決對外提供一個連接埠的問題,可以參考nginx反向 Proxy的做法。對於Master進程使用80連接埠對外提供服務,而對於fork的進程則使用隨機連接埠,Master進程接受到請求就將其轉寄到fork進程中
對於剛剛所說的代理模式,由於進程每收到一個串連會使用掉一個檔案描述符,因此代理模式中用戶端串連到代理進程,代理進程再去串連fork進程會使用掉兩個檔案描述符,OS中檔案描述符是有限的,為瞭解決這個問題,node引入進程間發送控制代碼的功能。
在node的IPC進程通訊API中,send(message,[sendHandle])的第二個參數就是控制代碼。
控制代碼就是一種標識資源的引用,它的內部包含了指向對象的檔案描述符。控制代碼可以用來描述一個socket對象,一個UDP套接子,一個管道
主進程向背景工作處理序發送控制代碼意味著當主進程接收到用戶端的socket請求後則直接將這個socket發送給背景工作處理序,而不需要再與背景工作處理序建立socket串連,則檔案描述符的浪費即可解決。我們來看範例程式碼:
main.js
var cp = require(‘child_process‘);var child = cp.fork(‘./child.js‘);var server = require(‘net‘).createServer();//監聽用戶端的串連server.on(‘connection‘,function(socket){ socket.end(‘handled by parent‘);});//啟動監聽8080連接埠server.listen(8080,function(){//給子進程發送TCP伺服器(控制代碼) child.send(‘server‘,server);});
child.js
process.on(‘message‘,function(m,server){ if(m===‘server‘){ server.on(‘connection‘,function(socket){ socket.end(‘handle by child‘); }); }});
使用telnet或curl都可以測試:
1 [email protected] ~/code/nodeStudy $ curl 192.168.10.104:80802 handled by parent3 [email protected] ~/code/nodeStudy $ curl 192.168.10.104:80804 handle by child5 [email protected] ~/code/nodeStudy $ curl 192.168.10.104:80806 handled by parent7 [email protected] ~/code/nodeStudy $ curl 192.168.10.104:80808 handled by parent
測試結果是每次對於用戶端的串連,有可能父進程處理也有可能被子進程處理。現在我們嘗試僅提供http服務,並且為了讓父進程更加輕量,僅讓父進程傳遞控制代碼給子進程而不做請求處理:
main.js
var cp = require(‘child_process‘);var child1 = cp.fork(‘./child.js‘);var child2 = cp.fork(‘./child.js‘);var child3 = cp.fork(‘./child.js‘);var child4 = cp.fork(‘./child.js‘);var server = require(‘net‘).createServer();//父進程將接收到的請求分發給子進程server.listen(8080,function(){ child1.send(‘server‘,server); child2.send(‘server‘,server); child3.send(‘server‘,server); child4.send(‘server‘,server); //發送完控制代碼後關閉監聽 server.close();});
child.js
var http = require(‘http‘);var serverInChild = http.createServer(function(req,res){ res.end(‘I am child.Id:‘+process.pid);});//子進程收到父進程傳遞的控制代碼(即用戶端與伺服器的socket連線物件)process.on(‘message‘,function(m,serverInParent){ if(m===‘server‘){ //處理與用戶端的串連 serverInParent.on(‘connection‘,function(socket){ //交給http服務來處理 serverInChild.emit(‘connection‘,socket); }); }});
當運行上述代碼,此時查看8080連接埠佔有會有如下結果:
[email protected] ~/code/nodeStudy $ lsof -i:8080COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAMEnode 5120 wang 11u IPv6 44561 0t0 TCP *:http-alt (LISTEN)node 5126 wang 11u IPv6 44561 0t0 TCP *:http-alt (LISTEN)node 5127 wang 11u IPv6 44561 0t0 TCP *:http-alt (LISTEN)node 5133 wang 11u IPv6 44561 0t0 TCP *:http-alt (LISTEN)
運行curl查看結果:
[email protected] ~/code/nodeStudy $ curl 192.168.10.104:8080I am child.Id:5127[email protected] ~/code/nodeStudy $ curl 192.168.10.104:8080I am child.Id:5133[email protected] ~/code/nodeStudy $ curl 192.168.10.104:8080I am child.Id:5120[email protected] ~/code/nodeStudy $ curl 192.168.10.104:8080I am child.Id:5126[email protected] ~/code/nodeStudy $ curl 192.168.10.104:8080I am child.Id:5133[email protected] ~/code/nodeStudy $ curl 192.168.10.104:8080I am child.Id:5126
node child_process模組