# 集群
~~~
穩定度: 1 - 實驗性
~~~
單個 Node 實例運行在單個線程中。要發揮多核系統的能力,用戶有時候需要啟動一個 Node 進程集群來處理負載。
集群模塊允許你方便地創建一個共享服務器端口的進程網絡。
~~~
cluster.on('exit', function(worker, code, signal) {
console.log('worker ' + worker.process.pid + ' died');
});
if (cluster.isMaster) {
cluster.fork();
cluster.fork();
console.log('master pid:' + process.pid);
} else {
// Workers can share any TCP connection
// In this case its a HTTP server
http.createServer(function(req, res) {
res.writeHead(200);
res.end("hello world\n");
}).listen(8000);
console.log('child pid:' + process.pid);
}
~~~
現在,運行 node 將會在所有工作進程間共享 8000 端口:
~~~
% NODE_DEBUG=cluster node server.js
23521,Master Worker 23524 online
23521,Master Worker 23526 online
23521,Master Worker 23523 online
23521,Master Worker 23528 online
~~~
這是一個近期推出的功能,在未來版本中可能會有所改變。請嘗試并提供反饋。
還要注意的是,在 Windows 中尚不能在工作進程中建立一個被命名的管道服務器。
### 它是如何工作的
工作進程是通過使用 `child_process.fork` 方法派生的,因此它們可以通過 IPC(進程間通訊)與父進程通訊并互相傳遞服務器句柄。
集群模塊支持兩種分配傳入連接的方式。
第一種(同時也是除 Windows 外所有平臺的缺省方式)為循環式:主進程監聽一個端口,接受新連接,并以輪流的方式分配給工作進程,并以一些內建機制來避免單個工作進程的超載。
第二種方式是,主進程建立監聽嵌套字,并將它發送給感興趣的工作進程,由工作進程直接接受傳入連接。
第二種方式理論上有最好的性能。然而在實踐中,由于操作系統的調度變幻莫測,分配往往十分不平衡。負載曾被觀測到超過 70% 的連接結束于總共八個進程中的兩個。
因為 `server.listen()` 將大部分工作交給了主進程,所以一個普通的node.js進程和一個集群工作進程會在三種情況下有所區別:
1. `server.listen({fd: 7})` 由于消息被傳遞到主進程,**父進程中的**文件描述符 7 會被監聽,并且句柄會被傳遞給工作進程,而不是監聽工作進程中文件描述符 7 所引用的東西。
1. `server.listen(handle)` 明確地監聽一個句柄會使得工作進程使用所給句柄,而不是與主進程通訊。如果工作進程已經擁有了該句柄,則假定您知道您在做什么。
1. `server.listen(0)` 通常,這會讓服務器監聽一個隨機端口。然而,在集群中,各個工作進程每次 `listen(0)` 都會得到一樣的“隨機”端口。實際上,端口在第一次時是隨機的,但在那之后卻是可預知的。如果您想要監聽一個唯一的端口,則請根據集群工作進程 ID 來生成端口號。
由于在 Node.js 或您的程序中并沒有路由邏輯,工作進程之間也沒有共享的狀態,因此在您的程序中,諸如會話和登錄等功能應當被設計成不能太過依賴于內存中的數據對象。
由于工作進程都是獨立的進程,因此它們會根據您的程序的需要被終止或重新派生,并且不會影響到其它工作進程。只要還有工作進程存在,服務器就會繼續接受連接。但是,Node 不會自動為您管理工作進程的數量,根據您的程序所需管理工作進程池是您的責任。
### cluster.schedulingPolicy
調度策略 `cluster.SCHED_RR` 表示輪流制,`cluster.SCHED_NONE` 表示由操作系統處理。這是一個全局設定,并且一旦您派生了第一個工作進程或調用了 `cluster.setupMaster()` 后便不可更改。
`SCHED_RR` 是除 Windows 外所有操作系統上的缺省方式。只要 libuv 能夠有效地分配 IOCP 句柄并且不產生巨大的性能損失,Windows 也將會更改為 `SCHED_RR` 方式。
`cluster.schedulingPolicy` 也可以通過環境變量 `NODE_CLUSTER_SCHED_POLICY` 設定。有效值為 `"rr"` 和 `"none"`。
### cluster.settings
- {Object}
- `exec` {String} 工作進程文件的路徑。(缺省為 `__filename`)
- `args` {Array} 傳遞給工作進程的字符串參數。(缺省為 `process.argv.slice(2)`)
- `silent` {Boolean} 是否將輸出發送到父進程的 stdio。(缺省為 `false`)
所有由 `.setupMaster` 設定的設置都會儲存在此設置對象中。這個對象不應由您手動更改或設定。
### 集群的主進程(判斷當前進程是否是主進程)
- {Boolean}
如果進程為主進程則為 `true`。這是由 `process.env.NODE_UNIQUE_ID` 判斷的,如果 `process.env.NODE_UNIQUE_ID` 為 undefined,則 `isMaster` 為 `true`。
### 當前進程是否是從主進程的fork出來的
- {Boolean}
如果當前進程是分支自主進程的工作進程,則該布爾標識的值為 `true`。如果 `process.env.NODE_UNIQUE_ID` 被設定為一個值,則 `isWorker` 為 `true`。
### 事件: 'fork'
- `worker` {Worker object}
當一個新的工作進程被分支出來,cluster 模塊會產生一個 'fork' 事件。這可被用于記錄工作進程活動,以及創建您自己的超時判斷。
~~~
cluster.on('fork', function(worker) {
timeouts[worker.id] = setTimeout(errorMsg, 2000);
});
cluster.on('listening', function(worker, address) {
clearTimeout(timeouts[worker.id]);
});
cluster.on('exit', function(worker, code, signal) {
clearTimeout(timeouts[worker.id]);
errorMsg();
});
~~~
### 事件: 'online'
- `worker` {Worker object}
分支出一個新的工作進程后,工作進程會響應一個在線消息。當主進程收到一個在線消息后,它會觸發該事件。'fork' 和 'online' 的區別在于前者發生于主進程嘗試分支出工作進程時,而后者發生于工作進程被執行時。
~~~
cluster.on('online', function(worker) {
console.log("嘿嘿,工作進程完成分支并發出回應了");
});
~~~
### 事件: 'listening'
- `worker` {Worker object}
- `address` {Object}
當工作進程調用 `listen()` 時,一個 `listening` 事件會被自動分配到服務器實例中。當服務器處于監聽時,一個消息會被發送到那個'listening'事件被分發的主進程。
事件處理器被執行時會帶上兩個參數。其中 `worker` 包含了工作進程對象,`address` 對象包含了下列連接屬性:地址 `address`、端口號 `port` 和地址類型 `addressType`。如果工作進程監聽多個地址,那么這些信息將十分有用。
~~~
cluster.on('listening', function(worker, address) {
console.log("一個工作進程剛剛連接到 " + address.address + ":" + address.port);
});
~~~
### 事件: 'disconnect'
- `worker` {Worker object}
當一個工作進程的 IPC 通道斷開時此事件會發生。這發生于工作進程結束時,通常是調用 `.kill()` 之后。
當調用 `.disconnect()` 后,`disconnect` 和 `exit` 事件之間可能存在延遲。該事件可被用于檢測進程是否被卡在清理過程或存在長連接。
~~~
cluster.on('disconnect', function(worker) {
console.log('工作進程 #' + worker.id + ' 斷開了連接');
});
~~~
### 事件: 'exit'
- `worker` {Worker object}
- `code` {Number} 如果是正常退出則為退出代碼。
- `signal` {String} 使得進程被終止的信號的名稱(比如 `'SIGHUP'`)。
當任意工作進程被結束時,集群模塊會分發`exit` 事件。通過再次調用`fork()`函數,可以使用這個事件來重啟工作進程。
~~~
cluster.on('exit', function(worker, code, signal) {
var exitCode = worker.process.exitCode;
console.log('工作進程 ' + worker.process.pid + ' 被結束('+exitCode+')。正在重啟...');
cluster.fork();
});
~~~
### 事件: 'setup'
- `worker` {Worker object}
當 `.setupMaster()` 函數被執行時觸發此事件。如果 `.setupMaster()` 在 `fork()` 之前沒被執行,那么它會不帶參數調用 `.setupMaster()`。
### cluster.setupMaster([settings])
- `settings` {Object}
- `exec` {String} 工作進程文件的路徑。(缺省為 `__filename`)
- `args` {Array} 傳給工作進程的字符串參數。(缺省為 `process.argv.slice(2)`)
- `silent` {Boolean} 是否將輸出發送到父進程的 stdio。(缺省為 `false`)
`setupMaster` 被用于更改缺省的 `fork` 行為。新的設置會立即永久生效,并且在之后不能被更改。
實例:
~~~
var cluster = require("cluster");
cluster.setupMaster({
exec : "worker.js",
args : ["--use", "https"],
silent : true
});
cluster.fork();
~~~
### cluster.fork([env])
- `env` {Object} 添加到子進程環境變量中的鍵值對。
- 返回 {Worker object}
派生一個新的工作進程。這個函數只能在主進程中被調用。
### cluster.disconnect([callback])
- `callback` {Function} 當所有工作進程都斷開連接并且句柄被關閉時被調用
調用此方法時,所有的工作進程都會優雅地將自己結束掉。當它們都斷開連接時,所有的內部處理器都會被關閉,使得主進程可以可以在沒有其它事件等待時優雅地結束。
該方法帶有一個可選的回調參數,會在完成時被調用。
### cluster.worker
- {Object}
對當前工作進程對象的引用。在主進程中不可用。
~~~
if (cluster.isMaster) {
console.log('我是主進程');
cluster.fork();
cluster.fork();
} else if (cluster.isWorker) {
console.log('我是工作進程 #' + cluster.worker.id);
}
~~~
### cluster.workers
- {Object}
一個儲存活動工作進程對象的哈希表,以 `id` 字段作為主鍵。它能被用作遍歷所有工作進程,僅在主進程中可用。
~~~
// 遍歷所有工作進程
function eachWorker(callback) {
for (var id in cluster.workers) {
callback(cluster.workers[id]);
}
}
eachWorker(function(worker) {
worker.send('向一線工作者們致以親切問候!');
});
~~~
如果您希望通過通訊通道引用一個工作進程,那么使用工作進程的唯一標識是找到那個工作進程的最簡單的辦法。
~~~
socket.on('data', function(id) {
var worker = cluster.workers[id];
});
~~~
### 類: Worker
一個 Worker 對象包含了工作進程的所有公開信息和方法。可通過主進程中的 `cluster.workers` 或工作進程中的 `cluster.worker` 取得。
### worker.id
- {String}
每個新的工作進程都被賦予一個唯一的標識,這個標識被儲存在 `id` 中。
當一個工作進程可用時,這就是它被索引在 cluster.workers 中的主鍵。
### worker.process
- {ChildProcess object}
所有工作進程都是使用 `child_process.fork()` 創建的,該函數返回的對象被儲存在 process 中。
參考:[Child Process 模塊](#)
### worker.suicide
- {Boolean}
該屬性是一個布爾值。它會在工作進程調用 `.kill()` 后終止時或調用 `.disconnect()` 方法時被設置。在此之前它的值是 `undefined`。
### worker.send(message, [sendHandle])
- `message` {Object}
- `sendHandle` {Handle object}
該函數等同于 `child_process.fork()` 提供的 send 方法。在主進程中您可以用該函數向特定工作進程發送消息。當然,在工作進程中您也能使用 `process.send(message)`,因為它們是同一個函數。
這個例子會回應來自主進程的所有消息:
~~~
} else if (cluster.isWorker) {
process.on('message', function(msg) {
process.send(msg);
});
}
~~~
### worker.kill([signal='SIGTERM'])
- `signal` {String} 發送給工作進程的終止信號的名稱
該函數會終止工作進程,并告知主進程不要派生一個新工作進程。布爾值 `suicide` 讓您區分自行退出和意外退出。
~~~
// 終止工作進程
worker.kill();
~~~
該方法的別名是 `worker.destroy()`,以保持向后兼容。
### worker.disconnect()
調用該函數后工作進程將不再接受新連接,但新連接仍會被其它正在監聽的工作進程處理。已存在的連接允許正常退出。當沒有連接存在,連接到工作進程的 IPC 通道會被關閉,以便工作進程安全地結束。當 IPC 通道關閉時 `disconnect` 事件會被觸發,然后則是工作進程最終結束時觸發的 `exit` 事件。
由于可能存在長連接,通常會實現一個超時機制。這個例子會告知工作進程斷開連接,并且在 2 秒后銷毀服務器。另一個備選方案是 2 秒后執行 `worker.kill()`,但那樣通常會使得工作進程沒有機會進行必要的清理。
~~~
process.on('message', function(msg) {
if (msg === 'force kill') {
server.close();
}
});
}
~~~
### 事件: 'message'
- `message` {Object}
該事件和 `child_process.fork()` 所提供的一樣。在主進程中您應當使用該事件,而在工作進程中您也可以使用 `process.on('message')`。
舉個例子,這里有一個集群,使用消息系統在主進程中統計請求的數量:
~~~
// 將請求通知主進程
process.send({ cmd: 'notifyRequest' });
}).listen(8000);
}
~~~
### 事件: 'online'
和 `cluster.on('online')` 事件一樣,但僅當特定工作進程的狀態改變時發生。
~~~
cluster.fork().on('online', function() {
// 工作進程在線
});
~~~
### 事件: 'listening'
- `address` {Object}
和 `cluster.on('listening')` 事件一樣,但僅當特定工作進程的狀態改變時發生。
~~~
cluster.fork().on('listening', function(address) {
// 工作進程正在監聽
});
~~~
### 事件: 'disconnect'
和 `cluster.on('disconnect')` 事件一樣,但僅當特定工作進程的狀態改變時發生。
~~~
cluster.fork().on('disconnect', function() {
// 工作進程斷開了連接
});
~~~
### 事件: 'exit'
- `code` {Number} 如果是正常退出則為退出代碼。
- `signal` {String} 使得進程被終止的信號的名稱(比如 `'SIGHUP'`)。
由單個工作進程實例在底層子進程被結束時觸發。詳見[子進程事件: 'exit'](#)。
~~~
var worker = cluster.fork();
worker.on('exit', function(code, signal) {
if( signal ) {
console.log("工人被信號 " + signal + " 殺掉了");
} else if( code !== 0 ) {
console.log("工作進程退出,錯誤碼:" + code);
} else {
console.log("勞動者的勝利!");
}
});
~~~