process.on('uncaughtException', function (err) {
......
// 对http连接设置 Connection: close响应头
servers.forEach(function (server) {
if (server instanceof http.Server) {
server.on('request', function (req, res) {
// Let http server set `Connection: close` header, and close the current request socket.
req.shouldKeepAlive = false;
res.shouldKeepAlive = false;
if (!res._header) {
res.setHeader('Connection', 'close');
}
});
}
});
// 设置一个定时函数关闭子进程, 并退出本身进程
// make sure we close down within `killTimeout` seconds
var killtimer = setTimeout(function () {
console.error('[%s] [graceful:worker:%s] kill timeout, exit now.', Date(), process.pid);
if (process.env.NODE_ENV !== 'test') {
// kill children by SIGKILL before exit
killChildren(function() {
// 退出本身进程
process.exit(1);
});
}
}, killTimeout);
// But don't keep the process open just for that!
// If there is no more io waitting, just let process exit normally.
if (typeof killtimer.unref === 'function') {
// only worked on node 0.10+
killtimer.unref();
}
var worker = options.worker || cluster.worker;
// cluster mode
if (worker) {
try {
// 关闭TCP连接
for (var i = 0; i < servers.length; i++) {
var server = servers[i];
server.close();
}
} catch (er1) {
......
}
try {
// 关闭ICP通道
worker.disconnect();
} catch (er2) {
......
}
}
});
前言
最近用Egg作为底层框架开发项目,好奇其多进程模型的管理实现,于是学习了解了一些东西,顺便记录下来。文章如有错误, 请轻喷
为什么需要多进程
伴随科技的发展, 现在的服务器基本上都是
多核cpu
的了。然而,Node是一个单进程单线程
语言(对于开发者来说是单线程,实际上不是)。我们都知道,cpu的调度单位是线程
,而基于Node的特性,那么我们每次只能利用一个cpu。这样不仅仅利用率极低,而且容错更是不能接受(出错时会崩溃整个程序)。所以,Node有了cluster来协助我们充分利用服务器的资源。cluster工作原理
关于cluster的工作原理推荐大家看这篇文章,这里简单总结一下:
hack
掉,而是统一由master的内部TCP监听
,所以不会出现多个子进程监听同一端口而报错的现象。请求统一经过master的内部TCP
,TCP的请求处理逻辑中,会挑选一个worker进程向其发送一个newconn内部消息,随消息发送客户端句柄
。(这里的挑选有两种方式,第一种是除Windows外所有平台的默认方法循环法,即由主进程负责监听端口,接收新连接后再将连接循环分发给工作进程。在分发中使用了一些内置技巧防止工作进程任务过载。第二种是主进程创建监听socket后发送给感兴趣的工作进程,由工作进程负责直接接收连接。)创建客户端实例(net.socket)执行具体的业务逻辑
,然后返回。如图:
图引用 出处
多进程模型
先看一下Egg官方文档的进程模型
大致上就是利用
Master
作为主线程,启动Agent
作为秘书进程协助Worker
处理一些公共事务(日志之类),启动Worker
进程执行真正的业务代码。多进程的实现
流程相关代码
首先从
Master
入手,这里暂时认为Master是最顶级的进程(事实上还有一个parent
进程,待会再说)。先从
Master的构造函数
看起综上, 可以看到Master的构造函数主要是
初始化和注册各类相应的事件
, 最后运行的是forkAgentWorker
函数, 该函数的关键代码可以看到:继续到
agent_worker.js
上面看,agent_worker
实例化一个agent
对象,agent_worker.js
有一句关键代码:可以看到,
agent_worker.js
中的代码向master
发出了一个信息, 动作为agent-start
, 再回到Master
中, 可以看到其注册了两个事件, 分别为once的forkAppWorkers和 on的onAgentStart
先看
onAgentStart
函数, 这个函数相对简单, 就是一些信息的传递:然后会执行
forkAppWorkers
函数,该函数主要是借助cfork包fork
对应的工作进程, 并注册一系列相关的监听事件,可以看到
forkAppWorkers
函数在监听Listening
事件时,会触发master
上的app-start
事件。总结下:
进程守护
根据官方文档,进程守护主要是依赖于graceful和egg-cluster这两个库。
未捕获异常
由执行的app文件可知,
app
实际上是继承于Application类, 该类下面调用了graceful()
。继续看
graceful
, 可以看到它捕获了process.on('uncaughtException')
事件, 并在回调函数里面关闭TCP
连接, 关闭本身进程, 断开与master
的IPC
通道。ok, 关闭了
IPC
通道后, 我们继续看cfork
文件, 即上面提到的fork worker
的包, 里面监听了子进程的disconnect
事件, 他会根据条件判断是否重新fork
一个新的子进程一般来说, 这个时候会继续等待一会然后就执行了上面说到的定时函数了, 即
退出进程
。OOM、系统异常
关于这种系统异常
, 有时候在子进程中是不能捕获到
的, 我们只能在master中进行处理, 也就是cfork
包。进程间通信(IPC)
在
master
中, 可以看到当agent和app被fork时
, 会监听他们的信息, 同时将信息转化成一个对象:可以看到最后调用的是
messenger.send
, 而messengeer.send就是根据from和to来决定将信息发送到哪里
master
则是直接根据action
信息emit
对应的注册事件而agent和worker则是通过一个
sendmessage
包, 实际上就是调用下面类似的方法最后, 在agent和app都继承的基础类
EggApplication
上, 调用了Messenger
类, 该类内部的构造函数如下:总结一下:
思路就是利用事件机制和IPC通道来达到各个进程之间的通信。
其他
学习过程中有遇到一个timeout.unref()的函数, 关于该函数推荐大家参考这个问题的6楼回答
总结
从前端思维转到后端思维其实还是很吃力的,加上Egg的进程管理实现确实非常厉害, 所以花了很多时间在各种api和思路思考上。
参考与引用
多进程模型和进程间通讯
Egg 源码解析之 egg-cluster