# 集群
~~~
稳定度: 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("劳动者的胜利!");
}
});
~~~