《陪伴》摄于 山东蓬莱岛
目录
- 前言
- egg-bin
- egg-bin 命令
- egg-bin 整体
- egg 进程模型
- egg-scripts
- egg-scripts 疑问
- egg-scripts 核心逻辑
- egg-scripts start 做了什么
- egg-scripts stop 做了什么
- egg-cluster
- egg-cluster 历史方案
- egg-cluster 解决的问题
- egg-cluster 使用的模块
- cfork 核心逻辑
- egg-cluster启动时序
- egg-cluster 启动 master 核心逻辑(1)
- egg-cluster 启动 master 核心逻辑(2)
- egg-cluster 启动agent 核心逻辑
- Applicaion/Agent 启动过程
- 交流
- 谢谢
前言
- 分享模块
- 分享内容
- why? 为什么有它们?
- what? 它们做了什么?
- how? 它们如何做的?
- 分享期望
- 我懂了!So Easy!
egg-bin
问:xx-bin 是做什么的? 答:统一解决一个生态、一个团队各种仓库里的开发命令 & 开发依赖 好处:
- 大家统一了,开箱即用
- 各仓库不在安装和共建链相关的依赖和配置,上手快
egg-bin 命令
egg开发者生态工具,集成到egg中,包含命令:
- test 单元测试
- debug 本地调试启动egg (ts-node支持)
- dev 本地开发启动egg (解决没有启动入口,对应egg-cluster start)
- cov 代码覆盖率
- autod 自动化生成 pkg.dependencies/devDependencies
- pkgfile 自动化生成 pkg.files 用来 npm 包发布
egg-bin 整体
egg 进程模型
在讲egg-scripts/egg-cluster 模块之前,我们回顾下egg进程模型, Worker 又名 Application
+--------+ +-------+
| Master |<-------->| Agent |
+--------+ +-------+
^ ^ ^
/ | \
/ | \
/ | \
v v v
+----------+ +----------+ +----------+
| Worker 1 | | Worker 2 | | Worker 3 |
+----------+ +----------+ +----------+
egg-scripts
定位:egg进程管理工具 - 生产环境 ,类比pm2
- egg-scripts start --daemon
- egg-scripts stop
egg-scripts 疑问
- “NODE_ENV=production egg-scripts start --title=demoserver --env=prod --workers=4 --daemon”
- egg-script 做了什么?进程守护在不在这里?
- egg-script 和 egg-cluster 什么关系?
egg-scripts 核心逻辑
这个模块和前面所示egg进程模型一点关系都没有, 只是如的parent,当isDaemon=true,它们唯一的关系也断了
// 核心逻辑
-> egg-scripts
-> spawn(path.join(__dirname, '../start-cluster'), eggArgs, options)
-> egg.startCluster
-> require('egg-cluster').startCluster
-> new Master(options).ready(callback)
┌────────┐
│ parent │
└────────┘
|
┌────────┐
│ master │
└────────┘
/ \
┌───────┐ ┌───────┐
│ worker│ ------ │ agent │
└───────┘ └───────┘
egg-scripts start 做了什么
- 问:egg-scripts 做了什么?
- 答: 继承common-bin 定义了两个命令 start/stop 命令和参数, start 核心逻辑如下
- 参数定义、检查、初始化、如日志路径
- 参数的透传
- spawn(path.join(__dirname, ‘…/start-cluster’), eggArgs, options)
- …/start-cluster.js 内容: require(options.framework).startCluster(options);
- options.framework 值: package.egg.framework || egg
- isDaemon 是否后台运行
- 日志路径不同 options.stdio = [ ‘xxx’, ‘xxx’, ‘xxx’, ‘ipc’ ]
- 子进程独立 child.unref()
egg-scripts stop 做了什么
- 问:egg-scripts stop 做了什么?
- 答:stop 核心逻辑
- 参数定义、检查
- 找到该服务启动的所有对应的进程:this.helper.findNodeProcess
- 过滤出master进程 “start-cluster"
- 杀死master进程,等待5秒, 内部会捉到杀死worker/agent
- this.killAppWorkers(appTimeout);
- this.killAgentWorker(agentTimeout);
- 检查worker(application)/agent 是否都主动已经杀死 ,正则匹配pid 启动path
- 兜底,通过pid 发送型号量杀死 this.helper.kill(pids, ‘SIGKILL’)
egg-cluster
在讲egg-cluster模块之前,我们再先回顾下nodejs cluster https://nodejs.org/dist/latest-v12.x/docs/api/cluster.html, 翻译:
- 单个 Node.js 实例运行在单个线程中,为了充分利用多核系统,有时需要启用一组 Node.js 进程去处理负载任务。
- cluster 模块可以创建共享服务器端口的子进程。
egg-cluster 历史方案
没有原生方案之前
- 方案1: 开启多个进程,每个进程绑定不同的端口,主进程对外接受所有的网络请求,然后代理转发。问题:
- 代理性能
- 浪费文件描述符
- 方案2:改进 - master创建createServer,listen端口,通过fork创建多个子进程,master把server传递给子进程,每个监听 server.on(‘connection’)事件处理。问题
- 惊群效应
- 方案3:再改进 - master创建createServer,listen端口,master进程监听connection事件。将获得的当前请求socket负载均衡的传递给子进程。问题
- 如何简单、快速、稳定、日志完善,拥有该模式
egg-cluster 解决的问题
nodejs cluster 解决的问题:
- 原生提供稳定、简单的使用方式
- 一份代码启动多个进程
- 多个进程监听同一个端口, hack掉worker进程中的net.Server实例的listen方法
- 请求-负载均衡算法
- 请求-句柄的传递
- 进程守护相关 (只负责提供 启动方法cluster.fork 和 workert退出事件disconnect/exit)
egg-cluster 使用的模块
- nodejs cluster 没有包含进程守护。故 egg-cluster 用了 npm包 cfork
- 为了优雅退出,退出前先清理。故 egg-cluster 用了 npm包 graceful-process
其中 graceful-process 核心逻辑很简单
process.once('SIGTERM', () => {
yourExitCb(0);
});
cfork 核心逻辑
// 1. 守护 worker 进程
cluster.on('exit', function(){
clsuter.fork();
});
cluster.on('disconnect', function(){
clsuter.fork();
});
// 2. 重启限流
var canFork = reforks.length < 60 || span > 60000;
if (!canFork) {
cluster.emit('reachReforkLimit');
}
// 3. master 进程异常捕获
if (process.listeners('uncaughtException').length === 0) {
process.on('uncaughtException', onerror);
}
egg-cluster 启动时序
在讲egg-cluster模块之前,我们先整体看下egg启动时序模型
- 先启动master, 再启动agent ,再启动worker
+---------+ +---------+ +---------+
| Master | | Agent | | Worker |
+---------+ +----+----+ +----+----+
| fork agent | |
+-------------------->| |
| agent ready | |
|<--------------------+ |
| | fork worker |
+----------------------------------------->|
| worker ready | |
|<-----------------------------------------+
| Egg ready | |
+-------------------->| |
| Egg ready | |
+----------------------------------------->|
egg-cluster 启动 master 核心逻辑(1)
- 启动master
- 初始化进程管理对象
- 初始化进程通信对象
// 1. 启动master进程, egg-scripts 里spawn启动的进程
new Master()
// 2. 初始化进程管理对象, 作用
// 2.1 获取worker/agent数量
// 2.2 获取worker/agent对象(如消息广播)
// 2.3 获取pid
// 2.4 startCheck 10s检查一次是否,如果count都为0则退出 master
this.workerManager = new Manager();
// 3. 初始化进程模型中之间相关通信对象工具
this.messenger = new Messenger(this);
// 使用如:
process.send({
action: 'xxx', // emit对应的事件
data: '', // 数据
to: 'agent/master/parent', // default to app
});
egg-cluster 启动 master 核心逻辑(2)
- 启动agent
- 启动worker
- 监听各种事件
- agent进程守护
// 1. 启动agent
this.forkAgentWorker();
// 2. 启动worker
this.once('agent-start', this.forkAppWorkers.bind(this));
// 3. 监听各种事件,作用:
// 3.1 打印日志
// 3.2 传播事件
// 3.3 删除监听事件和清理现场
this.on('agent-exit', this.onAgentExit.bind(this));
this.on('agent-start', this.onAgentStart.bind(this));
this.on('app-exit', this.onAppExit.bind(this));
this.on('app-start', this.onAppStart.bind(this));
this.on('reload-worker', this.onReload.bind(this));
process.once('SIGINT', this.onSignal.bind(this, 'SIGINT'));
process.once('SIGQUIT', this.onSignal.bind(this, 'SIGQUIT'));
process.once('SIGTERM', this.onSignal.bind(this, 'SIGTERM'));
process.once('exit', this.onExit.bind(this));
// 4. agent进程守护
// 4.1 而worker守护通过 npm cfork 内部实现
// 4.2 master进程不能守护,不然关闭不了
// 4.3 还记得前面讲到的egg-script stop 是如何啥死所有进程的不?
setTimeout(() => {
this.logger.info('[master] new agent_worker starting...');
this.forkAgentWorker();
}, 1000);
egg-cluster 启动 agent 核心逻辑
- 启动agent
- 传递aent状态
- 添加优雅退出
- 监听异常主动退出
// 1. 启动agent
-> this.forkAgentWorker();
-> childprocess.fork(this.getAgentWorkerFile(), args, opt)
-> getAgentWorkerFile() { return path.join(__dirname, 'agent_worker.js'); }
-> const Agent = require(options.framework).Agent; new Agent(options);
// 2. 传递状态
process.send({ action: 'agent-start', to: 'master' });
// 3. 添加优雅退出
gracefulExit({ beforeExit: () => agent.close(),});
// 4. 监听异常主动退出
agent.once('error', function() {...; logger; process.kill(process.pid);});
Applicaion/Agent 启动过程
- 启动 worker -> new Applicaion()
- 启动 agent -> new Agent()
这部分是egg之所以为"egg"的核心设计, 待更新更多分享的链接
交流
以上将egg的进程管理核心代码过了一遍,进一步思考:
- 问题1:pm2 reload 热更新做了啥?
- 问题2:pm2 提供reload命令,为啥 egg 却不能提供?
谢谢
写的不错,点个赞,可以投个稿到我们专栏~ https://zhuanlan.zhihu.com/eggjs
“NODE_ENV=production egg-scripts start --title=demoserver --env=prod --workers=4 --daemon”
不需要传 NODE_ENV
, egg-scripts 里面做了
其中 graceful-process 核心逻辑很简单
这里其实没那么简单,判断 disconnect 那里才是关键。
关于 pm2 的进一步思考
不错的问题,如果有课后答案就更好了~
@atian25 好的,我整理补充下,然后投个搞~
课后答案: 问题1:pm2 reload 热更新做了啥?
- 当我们执行pm2 reload 时是一种热更新,对流量无影响。pm2 自己做的进程管理, master依次将worker 杀死再启动,保证同时至少一个worker可用。杀死一个worker同时,master不再将流量派发给当前正在重启的worker,同时为了保证之前已经在处理的流量请求处理完毕, 杀死前会等待些时间 。"By default, pm2 waits 1600ms before sending SIGKILL signal if the applications doesn’t exit itself。 cluster 模式 default to 3000ms。
- 更多优雅退出和优雅启动官方文档:https://pm2.io/docs/runtime/best-practices/graceful-shutdown/
问题2:pm2 提供reload命令,为啥 egg 却不能提供?
- egg 进程管理和流量负载均衡使用的是nodejs 原生的 cluster, nodejs官方不支持热更新
- 重启Worker:cluster无法控制流量,也无法保证正在处理的请求不中断
- 删除require.cahe ,这个处理不当会造成内存泄露。笔者也曾给 nodejs14 贡献过一个官方自己该问题处理不当, 造成的内存泄露问题: https://github.com/nodejs/node/pull/32837
- 故eggjs官方推荐:在nodejs服务前面的Router层处理,当服务部署时将流量全部切走。毕竟定位是企业级应用,多机器甚至多集群,以及部署过程中流量自动切流是运维层各公司的标配~
@atian25 已更新相关问题 & 已投稿,感谢,天猪~
graceful-process
有针对是 cluster 和 fork 两者情况做处理