认识 procfile.js

procfile.js 是一个普通的 js 文件,提供链式的语法定义应用。

procfile.js 是描述应用进程结构的文件,可以基于 procfile.js 来编排进程结构,使用 Pandora.js 提供的各种强大功能。

当然把进程交给 Pandora.js 管理,不只是帮你创建进程这么简单,更是:

Pandora.js 会守护创建出来的进程。

小到自动重启、切割日志文件、重启次数计数。

大到 30 多项 Metrics 指标采集、自动的全链路 Trace 追踪、对接现有 APM (比如 Open-Falcon)等等。

应该把 procfile.js 放在哪里?

Pandora.js 会在下述位置查找 procfile.js

  • ${appDir}/procfile.js

启动一个简单的 Node.js 程序

下文中 pandora 对象的类型为 ProcfileReconcilerAccessor,你可以点击查看详细 API。

我们先看个例子:

procfile.js

  1. // procfile.js 是一个普通的 Node.js 模块,必须导出一个 function
  2. // function(pandora) 的第一个参数是 pandora,这个对象用于定义我们的进程结构
  3. module.exports = function(pandora) {
  4. pandora
  5. // 定义一个进程,名字叫 processA
  6. .process('processA')
  7. // 如果 scale 大于 1 ,将使用 Node.js 的 Cluster 模块自动产生进程组
  8. // 默认值即是 1
  9. .scale(1)
  10. // 定义进程环境变量,创建出来的进程中可以通过 process.env 获得
  11. .env({
  12. ENV_VAR1: 'VALUE_OF_ENV_VAR1'
  13. })
  14. // 这个进程的入口文件地址
  15. .entry('./app.js');
  16. }

app.js

  1. console.log(`Got the value of ENV_VAR1: ${process.env. ENV_VAR1} from Process [pid = ${process.pid}]`);

然后我们运行命令:

  1. $ pandora dev # 通过 pandora dev 前台启动应用,多用于本地调试
  2. Got the value of ENV_VAR1: VALUE_OF_ENV_VAR1 from Process [pid = 19766]
  3. 2017-12-11 13:59:12,578 INFO 19530 Process [name = processA, pid = 19766] Started successfully!
  4. ** Application start successful. **

基于 Scale 自动缩放进程

尝试把上面例子中的 .scale(1) 改为 .scale(4),然后前台启动:

  1. $ pandora dev # 通过 pandora dev 前台启动应用,多用于本地调试
  2. Got the value of ENV_VAR1: VALUE_OF_ENV_VAR1 from Process [pid = 19913]
  3. Got the value of ENV_VAR1: VALUE_OF_ENV_VAR1 from Process [pid = 19915]
  4. Got the value of ENV_VAR1: VALUE_OF_ENV_VAR1 from Process [pid = 19916]
  5. Got the value of ENV_VAR1: VALUE_OF_ENV_VAR1 from Process [pid = 19914]
  6. 2017-12-11 14:04:57,836 INFO 19910 Process [name = processA, pid = 19912] Started successfully!
  7. ** Application start successful. **

我们可以看到进程扩展到了 4 个,分别是 19913、19915、19916、19914。我们还可以看到 19912 ,这是四个进程的 Master 。

下图可能更容易理解:

img

两个便捷的 Alias:Fork 和 Cluster

为了方便用户使用,我们提供了两个简便 Alias ,Cluster 和 Fork。一方面是为了简便使用。更是为了兼容存量的 Node.js 应用,让存量 Node.js 也可以很方便的使用。

Fork

pandora.fork() 是 process.process() 的一个 Alias。

procfile.js

  1. module.exports = function(pandora) {
  2. // 直接启动 ./app.js,等价于直接用 child_process.spawn 启动。
  3. pandora.fork('aForkedProcess', './app.js');
  4. }

上面的例子等价于下面:

procfile.js

  1. module.exports = function(pandora) {
  2. pandora
  3. // 定义一个进程 aForkedApp
  4. .process('aForkedApp')
  5. // 指定只启动一个
  6. .scale(1)
  7. // 指定入口文件地址
  8. .entry('./app.js');
  9. }

Cluster

pandora.cluster() 是 process.service() 的一个 Alias。

procfile.js

  1. module.exports = function(pandora) {
  2. // 用 Cluster 模块启动 app.js
  3. // 默认分配到 worker 进程
  4. // Pandora.js 内置了一个 process('worker').scale('auto') 的定义( 'auto' 表示 CPU 数量 )
  5. pandora.cluster('./app.js');
  6. }

上面的例子等价于下面:

procfile.js

  1. module.exports = function(pandora) {
  2. // 定义 worker 进程,其实这个定义 Pandora.js 早已内置
  3. pandora
  4. .process('worker')
  5. .scale('auto');
  6. // 定义一个 Service,并分配到 worker 进程
  7. // 只做一件事情,引入 './app.js'
  8. pandora
  9. .service('scalableService', class ScalableService {
  10. start() {
  11. require('./app.js');
  12. }
  13. })
  14. .process('worker');
  15. }

