eggjs启动从回车到ready
JavaScript代码是单线程运行的,因而一旦有未捕获的异常抛出线程就会挂掉,业务也就不可访问了,所以一般我们在使用koa,express,thinkjs等其他node框架时一般会使用pm2去管理node进程,保证业务的高可用性。而阿里开源的egg框架本身自带的egg-cluster模板已经帮我们做了这个事情,egg的多进程模型和进程间通信官方文档上写的已经很清楚了,今天学习一下源码,希望有所收获。
egg-bin dev
运行一个egg项目,npm run dev
在package.json文件里我们发现默认其实执行的是egg-bin dev
。egg-bin原来是egg提供的一个开发时使用的命令行工具,翻开egg-bin的代码,我们可以看到egg-bin其实是基于common-bin开发的,这里不赘述common-bin的用法,感兴趣的童鞋自行去查阅。在lib/cmd/dev.js
里我们可以看到egg-bin dev
执行的逻辑(去掉debug日志):
1 | * run(context) { |
从上面代码可以看出主要有两步,
- this.formatArgs(context),将context上的参数转成自己需要的格式
- this.helper.forkNode(),这个是
common-bin
的语法:
forkNode(modulePath, args, opt) - fork child process, wrap with promise and gracefull exit
forkNode函数用于fork一个子进程,第一个参数子进程要执行的文件的路径
1 | this.serverBin = path.join(__dirname, '../start-cluster'); |
start-cluster
文件里主要源码如下:
1 | const options = JSON.parse(process.argv[2]); |
到此我们知道整个egg-bin dev
想要做的事情只有两件:
- 获取参数options,重点是options.framework,即找到要加载的框架
- 执行require(framework).startCluster(),加载框架并执行startCluster
回过头来继续看lib/cmd/dev.js
,在formatArgs
函数里我们找到获取framework的逻辑:接下来我们去看下getFrameworkPath的实现逻辑。1
2
3
4
5const utils = require('egg-utils');
argv.framework = utils.getFrameworkPath({
framework: argv.framework,
baseDir: argv.baseDir,
});
egg-utils
在lib/framework.js
里我们很容易找到:
1 | function getFrameworkPath({ framework, baseDir }) { |
上面代码详细讲述了获取framework的逻辑:
- 首先看
npm run dev
执行时是否指定framework,有的话继续判断是否是绝对路劲,转为合适的格式返回 - 尝试读取package.json,查看是否有egg以及egg的framework配置
- 默认返回
egg
这里我们看默认egg的情况,则forkNode执行的语句为require('egg').startCluster(options)
require(‘egg’).startCluster(options)
在egg的index.js
文件的开头我们看到:
1 | /** |
很好,终于看到egg-cluster
了。
egg-cluster
首先是index.js
中暴露的startCluster方法,也是整个egg-cluster的入口方法:
1 | exports.startCluster = function(options, callback) { |
可以看出,egg-cluster是一个Master实例,Master就显得至关重要了。
介绍master之前先梳理一下egg-cluster的目录结构
1 | egg-cluster |
Master
先看master.js,下面截取出Master构造函数的关键代码:
1 | this.options = parseOptions(options); |
从上往下执行,首先是parseOptions,这个函数是lib/utils/options.js
,用来解析并返回正确格式的options
options.js
1 | function(options) { |
上面代码片段仅截取部分,显示options的各个属性,和egg-bin dev提供的基本一致。具体的略过不讲。
接下来是new Manager();
Manager
将manager.js里的属性和方法简单画成UML图形如下:
Manager主要是用于管理agent和worker,有两个方法比较特殊,分别是count()和startCheck(),count()返回agent和worker的数量,而startCheck()每10秒执行一次,判断count返回的agent和worker数量是否大于0,出现3次异常,则发出exception事件,并不再执行。
Messenger
将messenger.js里的属性和方法简单画成UML图如下:
Messenger类
- send: 做了一些from和to的处理工作,并根据from和to,调用其他四个方法。
- sendToMaster: 使用的是this.master.emit方法,Master本身就是继承于EventEmitter,直接调用emit方法,使用master.on既可以监听到。
- sendToParent: 调用的是process.send()
If Node.js is spawned with an IPC channel, the process.send() method can be used to send messages to the parent process. Messages will be received as a ‘message’ event on the parent’s ChildProcess object.
- sendToAppWorker: 调用的是sendmessage(worker, data);
- sendToAgentWorker: 调用的是sendmessage(agentWorker, data);
sendmessage(childprocess, message): Send a cross process message. If a process is not child process, this will just call process.emit(‘message’, message) instead.
terminate.js
terminate.js文件主要用于终止进程,这里不再赘述。
启动agentWorker,agent_worker.js
回到Master的构造函数中,从之前整理出的代码片段来看,实例化manager,messenger之后,注册ready的回调函数,接下来就是启动agent进程了。
1 | forkAgentWorker() { |
上面片段仅截取关键部分。可以看出使用了node的原生模块child_process
的fork方法。下面继续看agent_worker.js
;
1 | const Agent = require(options.framework).Agent; |
从上面可以看到agentWorker实例了framework的Agent,而根据之前的分析,framework默认情况下是egg,这里为了简化分析,将framework认为是egg,那可以认为这个子进程执行了new Agent(options)操作;并且在ready回调中向master进程发送agent-start
消息。而发送的这个消息则至关重要,master进程中对于它的监听回调函数中执行了worker进程的fork。
启动appWorker,app_worker.js
仍然回到Master的构造函数那里,可以看到
this.once(‘agent-start’, this.forkAppWorkers.bind(this));
当agentWorker进程启动ready后,发送agent-start消息给master进程,master进程第一次收到后执行forkAppWorkers();
1 | forkAppWorkers() { |
cfork npm包,使用原生cluster的setupMaster方法和fork方法。对cfork感兴趣的童鞋可以去看
cluster fork and restart easy way
我们接着看简化版的app_worker.js
1 | const Application = require(options.framework).Application; |
从上面可以知道,app_worker中执行了new Application(),并使用原生http(或https)模块启动一个server。当server执行listen方法时,触发了master中 forkAppWorkers方法中注册的listening回调事件
cluster.on(‘listening’, (worker, address) => {…});
该回调事件中向maste发送了app-start
事件。app-start
的回调函数中在最后执行了
this.ready(true);(这里使用了get-ready, 构造函数中通过
ready.mixin(this);
,注入ready方法,并添加回调函数)
这一句会触发master构造函数中注册的ready回调函数。该回调函数中将isStarted设置成true, 并想parent,app,agent发送egg-ready
事件。
到这里启动就基本完成了。
启动的时序正如官方文档所描述的:
1 | +---------+ +---------+ +---------+ |
写在最后
本文只是简单的从源码角度大致梳理了egg启动过程中的做的一些事情,很多东西还需要进一步深入研究,比如agent_worker.js
使用的是child_process
的fork方法,而app_worker.js
使用的是cfork(使用的是原生的cluster的fork),需要研究下child_process和cluster。此外,本文还没涉及到Agent和Application的具体的实现,Agent和Application都是基于EggApplication,而EggApplication是基于EggCore的,EggCore继承于Koa。等等。