最近 socket 服务遇到了脏进程的问题,也有人叫僵尸进程,复现步骤基本如下
const cp = require('child_process');
const worker1 = cp.fork('./bin/worker');
const worker2 = cp.fork('./bin/worker');
setTimeout(() => {
console.info(`主进程关闭`);
process.exit();
}, 3000);
// bin/worker
setInterval(() => {
console.info(`子进程 setInterval:`, Date.now());
}, 3000);
无论是process.exit()
还是 ctrl+c 结束,主进程关闭后子进程依然在进行,查看 NODE API 发现有这一句:
On Linux, child processes of child processes will not be terminated when attempting to kill their parent.
在 Linux,子进程的子进程不会被终止,当它们的父进程退出时
然后在网上有人说:
默认情况,父进程退出,子进程变为"孤儿进程",孤儿进程会被托管给进程 init,也就是进程 1,所以 kill B 进程,B 进程的子进程 C D 会被托管给进程 1,不会被终止。
请注意“僵尸进程”,你 KILL B 进程的时候 ,A 进程有没有 wait,如果没有会产生僵尸进程。想终止 CD 进程 可以对 CD 执行 kill 操作强制终止,发送 KILL 信号,以及使用 IPC 来通知进程,让进城自己退出。
NODE API 中有提供方法process.kill(pid[, signal])
,用于终止指定进程,但如果 PID 已经重新分配给其他进程,信号就会被发送到新的进程里,造成难以复现和排查的错误,且,子进程退出前通常需要做一些收尾工作,比如发出警报,收集日志等,所以直接终止子进程大部分时间也并不适用
最终方案:
// 主
process.on('uncaughtException', (err, origin) => {
// 记录日志、发起报警
elegantExit();
});
process.on('exit', elegantExit);
process.on('SIGINT', elegantExit); //catches ctrl+c event
process.on('SIGUSR1', elegantExit); // catches "kill pid" (for example: nodemon restart)
process.on('SIGUSR2', elegantExit);
// 优雅退出
function elegantExit() {
if (elegantExit.called) return;
elegantExit.called = true;
子进程.send({ cmd: 'SIGHUP' }); // 不使用 kill 方法,仅 send 一条 SIGHUP 消息
process.exit();
}
// 子
process.on('message', ({ cmd }) => {
if (cmd === 'SIGHUP') exitProcessByMain();
});
process.on('uncaughtException', (err, origin) => {
// 记录日志、发起报警
exitProcessByMain();
});
// 优雅退出
function exitProcessByMain() {
// 3000s 后进程还在,手动结束
const to = setTimeout(() => process.exit(), 3000);
to.unref(); // unref 不会把进程保持住
}
关于pm2,它通知会辅助管理进程,比如进程守护和垃圾进程回收(第一个例子使用 pm2 进程启动、停止、重启并不会产生上面的问题)。但实际应用中,子进程会被未断开的连接保持住,导致无法正常结束,且 pm2 在这种情况下并没有正确处理,且,如果这时手动 kill -KILL PID 会触发 pm2 的进程守护,导致这个僵尸进程引发一些无法想象事情
网友评论