Service 机制

上面讲到了 pandora.cluster() 是 process.service() 的一个 Alias,那 Service 是什么呢?

Service 也是往进程里定义 Node.js 程序,不过更结构化,提供下面的基础能力:

  1. 标准的 async start() 接口,用户可以异步的启动 Node.js 程序。
  2. 标准的 async stop() 接口,用户可以优雅的下线 Node.js 程序(比如优雅下线 RPC 服务,从而避免出现服务闪断)。
  3. 结构化的日志管理、配置能力。
  4. 进程内的启动顺序(依赖关系)管理。

下面是一个简单的例子:

procfile.js

  1. module.exports = function (pandora) {
  2. pandora
  3. // 定义一个 Service ,名字叫 httpServer,实现在 ./httpServer
  4. .service('httpServer', './httpServer')
  5. // 对其配置
  6. .config({
  7. port: 1338
  8. })
  9. // 分配到 worker 进程。这个其实不用写,默认就是分配到 worker 进程。
  10. // Pandora.js 内置一个 pandora.process('worker').scale('auto') 的定义。
  11. .process('worker');
  12. };

httpServer.js

  1. const http = require('http');
  2. module.exports = class HTTPServer {
  3. // 构造时会传递一个上下文对象
  4. constructor(serviceContext) {
  5. console.log(serviceContext);
  6. this.config = serviceContext.config;
  7. this.logger = serviceContext.logger;
  8. }
  9. // 标准的启动接口
  10. async start () {
  11. this.server = http.createServer((req, res) => {
  12. // 标准的日志对象,会记录在 ${logsDir}/${appName}/service.log
  13. this.logger.info('Got a request url: ' + req.url);
  14. res.writeHead(200, {'Content-Type': 'text/plain'});
  15. res.end('Hello Pandora.js');
  16. });
  17. // 标准的启动接口,异步等待 Server 监听成功才算启动成功
  18. await new Promise((resolve, reject) => {
  19. this.server.listen(this.config.port, (err) => {
  20. if(err) {
  21. return reject(err);
  22. }
  23. resolve();
  24. });
  25. });
  26. console.log('Listens on http://127.0.0.1:' + this.config.port);
  27. }
  28. // 标准的停止接口,停止时会有 5 秒的时间窗口用于处理善后工作
  29. async stop () {
  30. await new Promise((resolve) => {
  31. this.server.close(resolve);
  32. });
  33. console.log('Service stopped');
  34. }
  35. };

那我们就可以使用如下命令启动:

  1. $ pandora dev
  2. Listens on http://127.0.0.1:1338
  3. 2017-12-11 14:34:22,340 INFO 21481 Process [name = worker, pid = 21483] Started successfully!
  4. ** Application start successful. **

我们看到 Listens on http://127.0.0.1:1338,服务已经成功启动。

然后我们访问下 http://127.0.0.1:1338,然后查看 ${logsDir}/${appName}/service.log 可以看到日志也已经写入了。

然后 Ctrl + c 停止应用,我们可以看到同样有 Service stopped 的输出。

Pandora dev 与 Pandora start

  1. Pandora dev 多用于本地调试,不启动 Daemon,并前台启动应用。
  2. Pandora start 多用于生产环境启动,使用 Daemon 守护应用,后台启动应用。

procfile.js 中可以使用如下判断启动来源:

  1. if (pandora.dev) {
  2. // pandora dev 启动的
  3. } else {
  4. // pandora start 启动的
  5. }

内置的默认定义

procfile.js 为了方便使用有一些默认行为:

  1. // 定义默认 Service 的分配进程为 worker 进程
  2. pandora.defaultServiceCategory('worker');
  3. // 定义上面说到的 worker 进程
  4. pandora.process('worker')
  5. // 进程进程的横向扩展,如果 dev 模式只启动 1 个,生产环境启动时启动 CPU 数量个
  6. .scale(pandora.dev ? 1 : 'auto')
  7. // 定义一个环境变量
  8. .env({worker: 'true'});
  9. // 定义一个后台任务进程
  10. pandora.process('background')
  11. // 强制只启动 1 个
  12. .scale(1)
  13. .env({background: 'true'});
  14. // 定义基础的 Logger Service
  15. pandora.service('logger', LoggerService)
  16. /**
  17. * 进程不是定义了就会启动,比如有向其分配 Service 后才会启动
  18. * 比如 .process('worker') 表示向 worker 进程分配
  19. * 比如 .process('all') 表示向所有进程分配
  20. * 比如 .process('weak-all') 表示向所有激活进程(有其他 Service 在这个进程的)分配
  21. */
  22. .process('weak-all');