框架驱动
使用 pomelo 开发应用时,我们一直关注的是给框架填入相应的回调,给 app 配置一些上下文。而没有太多关心整个框架的驱动力量。 在我们启动应用的时候,我们会在 game-server 目录下执行 pomelo start,然后就能看到很多 log 信息,这样就启动了很多服务器。但是我们并没有太多关心框架是如何做到这一点的,在这部分,将试图向你讲述 pomelo 框架是如何驱动的。
首先看下图:

这张图是以 chat 为例,当 chat 应用启动后,使用命令
pstree -au
得到的进程树的一部分,我们从图上可以清楚地看出,pomelo start 调用进程会创建子进程,子进程执行的是 node app.js env=development,然后这个子进程又会创建很多子进程,这些子进程执行的跟原进程同样的文件,只是多了更多的参数,它们后面会有一些诸如id、serverType、port 等这样的参数。好了,现在我们可以做一些简单的解释了,pomelo start 的进程直接创建的那个子进程,实际上就是我们的 master 服务器进程,而由 master 服务器创建的那些子进程,他们执行的是类似于
`node /app.js env=development id=chat-server-1 ...`
这样命令的则是由 master 服务器创建的子进程,这些子进程也就是我们的应用服务器。当然,这里的进程都在一台主机上,所以会有父子关系,也就是说 master 进程是其他应用服务器的父进程。如果各个进程分布在不同的物理主机上的话,pomelo 默认会使用 ssh 的方式远程启动相应的服务器,那样的话,master 进程与应用程序进程就不会再有父子关系了。 下面我们从 pomelo.createApp 调用开始,来梳理 pomelo 的启动过程。 首先我们使用 pomelo start,此时命令行工具 pomelo 会检测 start 后面有没有其他参数,比如是否需要 daemon 的形式启动,后面的参数是否指定了 env 等,在这里,我们仅仅使用了 pomelo start,后面没有跟任何其他参数,此时命令行工具将会为其添加一个默认的 env 参数,即 env=develpment,然后就启动了 node.js 进程,如下:
`node /app.js env=development`
此时 pomelo start 就以没有过多参数的方式启动了app.js。
Application的初始化
接下来我们来分析app.js的执行:
如上面的时序图所示:
- pomelo 调用 createApp();
- pomelo 的 createApp 调用中会调用 app 的 init 方法,完成对 app 的初始化;
- app 会使用 appUtil 提供的 defaultConfiguration 来完成自己的初始化配置;
- appUtil 的 defaultConfiguration 会调用 app 的一些初始化方法,这些方法包括 setEnv,loadMaster,loadServers,parseArgs,configLogger。这里 setEnv 操作会将当前的 env 设定为 development, loadMaster 调用会加载 master 服务器的配置信息,loadServers 会加载所有的应用服务器的配置信息。parseArgs 是一个很关键的操作,由于我们在启动参数中仅仅指定了 env,其他的参数并没有指定,此时 pomelo 认为目前启动的不是应用服务器,而是 master 服务器。这样,当前的进程将使用 master 的配置信息,并将其自己的 serverId,serverType 等参数,均设置为 master 服务器所有的。实际上,对于应用服务器来说,如我们在上面进程树图中看到的那样,如果启动的是应用服务器的话,其 node app.js 后面将会带有很多参数,包括 id,serverType,port,clientPort 等参数,这些参数在 parseArgs 这一步将会被处理,从而确定当前服务器的 id,当前服务器的类型以及其他所必须的配置信息;
- 在执行完上面的操作之后,app 进入到 INITED 状态,同时 pomelo 的 createApp 返回。
Master 服务器启动
当执行完上面的用户编辑的代码后,将会进入到的是 app.start() 调用,当 app.start 调用时,首先会加载默认的组件,对于 master 服务器来说,其加载的默认组件为 master 组件和 monitor 组件。下面主要分析 master 组件在启动中的作用,分析一下 master 组件的启动过程:
如上面的时序图所示:
- 首先 app.start() 首先会加载默认组件,由于没有指定服务器类型,此时会默认为 master 类型,拿到 master 的配置信息,加载 Master 组件,由于 Master 组件是以工厂函数的方式导出的,故会创建 Master 组件,Master 组件的创建过程会创建 MasterConsole,MasterConsole 会创建 MasterAgent,MasterAgent 会创建监听 Socket,用来监听应用服务器的监控和管理请求的;
- 在加载完所有的组件包括 Master 组件后,会启动所有的组件,对于 Master 服务器来说,会启动 Master 组件和 Monitor 组件,也就是调用相应组件的 start 方法,这里先讨论 Master 组件。Master 组件会注册默认的 Module,这是通过 Application 的 registerAdmin 实现的,这里多说一句,如果用户自定义了 Module,可以在 app.start 调用之前调用 registerAdmin,将自定义的 Module 挂到 app 上;
- 在将所有默认的 Module 都挂到 app 上后,Master 组件会启动 MasterConsoleService。在启动 MasterConsoleService 时,MasterConsoleService 会从 app 处拿到所有挂到其上面的 Module,然后将 Module 注册到自己的 Module 仓库中,这一步实际上就是 Module 放到一个以 ModuleId 做键的 Map 中,以使得后来有请求时,可以直接进行查询回调;
- 然后,开启 MasterAgent 的监听,这个时候,Master 组件就已经可以接受监控管理请求了;
- 在开启监听后,下一步就是 enable 所有的 Module,主要就是根据 Module 的参数 type 和 interval,来确定 Module 的回调触发方式,是周期性的由 Master 来拉,还是周期性的由 Monitor 来推,还是不是周期性的触发,使用其他的事件触发方式。如果使用了周期性的触发的话,将会对周期性的事件进行调度,调度是由 pomelo-schedule 提供的;
- 下一步,将启动所有的 Module,如果有 Module 定义了 start 方法的话,将在这步被回调,pomelo 核心中的 Module console 就提供了 start 方法。到这一步为止,Master 组件已经开启了请求监听,挂载好了每一个 Module 相应的回调函数,并且对需要周期执行的监控任务已经完成了调度;
- 是时候启动所有的应用服务器了。当 Master 组件完成了所有的其自身的 Module 的初始化和开启任务后,Master 会委托 Starter 来完成整个服务器群的启动。需要注意:由于每一个服务器一旦启动,就会向 Master 报告其状态,所以 Master 组件必须在启动应用服务器之前就准备好对来自服务器的监控数据作出回应,因此,这也是为什么 Master 组件在完成很多其自身的操作后,才去启动应用服务器;
- 我们知道,在 app 初始化的时候,master 服务器已经加载了所有服务器的配置信息,每一个服务器要启动的地址,服务器的类型,服务器应该监听的端口等信息。Starter 在启动这些服务器的时候将会分析服务器是在本地启动还是远程启动,如果在本地启动的话,将会起一个子进程,如果在远程启动的话,会使用 ssh 的方式远程启动相应的进程。这个时候的启动命令行,将会附带更多的参数,对于 chat 例子中来说,其 chat-server 启动命令行大概如下面的样子:
node
/app.js env=development id=chat-server-1 host=127.0.0.1 port=6050 serverType=chat - 此后,应用服务器将会被启动;
- 在调用了 Master 组件的 start 方法后,由于 Master 方法没有定义 afterStart 方法,故此时 Master 组件的启动过程已经完毕,Master 组件将会监听来自其他服务器的 Monitor 发出的请求或者给其他的 Monitor 推送通知,当然这些操作都是在对应的 Module 定义的回调中进行的。
应用服务器启动
- 对于应用服务器来说,与 master 服务器一样,会执行 app.js,也会创建 app,加载一些配置信息,setEnv 等。应用服务器与 Master 服务器在 App init 阶段唯一的不同就是,Master 服务器的启动命令行中,没有具体服务器参数,使得 pomelo 会把其当作 Master 服务器启动,而应用服务器的启动命令行,则包含了全部的应用服务器参数,因此 pomelo 会按照他们具体的配置信息为其默认加载不同的组件,对于 master 服务器来会加载 Master 组件,对于应用服务器来说,则会根据应用服务器的配置不同,默认加载相应的组件;
- 在创建完 app,并完成初始化后,app.js 中的对 app 进行配置的代码会执行,这里包括 app.route 调用,app.set 调用等等;
- 在执行完用户的配置代码后,app.js 进入到 Application.start 调用,在这里 pomelo 框架会针对每一种服务器将默认加载不同的组件。 对于 pomelo 来说,所有服务器的执行都可以看作是对其组件进行生命周期管理的过程。每一个 pomelo 的组件都会定义 start,afterStart,stop 方法,用来框架进行生命周期管理时候回调。下面展示了其调用顺序:
- pomelo 框架总是先顺序地调用所有组件的 start 方法后,才去顺序的调用每个组件的 afterStart 方法。设置 afterStart 方法的原因是,可能有组件互相依赖的时候,某些组件的某些启动过程依赖于另外某个组件的启动过程时,可以将有依赖的部分放到 afterStart 方法里面,这样可以使得在有依赖的部分执行时,其所依赖的组件已经完成初始化。或者还有一些需要等待全局就绪的工作也可以放到这里来做;
- 不同的组件在启动时会开启不同的功能,关于应用服务器加载的组件的具体启动过程,由于涉及到其提供的功能细节,将在后面介绍,这里关注的不是组件的细节功能,而是整个应用程序的驱动方式;
- 当所有组件的 afterStart 方法都调用完毕后,应用服务器启动完成。此时,由于其加载的组件开启了监听接口,应用服务器将接受外来的请求,然后进入相应的回调。
服务器的关闭
当我们调用了 pomelo start 启动了 master 服务器,master 服务器又启动了所有的应用服务器后,所有的服务器都进入到自己的事件循环中,接受外来的请求事件或者是自身的定时器事件执行相应的回调方法。当我们想关闭服务器时候,我们可以很暴力地使用 Ctrl+C 直接结束掉所有的进程,也可以优雅地通过命令行工具执行 pomelo stop 进行关闭。实际上,命令行工具是作为 pomelo 管理框架中的 client 角色的,它使用的 Module 是 pomelo 内建的 console,通过 console Module 给 master 服务器发出关闭服务器请求,master 会给每一个服务器发出停止通告。每个应用服务器接收到 stop 通告后,回调函数里面会调用 app.stop,app.stop 则会如上面图中那样,按照加载的逆序分别调用每一个组件的 stop 回调方法。当所有的组件的 stop 调用完后,服务器完成关闭。master 在发出停止通告后,自己稍作等待也调用 app.stop,stop 掉自己加载的组件完成关闭。