?
一段常見的示例程式碼
const cluster = require('cluster');
const http = require('http');
if (cluster.isMaster) {
// 根據cpu核心數出fork相同數量的子程式
} else {
// 用http模組建立server監聽某一個埠
}
複製程式碼
引出如下問題:
-
cluster
模組如何區分子程式和主程式? -
程式碼中沒有在主程式中建立伺服器,那麼如何主程式如何承擔代理伺服器的職責?
-
多個子程式共同偵聽同一個埠為什麼不會造成埠
reuse error
?
1. cluster
模組如何區分主程式/子程式
const childOrMaster = 'NODE_UNIQUE_ID' in process.env ? 'child' : 'master';
module.exports = require(`internal/cluster/${childOrMaster}`);
複製程式碼
結論: 判斷環境變數中是否含有NODE_UNIQUE_ID
, 有則為子程式,沒有則為主程式
1.1 isMaster
& isWorker
這樣的話, 在對應的檔案中isMaster
和isWorker
的值就明確啦
// child.js
module.exports = cluster;
cluster.isWorker = true;
cluster.isMaster = false;
// master.js
module.exports = cluster;
cluster.isWorker = false;
cluster.isMaster = true;
複製程式碼
那麼接下來的問題是: NODE_UNIQUE_ID從哪裡來?
1.2 NODE_UNIQUE_ID
從哪裡來的?
在internal/cluster/master.js
檔案中搜尋NODE_UNIQUE_ID
----> 上層為createWorkerProcess
函式
----> 上層為cluster.fork
函式
master.js
原始碼中相關部分
const { fork } = require('child_process');
cluster.workers = {}
var ids = 0;
cluster.fork = function(env) {
const id = ++ ids;
const workerProcess = createWorkerProcess(id, env);
const worker = new Worker({
id: id,
process: workerProcess
});
cluster.workers[worker.id] = worker;
return worker
}
function createWorkerProcess(id, env) {
const workerEnv = { ...process.env, ...env, NODE_UNIQUE_ID: `${id}` };
return fork(args, {
env: workerEnv
})
}
複製程式碼
結論: 變數NODE_UNIQUE_ID
是在主程式fork
子程式時傳遞進去的引數,因此採用cluster.fork
建立的子程式是一定包含NODE_UNIQUE_ID
的,而直接使用child_process.fork
的子程式是沒有NODE_UNIQUE_ID
的
並且, NODE_UNIQUE_ID
將作為主程式中儲存活躍的工作程式物件的鍵值
2. 主程式中是否存在TCP伺服器, 如果有, 什麼時候建立的?
繼續描述一下這個問題的由來:
const cluster = require('cluster');
const http = require('http');
if (cluster.isMaster) {
// 根據cpu核心數出fork相同數量的子程式
} else {
// 用http模組建立server監聽某一個埠
}
複製程式碼
並沒有在cluster.isMaster
條件語句中建立伺服器, 也沒有提供伺服器相關的路徑,介面。而主程式又需要承擔代理伺服器的
職責,那麼主程式中是否存在TCP
伺服器?
我們來猜猜看可能的步驟
-
子程式會執行
http.createServer
-
http
模組會呼叫net
模組, 因為http.Server
繼承net.Server
-
同時偵聽埠, 建立
net.Server
例項, 建立的例項呼叫listen(port)
, 等待連結
這時如果主程式要建立伺服器就需要把建立伺服器相關資訊給主程式, 繼續猜測
-
假設主程式已經拿到了伺服器相關的資訊, 主程式自己來建立
-
後面的
fork
子程式就不用自己建立了,而是從主程式中get
到相關資料
既然要在主程式需要得到完整的建立伺服器相關資訊, 那麼很可能在net
模組listen
相關方法中進行處理
2.1 在原始碼中找答案
Server.prototype.listen
找找看,什麼時候把伺服器相關資訊傳遞給主程式了?
Server.prototype.listen = function(...args) {
// 無視其他的判斷邏輯, 直達它的內心!
if (成功) {
listenInCluster()
return this
} else {
// 無視
}
}
複製程式碼
總的來說就是: 在Server.prototype.listen
函式中,在成功進入條件語句後所有的情況都執行了listenInCluster
函式後返回
接下來看listenInCluster
函式
function listenInCluster(server, 建立伺服器需要的資料) {
if (cluster === undefined) cluster = require('cluster')
// 判斷是否是主程式
if (cluster.isMaster) {
server._listen2(建立伺服器需要的資料)
return
}
// 建立伺服器需要的資料
const serverQuery = {
address: address,
port: port,
addressType: addressType,
fd: fd,
flags,
};
// 只剩下子程式
cluster._getServer(server, 建立伺服器需要的資料, listenOnMasterHandle);
function listenOnMasterHandle(err, handle) {
server._handle = handle
server._listen2(建立伺服器需要的資料)
}
}
複製程式碼
按照前面的推斷: 子程式會給主程式傳送建立server需要的資料, 主程式去建立
所以接下來去看cluster
模組的child._getServer
函式
cluster._getServer = function(obj, options, cb) {
// 組裝傳送的資料
const message = {
act: 'queryServer',
...options,
}
// 傳送資料
send(message, (reply, handle) => {
})
}
複製程式碼
那麼接下來主程式就應該對queryServer
作出想要的處理
具體可以看cluster/master.js
const RoundRobinHandle = require('internal/cluster/round_robin_handle');
const handles = new Map()
function onmessage(message, handle) {
if (message.act === 'queryServer') {
queryServer(worker, message)
}
}
queryServer(worker, message) {
const key = `${message.address}:${message.port}:${message.addressType}:` +
`${message.fd}:${message.index}`;
const constructor = RoundRobinHandle
let handle = new constructor(建立伺服器相關資訊)
handles.set(key, handle);
}
複製程式碼
終於要到終點了:
在internal/cluster/round_robin_handle.js
中
function RoundRobinHandle(建立伺服器相關資訊) {
this.server = net.createServer()
this.server.listen(.....)
}
複製程式碼
2.2 主程式在cluster
模式下如何建立伺服器的結論
主程式fork
子程式, 子程式中有顯式建立伺服器的操作,但實際上在cluster
模式下, 子程式是把建立伺服器所需要的資料傳送給主程式, 主程式來隱式建立TCP
伺服器
流程圖
3. 為什麼多個子程式可以監聽同一個埠?
這個問題可以轉換為: 子程式中有沒有也建立一個伺服器,同時偵聽某個埠呢?
其實,上面的原始碼分析中可以得出結論:子程式中確實建立了net.Server
物件,可是它沒有像主程式那樣在libuv
層構建socket
控制程式碼,子程式的net.Server
物件使用的是一個假控制程式碼來'欺騙'使用者埠已偵聽
3.1 首先要明確預設的排程策略: round-robin
這部分可以參考文章Node.js V0.12 新特性之 Cluster 輪轉法負載均衡
主要就是說:Node.js v0.12
引入了round-robin方式
, 用輪轉法來分配請求, 每個子程式的獲取的時間的機會都是均等的(windows除外)
原始碼在internal/cluster/master.js
中
var schedulingPolicy = {
'none': SCHED_NONE,
'rr': SCHED_RR
}[process.env.NODE_CLUSTER_SCHED_POLICY];
if (schedulingPolicy === undefined) {
// FIXME Round-robin doesn't perform well on Windows right now due to the
// way IOCP is wired up.
schedulingPolicy = (process.platform === 'win32') ? SCHED_NONE : SCHED_RR;
}
cluster.schedulingPolicy = schedulingPolicy;
複製程式碼
3.2 證明子程式拿到的是假控制程式碼
上面說明了:預設的排程策略是round-robin
, 那麼子程式將建立伺服器的資料傳送給主程式, 當主程式傳送建立伺服器成功的訊息後,子程式會執行回撥函式
原始碼在internal/cluster/child.js _getServer中
cluster._getServer = function(obj, options, cb) {
const indexesKey = [address,
options.port,
options.addressType,
options.fd ].join(':');
send(message, (reply, handle) => {
if (typeof obj._setServerData === 'function')
obj._setServerData(reply.data);
// 這裡可以反推出主程式返回的handle為null
if (handle)
shared(reply, handle, indexesKey, cb); // Shared listen socket.
else
rr(reply, indexesKey, cb); // Round-robin.
});
}
複製程式碼
rr
函式, 注意這裡的回撥函式其實就是net
模組中的listenOnMasterHandle
方法
function rr(message, indexesKey, cb) {
const key = message.key
const handle = { close, listen, ref: noop, unref: noop };
handles.set(key, handle)
// 將假控制程式碼傳遞給上層的net.Server
cb(0, handle)
}
複製程式碼
所以結論是這樣:子程式壓根沒有建立底層的服務端socket
做偵聽,所以在子程式建立的HTTP
伺服器偵聽的埠根本不會出現埠複用的情況
3.3 子程式沒有建立底層socket, 如何接收請求和傳送響應呢?
顯而易見:主程式的伺服器中會建立RoundRobinHandle
決定分發請求給哪一個子程式,篩選出子程式後傳送newconn
訊息給對應的子程式
4. 請求分發策略 RoundRobin
原始碼見internal/cluster/round_robin_handle
module.exports = RoundRobinHandle
function RoundRobinHandle(建立伺服器需要的引數) {
// 儲存空閒的子程式
this.free = []
// 存放待處理的使用者請求
this.handles = []
}
// 負責篩選出處理請求的子程式
RoundRobinHandle.prototype.distribute = function(err, handle) {
this.handles.push(handle)
const worker = this.free.shift()
if (worker) {
this.handoff(worker)
}
}
// 獲取請求,並通過IPC傳送控制程式碼handle和newconn訊息,等待子程式返回
RoundRobinHandle.prototype.handoff = function(worker) {
const handle = this.handles.shift()
if (handle === undefined) {
this.free.push(worker)
return
}
const message = { act: 'newconn', key: this.key }
sendHelper(worker.process, message, handle, reply => {
if (reply.accepted)
handle.close();
// 某個子程式辦事不力,給下一個子程式再試試
else
this.distribute(0, handle)
this.handoff(worker)
})
}
複製程式碼