預備知識
在如今機器的CPU都是多核的背景下,Node的單線程設計已經沒法更充分的"壓榨"機器性能了。所以從v0.8開始,Node新增了一個內置模塊――“cluster”,故名思議,它可以通過一個父進程管理一坨子進程的方式來實現集群的功能。
學習cluster之前,需要了解process相關的知識,如果不了解的話建議先閱讀process模塊、child_process模塊。
cluster借助child_process模塊的fork()方法來創建子進程,通過fork方式創建的子進程與父進程之間建立了IPC通道,支持雙向通信。
cluster模塊最早出現在node.js v0.8版本中
為什么會存在cluster模塊?
Node.js是單線程的,那么如果希望利用服務器的多核的資源的話,就應該多創建幾個進程,由多個進程共同提供服務。如果直接采用下列方式啟動多個服務的話,會提示端口占用。
const http = require('http');http.createServer((req, res) => { res.writeHead(200); res.end('hello world/n');}).listen(8000);// 啟動第一個服務 node index.js &// 啟動第二個服務 node index.js & throw er; // Unhandled 'error' event ^Error: listen EADDRINUSE :::8000 at Server.setupListenHandle [as _listen2] (net.js:1330:14) at listenInCluster (net.js:1378:12) at Server.listen (net.js:1465:7) at Object.<anonymous> (/Users/xiji/workspace/learn/node-basic/cluster/simple.js:5:4) at Module._compile (internal/modules/cjs/loader.js:702:30) at Object.Module._extensions..js (internal/modules/cjs/loader.js:713:10) at Module.load (internal/modules/cjs/loader.js:612:32) at tryModuleLoad (internal/modules/cjs/loader.js:551:12) at Function.Module._load (internal/modules/cjs/loader.js:543:3) at Function.Module.runMain (internal/modules/cjs/loader.js:744:10)如果改用cluster的話就沒有問題
const cluster = require('cluster');const http = require('http');const numCPUs = require('os').cpus().length;if (cluster.isMaster) { console.log(`Master ${process.pid} is running`); // Fork workers. for (let i = 0; i < numCPUs; i++) { cluster.fork(); } cluster.on('exit', (worker, code, signal) => { console.log(`worker ${worker.process.pid} died`); });} else { // Workers can share any TCP connection // In this case it is an HTTP server http.createServer((req, res) => { res.writeHead(200); res.end('hello world/n'); }).listen(8000); console.log(`Worker ${process.pid} started`);}// node index.js 執行完啟動了一個主進程和8個子進程(子進程數與cpu核數相一致)Master 11851 is runningWorker 11852 startedWorker 11854 startedWorker 11853 startedWorker 11855 startedWorker 11857 startedWorker 11858 startedWorker 11856 startedWorker 11859 startedcluster是如何實現多進程共享端口的?
cluster創建的進程分兩種,父進程和子進程,父進程只有一個,子進程有多個(一般根據cpu核數創建)
有三個問題需要回答:
子進程為何調用listen不會綁定端口?
net.js源碼中的listen方法通過listenInCluster方法來區分是父進程還是子進程,不同進程的差異在listenInCluster方法中體現
function listenInCluster(server, address, port, addressType, backlog, fd, excluseive) { if (cluster.isMaster || exclusive) { server._listen2(address, port, addressType, backlog, fd); return; } const serverQuery = { address: address ......}; cluster._getServer(server, serverQuery, listenOnMasterHandle); function listenOnMasterHandle(err, handle) { server._handle = handle; server._listen2(address, port, addressType, backlog, fd); }}上面是精簡過的代碼,當子進程調用listen方法時,會先執行_getServer,然后通過callback的形式指定server._handle的值,之后再調用_listen2方法。
cluster._getServer = function(obj, options, cb) { ... const message = util._extend({ act: 'queryServer', index: indexes[indexesKey], data: null }, options); message.address = address; send(message, (reply, handle) => { if (handle) shared(reply, handle, indexesKey, cb); // Shared listen socket. else rr(reply, indexesKey, cb); // Round-robin. }); ...};_getServer方法會向主進程發送queryServer的message,父進程執行完會調用回調函數,根據是否返回handle來區分是調用shared方法還是rr方法,這里其實是會調用rr方法。而rr方法的主要作用就是偽造了TCPWrapper來調用net的listenOnMasterHandle回調函數
function rr(message, indexesKey, cb) { var key = message.key; function listen(backlog) { return 0; } function close() { if (key === undefined) return; send({ act: 'close', key }); delete handles[key]; delete indexes[indexesKey]; key = undefined; } function getsockname(out) { if (key) util._extend(out, message.sockname); return 0; } const handle = { close, listen, ref: noop, unref: noop }; handles[key] = handle; cb(0, handle);}由于子進程的server拿到的是圍繞的TCPWrapper,當調用listen方法時并不會執行任何操作,所以在子進程中調用listen方法并不會綁定端口,因而也并不會報錯。
父進程何時創建的TCP Server
在子進程發送給父進程的queryServer message時,父進程會檢測是否創建了TCP Server,如果沒有的話就會創建TCP Server并綁定端口,然后再把子進程記錄下來,方便后續的用戶請求worker分發。
父進程是如何完成分發的
父進程由于綁定了端口號,所以可以捕獲連接請求,父進程的onconnection方法會被觸發,onconnection方法觸發時會傳遞TCP對象參數,由于之前父進程記錄了所有的worker,所以父進程可以選擇要處理請求的worker,然后通過向worker發送act為newconn的消息,并傳遞TCP對象,子進程監聽到消息后,對傳遞過來的TCP對象進行封裝,封裝成socket,然后觸發connection事件。這樣就實現了子進程雖然不監聽端口,但是依然可以處理用戶請求的目的。
cluster如何實現負載均衡
負載均衡直接依賴cluster的請求調度策略,在v6.0版本之前,cluster的調用策略采用的是cluster.SCHED_NONE(依賴于操作系統),SCHED_NODE理論上來說性能最好(Ferando Micalli寫過一篇Node.js 6.0版本的cluster和iptables以及nginx性能對比的文章)但是從實際角度發現,在請求調度方面會出現不太均勻的情況(可能出現8個子進程中的其中2到3個處理了70%的連接請求)。因此在6.0版本中Node.js增加了cluster.SCHED_RR(round-robin),目前已成為默認的調度策略(除了windows環境)
可以通過設置NODE_CLUSTER_SCHED_POLICY環境變量來修改調度策略
NODE_CLUSTER_SCHED_POLICY='rr'NODE_CLUSTER_SCHED_POLICY='none'
或者設置cluster的schedulingPolicy屬性
cluster.schedulingPolicy = cluster.SCHED_NONE;cluster.schedulingPolicy = cluster.SCHED_RR;
Node.js實現round-robin
Node.js內部維護了兩個隊列:
當新請求到達的時候父進程將請求暫存handles隊列,從free隊列中出隊一個worker,進入worker處理(handoff)階段,關鍵邏輯實現如下:
RoundRobinHandle.prototype.distribute = function(err, handle) { this.handles.push(handle); const worker = this.free.shift(); if (worker) { this.handoff(worker); }};worker處理階段首先從handles隊列出隊一個請求,然后通過進程通信的方式通知子worker進行請求處理,當worker接收到通信消息后發送ack信息,繼續響應handles隊列中的請求任務,當worker無法接受請求時,父進程負責重新調度worker進行處理。關鍵邏輯如下:
RoundRobinHandle.prototype.handoff = function(worker) { const handle = this.handles.shift(); if (handle === undefined) { this.free.push(worker); // Add to ready queue again. return; } const message = { act: 'newconn', key: this.key }; sendHelper(worker.process, message, handle, (reply) => { if (reply.accepted) handle.close(); else this.distribute(0, handle); // Worker is shutting down. Send to another. this.handoff(worker); });};注意:主進程與子進程之間建立了IPC,因此主進程與子進程之間可以通信,但是各個子進程之間是相互獨立的(無法通信)
參考資料
https://medium.com/@fermads/node-js-process-load-balancing-comparing-cluster-iptables-and-nginx-6746aaf38272
總結
以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對武林網的支持。
新聞熱點
疑難解答