精华 Egg系列分享 -egg-bin/scripts/cluster
发布于 20 天前 作者 lianxuify 1154 次浏览 来自 分享

enter image description here

《陪伴》摄于 山东蓬莱岛


目录

  • 前言
  • 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)

  1. 启动master
  2. 初始化进程管理对象
  3. 初始化进程通信对象
// 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)

  1. 启动agent
  2. 启动worker
  3. 监听各种事件
  4. 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 核心逻辑

  1. 启动agent
  2. 传递aent状态
  3. 添加优雅退出
  4. 监听异常主动退出
// 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 却不能提供?

谢谢

6 回复

写的不错,点个赞,可以投个稿到我们专栏~ 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 两者情况做处理

回到顶部