node-cluster
Version:
A graceful node library to contribute a permanent "master-worker" server.
779 lines (679 loc) • 16.7 kB
JavaScript
/* vim: set expandtab tabstop=2 shiftwidth=2 foldmethod=marker: */
var Socket = require('net').Socket;
var TCP = process.binding('tcp_wrap').TCP;
function noop() {}
function log(pname, msg) {
pname = (pname || 'unknow') + ':' + process.pid;
console.log('[' + pname + '][' + new Date() + '] ' + msg);
}
/**
* @消息类型
* @奇数表示 master -> worker
*/
var MESSAGE = {
RELOAD : 2, /**< 重新加载配置、缓存,api (msg) => master (signal) => worker */
REQ_FD : 11,
HEART : 20,
};
/**
* @子进程状态
*/
var CSTATUS = {
PEDDING : 0, /**< 正在初始化 */
RUNNING : 10, /**< 正常工作 */
STOPING : 20, /**< 准备退出 */
};
/**
* @找不到可用worker时的默认响应体
*/
var RESPONSE_ON_NO_WORKER = new Buffer('HTTP/1.1 410 Gone\r\nServer: ANC/1.0\r\nConnection: close\r\nContent-Type: text/plain;charset=utf-8\r\nContent-Length: 169\r\nX-Expire: 300\r\nX-Message: None available workers.\r\n\r\n');
/* {{{ function intval() */
function intval(num, dft) {
num = parseInt(num, 10);
dft = parseInt(dft, 10);
return num ? num : (dft ? dft : 0);
}
/* }}} */
/* {{{ function isport() */
function isport(num) {
return num == intval(num, 0) && num > 0;
}
/* }}} */
/* {{{ function timestamp() */
function timestamp() {
return intval((new Date()).getTime() / 1000, 0);
}
/* }}} */
/* {{{ function startAll() */
/**
* 启动所有子进程
*/
function startAll(obj, listen, append) {
if (!obj.bindings) {
return;
}
for (var name in obj.bindings) {
var cfg = obj.bindings[name];
var num = (false === append) ? cfg.cnum : cfg.cnum - cfg.pnum;
if (num > 0) {
startName(obj, name, num);
}
if (listen && intval(name) == name && name > 0) {
listenAt(obj, name);
}
}
// listen port mapping
if (listen) {
for (var port in obj.portMapping) {
listenAt(obj, port);
}
}
}
/* }}} */
/* {{{ function startName() */
/**
* 根据名字启动子进程
*
* @access private
*/
function startName(obj, name, num) {
if (!obj.bindings[name]) {
return false;
}
var cfg = obj.bindings[name];
num = intval(num, cfg.cnum);
for (var i = 0; i < num; i++) {
startWorker(obj, name, cfg.path);
}
return true;
}
/* }}} */
/* {{{ function startWorker() */
/**
* 启动一个子进程
*
* @access private
*/
function startWorker(obj, name, path) {
if (!obj.hasOwnProperty('children')) {
return;
}
var env = process.env;
env.__USER = obj.bindings[name].user;
env.__GROUP = obj.bindings[name].group;
var sub = require('child_process').fork(path, obj.bindings[name].args, {
'cwd' : process.cwd(),
'env' : env,
});
var pid = sub.pid;
if (!obj.heartmsg[name]) {
obj.heartmsg[name] = {};
}
obj.heartmsg[name][pid] = {
'uptime' : timestamp(),
'scores' : 0,
'remain' : 5,
'status' : CSTATUS.PEDDING,
};
var debugMsg = 'new worker forked (' + pid + ':' + path + ') for ' + name;
var otherPorts = obj.bindings[name].otherPorts;
if (otherPorts.length > 0) {
debugMsg += ', ' + otherPorts.join(', ');
}
log('master', debugMsg);
obj.bindings[name].pnum++;
obj.children[pid] = sub;
sub.on('message', function (msg) {
if (!msg.type || !msg.data) {
return;
}
switch (msg.type) {
case MESSAGE.HEART:
onHeartMessage(obj, name, pid, msg.data);
break;
case MESSAGE.RELOAD:
onReloadMessage(obj, name, msg.data);
break;
default:
break;
}
});
sub.on('exit', function (code, signal) {
obj.bindings[name].pnum--;
delete obj.children[pid];
delete obj.heartmsg[name][pid];
delete obj.killings[name][pid];
if (code && 'SIGKILL' !== signal) {
var tmp = {};
var now = timestamp();
for (var idx in obj.restarts) {
tmp[idx] = [];
for (var i = 0; i < obj.restarts[idx].length; i++) {
if (obj.restarts[idx][i] > now) {
tmp[idx].push(obj.restarts[idx][i]);
}
}
}
tmp[name].push(now + obj.options.restart_time_window);
obj.restarts = tmp;
if (obj.restarts[name].length > (obj.options.max_fatal_restart * obj.bindings[name].cnum)) {
var msg = obj.restarts[name].length + ' times fatal in ' +
obj.options.restart_time_window + ' seconds.';
return callEvent(obj, 'restartgiveup', name, msg);
}
}
if (!obj.shutdown) {
startAll(obj, false, true);
}
});
}
/* }}} */
/* {{{ function onHeartMessage() */
/**
* 处理心跳信息
*
* @access private
*/
function onHeartMessage(obj, name, pid, data) {
if (!obj.heartmsg[name]) {
return;
}
if (!obj.heartmsg[name][pid]) {
obj.heartmsg[name][pid] = {};
}
var del = obj.killings[name];
var msg = obj.heartmsg[name][pid];
for (var i in data) {
msg[i] = data[i];
}
/**
* @有新进程可用, kill待退役进程
*/
if (msg.status == CSTATUS.RUNNING && true !== del[pid]) {
for (var tmp in del) {
try {
process.kill(tmp, 'SIGTERM');
} catch (e) {}
}
}
/**
* @最大连接数检查
*/
var cfg = obj.bindings[name];
if (cfg.maxc > 0 && msg.scores >= cfg.maxc && true !== del[pid] && cfg.path) {
del[pid] = true;
startWorker(obj, name, cfg.path);
}
}
/* }}} */
/* {{{ function onReloadMessage() */
/**
* 处理重载信息
*
* @access private
*/
function onReloadMessage(obj, name, data) {
if (data.name) {
push2Kill(obj, name);
} else {
push2Kill(obj);
}
}
/* }}} */
/* {{{ function callEvent() */
function callEvent(obj, evt, name, msg) {
if ('function' === typeof obj.__events[evt]) {
obj.__events[evt](name, msg);
}
}
/* }}} */
/* {{{ function listenAt() */
/**
* 监听端口
*
* @access private
*/
function listenAt(obj, port) {
var server = new TCP();
server.bind('0.0.0.0', port);
server.listen(1024);
// 如果是映射端口,则使用第一个端口
var name = obj.portMapping[port] || port;
if (!obj.bindings[name]._tcp) {
obj.bindings[name]._tcp = [ server ];
} else {
obj.bindings[name]._tcp.push(server);
}
server.onconnection = function(handle) {
var pid = fetchWorker(obj, name);
if (!pid) {
var res = new Socket({
'handle': handle,
});
res.writable = true;
res.end(obj.bindings[name].gone);
res.destroy();
return;
}
obj.heartmsg[name][pid].remain++;
try {
obj.children[pid].send({
type: MESSAGE.REQ_FD,
port: port
}, handle);
} catch (e) {}
process.nextTick(function () {
handle.close();
handle = null;
});
}
}
/* }}} */
/* {{{ function fetchWorker() */
/**
* 选择一个worker处理请求
*
* @access private
*/
function fetchWorker(obj, name) {
if (!obj.heartmsg[name]) {
return;
}
var pid = 0;
var max = 4294967296;
for (var idx in obj.heartmsg[name]) {
var sub = obj.heartmsg[name][idx];
if (max > sub.remain && CSTATUS.RUNNING == sub.status) {
pid = idx;
max = sub.remain;
}
}
return obj.children[pid] ? pid : null;
}
/* }}} */
/* {{{ function push2Kill() */
function push2Kill(obj, name) {
if (!obj.heartmsg) {
return;
}
function killbyname(obj, name) {
for (var pid in obj.heartmsg[name]) {
obj.killings[name][pid] = true;
}
}
if (undefined === name || null === name) {
for (var name in obj.heartmsg) {
killbyname(obj, name);
}
} else {
killbyname(obj, name);
}
}
/* }}} */
function Master(options) {
if (!(this instanceof Master)) {
return new Master(options);
}
/**
* @master参数
*/
this.options = {
'max_fatal_restart' : 5, /**< 异常退出最大重启次数 */
'restart_time_window' : 60, /**< 异常重启统计时间窗口 */
'pidfile' : null, /**< 进程PID文件 */
};
for (var i in options) {
this.options[i] = options[i];
}
/**
* @重启记录
*/
this.restarts = {};
/**
* @事件回调
*/
this.__events = {
restartgiveup: function (name, msg) {
log('worker', name + ':' + msg);
}
};
/**
* @shutdown标记
*/
this.shutdown = false;
/**
* @注册的监听分发列表
*/
this.bindings = {};
/**
* @已经启动的子进程列表
*/
this.children = {};
/**
* @心跳信息
*/
this.heartmsg = {};
/**
* @即将被kill的子进程
*/
this.killings = {};
/**
* 一批worker可以监听多个端口做映射
* {
* port2: port1,
* port3: port1,
* ...
* }
* @type {Object}
*/
this.portMapping = {};
}
/**
* Register child worker infomation
*
* e.g.:
* master.register(8080, 'app.js').dispatch();
*
* @param {Number|String|Array[port1, port2, ...]} name, net listen port or socket file, or multi port array.
* @param {String} path, worker start file path
* @param {Object} option:
* cnum : child nums
* gone : gone message
* maxc : max connections
* user : uid that the worker run as
* group : gid that the worker run as
* args : child process args
* @param {Number} cnum, total child worker number, default is `os cpu number`
* @param {String} gone, no handle worker response, default is `RESPONSE_ON_NO_WORKER`
* @param {Number} max_conn, max connections, default is `-1`, no limit
* @return {Object} the `Master` instance
* @api public
*/
Master.prototype.register = function(name, path, option) {
var cpus = require('os').cpus().length;
option = option || {};
var otherPorts = [];
if (Array.isArray(name)) {
otherPorts = name.slice(1);
name = name[0];
}
this.bindings[name] = {
'path' : path,
'cnum' : option.cnum ? intval(option.cnum, cpus) : cpus,
'pnum' : 0,
'gone' : option.gone ? new Buffer(option.gone) : RESPONSE_ON_NO_WORKER,
'maxc' : option.maxc ? intval(option.max_conn, -1) : -1,
'user' : option.user ? option.user : null,
'group' : option.group ? option.group : null,
'args' : option.args ? option.args : [],
otherPorts: otherPorts
};
this.heartmsg[name] = {};
this.killings[name] = {};
this.restarts[name] = [];
if (otherPorts.length > 0) {
for (var i = 0, l = otherPorts.length; i < l; i++) {
this.portMapping[otherPorts[i]] = name;
}
}
return this;
};
/* {{{ Master prototype on() */
/**
* Register events callback
*
* @param {String} event name
* @param {Function(port, msg)} callback function, with arguments like:
* port : name of the worker
* msg : string message
* @return {Object this}
*/
Master.prototype.on = function(evt, callback) {
var events = {};
if (arguments.length > 1 && ('string' === typeof evt)) {
events[evt] = callback;
} else {
events = evt;
}
for (var idx in events) {
if ('function' === typeof events[idx]) {
this.__events[idx] = events[idx];
}
}
return this;
}
/* }}} */
/* {{{ Master prototype dispatch() */
/**
* @请求分发
*/
Master.prototype.dispatch = function () {
var _self = this;
if (_self.options.pidfile) {
var fs = require('fs');
fs.writeFile(_self.options.pidfile, process.pid, function (error) {
if (error) {
log('master', 'touch pidfile "' + _self.options.pidfile + '" failed.');
} else {
process.on('exit', function () {
try {
if (process.pid != fs.readFileSync(_self.options.pidfile, 'utf-8').trim()) {
return;
}
fs.unlinkSync(_self.options.pidfile);
} catch (e) {
}
});
}
});
}
/**
* @启动子进程
*/
startAll(_self, true, true);
/**
* @忽略HUP信号
*/
process.on('SIGHUP', function () {});
/**
* @TERM信号,优雅退出
*/
process.on('SIGTERM', function () {
_self.shutdown = true;
for (var pid in _self.children) {
process.kill(pid, 'SIGTERM');
}
var interval = setInterval(function () {
var num = 0;
for (var pid in _self.children) {
num++;
}
if (_self.shutdown && num === 0) {
clearInterval(interval);
process.exit(0);
}
}, 200);
});
/**
* @USR1信号,重启所有子进程
*/
process.on('SIGUSR1', function () {
push2Kill(_self);
startAll(_self, false, false);
});
return this;
};
/* }}} */
/* {{{ Master prototype close() */
Master.prototype.close = function() {
for (var name in this.bindings) {
var config = this.bindings[name];
if (config._tcp) {
for (var i = 0, l = config._tcp.length; i < l; i++) {
config._tcp[i].close();
}
delete config._tcp;
}
}
for (var pid in this.children) {
process.kill(pid, 'SIGKILL');
}
}
/* }}} */
function Worker(termTimeout) {
if (this instanceof Worker !== true) {
return new Worker(termTimeout);
}
this.termTimeout = termTimeout || -1; // terminate timeout, default is -1, no timeout
/**
* @未处理请求个数
*/
this.remain = 0;
/**
* @已处理请求数(keep-alive下不经过master)
*/
this.scores = 0;
/**
* @进程状态
*/
this.status = CSTATUS.PEDDING;
/**
* @动态监控信息
*/
this.monmsg = {};
/**
* @切换用户
*/
if (process.env.__USER && 'null' != process.env.__USER) {
try {
process.setuid(process.env.__USER);
} catch (e) {
log('worker', e + ', ignore');
}
}
/**
* @切换用户组
*/
if (process.env.__GROUP && 'null' != process.env.__GROUP) {
try {
process.setgid(process.env.__GROUP);
} catch (e) {
log('worker', e + ', ignore');
}
}
}
/* {{{ worker prototype ready() */
Worker.prototype.ready = function (callback) {
this.remain = 0;
this.status = CSTATUS.RUNNING;
function monsend(obj) {
var msg = obj.monmsg;
msg.status = obj.status;
msg.scores = obj.scores;
try {
process.send({
'type' : MESSAGE.HEART,
'data' : msg,
})
} catch (e) {};
}
var worker = this;
monsend(worker);
/**
* @报告心跳
*/
if (process.hasOwnProperty('send')) {
setInterval(function () {
monsend(worker);
}, 2000);
}
/**
* @处理FD
*/
process.on('message', function (msg, handle) {
if (!msg || MESSAGE.REQ_FD !== msg.type || !handle) {
return;
}
if (!callback) {
handle.close();
return;
}
// worker.remain++;
var socket = new Socket({
'handle' : handle,
});
socket.readable = true;
socket.writable = true;
socket.resume();
socket.on('error', function (err) {
worker.release();
});
socket.emit('connect');
callback(socket, msg.port);
});
/**
* @信号捕捉
*/
process.on('SIGUSR1', noop);
process.on('SIGHUP', noop);
process.on('SIGTERM', function () {
worker.status = CSTATUS.STOPING;
// xxx: 需要立即发送心跳作为回应, 使得master不再发送请求过来
monsend(worker);
var termTimer = null;
if (worker.termTimeout > 0) {
// after `termTimeout` seconds, worker process must be killed.
termTimer = setTimeout(function () {
if (CSTATUS.STOPING === worker.status) {
log('worker', 'terminated timeout after ' + process.uptime() + ' seconds.');
process.exit(0);
}
}, worker.termTimeout);
}
setInterval(function () {
if (worker.remain < 1 && CSTATUS.STOPING === worker.status) {
termTimer && clearTimeout(termTimer);
log('worker', 'terminated after ' + process.uptime() + ' seconds.');
process.exit(0);
}
}, 200);
});
};
/* }}} */
/* {{{ Worker prototype transact() */
/**
* @请求开始,占用remain资源
*/
Worker.prototype.transact = function () {
this.remain++;
};
/* }}} */
/* {{{ Worker prototype release() */
/**
* @请求结束,释放remain资源
*/
Worker.prototype.release = function (remain) {
this.scores++;
if ('number' === (typeof arguments[0])) {
this.remain = arguments[0];
} else if (this.remain > 0) {
this.remain--;
}
};
/* }}} */
/* {{{ Worker prototype monset() */
/**
* @设置动态监控变量
*/
Worker.prototype.monset = function (key, value) {
this.monmsg[key] = value;
};
/* }}} */
exports.MSGTYPE = MESSAGE;
exports.Master = Master;
exports.Worker = Worker;
exports.ready = function (callback) {
var worker = new Worker();
worker.ready(callback);
return worker;
};