认识 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

// procfile.js 是一个普通的 Node.js 模块,必须导出一个 function
// function(pandora) 的第一个参数是 pandora,这个对象用于定义我们的进程结构
module.exports = function(pandora) {

pandora

// 定义一个进程,名字叫 processA
.process('processA')

// 如果 scale 大于 1 ,将使用 Node.js 的 Cluster 模块自动产生进程组
// 默认值即是 1
.scale(1)

// 定义进程环境变量,创建出来的进程中可以通过 process.env 获得
.env({
ENV_VAR1: 'VALUE_OF_ENV_VAR1'
})

// 这个进程的入口文件地址
.entry('./app.js');

}

app.js

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

然后我们运行命令:

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

# 基于 Scale 自动缩放进程

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

$ pandora dev # 通过 pandora dev 前台启动应用,多用于本地调试
Got the value of ENV_VAR1: VALUE_OF_ENV_VAR1 from Process [pid = 19913]
Got the value of ENV_VAR1: VALUE_OF_ENV_VAR1 from Process [pid = 19915]
Got the value of ENV_VAR1: VALUE_OF_ENV_VAR1 from Process [pid = 19916]
Got the value of ENV_VAR1: VALUE_OF_ENV_VAR1 from Process [pid = 19914]
2017-12-11 14:04:57,836 INFO 19910 Process [name = processA, pid = 19912] Started successfully!
** 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

module.exports = function(pandora) {

// 直接启动 ./app.js,等价于直接用 child_process.spawn 启动。
pandora.fork('aForkedProcess', './app.js');

}

上面的例子等价于下面:

procfile.js

module.exports = function(pandora) {

pandora

// 定义一个进程 aForkedApp
.process('aForkedApp')

// 指定只启动一个
.scale(1)

// 指定入口文件地址
.entry('./app.js');

}

Cluster

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

procfile.js

module.exports = function(pandora) {

// 用 Cluster 模块启动 app.js
// 默认分配到 worker 进程
// Pandora.js 内置了一个 process('worker').scale('auto') 的定义( 'auto' 表示 CPU 数量 )
pandora.cluster('./app.js');

}

上面的例子等价于下面:

procfile.js

module.exports = function(pandora) {

// 定义 worker 进程,其实这个定义 Pandora.js 早已内置
pandora
.process('worker')
.scale('auto');

// 定义一个 Service,并分配到 worker 进程
// 只做一件事情,引入 './app.js'
pandora
.service('scalableService', class ScalableService {
start() {
require('./app.js');
}
})
.process('worker');

}

# 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

module.exports = function (pandora) {

pandora

// 定义一个 Service ,名字叫 httpServer,实现在 ./httpServer
.service('httpServer', './httpServer')

// 对其配置
.config({
port: 1338
})

// 分配到 worker 进程。这个其实不用写,默认就是分配到 worker 进程。
// Pandora.js 内置一个 pandora.process('worker').scale('auto') 的定义。
.process('worker');

};

httpServer.js


const http = require('http');

module.exports = class HTTPServer {

// 构造时会传递一个上下文对象
constructor(serviceContext) {
console.log(serviceContext);
this.config = serviceContext.config;
this.logger = serviceContext.logger;
}

// 标准的启动接口
async start () {

this.server = http.createServer((req, res) => {

// 标准的日志对象,会记录在 ${logsDir}/${appName}/service.log
this.logger.info('Got a request url: ' + req.url);

res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello Pandora.js');

});

// 标准的启动接口,异步等待 Server 监听成功才算启动成功
await new Promise((resolve, reject) => {
this.server.listen(this.config.port, (err) => {
if(err) {
return reject(err);
}
resolve();
});
});

console.log('Listens on http://127.0.0.1:' + this.config.port);

}
// 标准的停止接口,停止时会有 5 秒的时间窗口用于处理善后工作
async stop () {
await new Promise((resolve) => {
this.server.close(resolve);
});
console.log('Service stopped');
}
};

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

$ pandora dev
Listens on http://127.0.0.1:1338
2017-12-11 14:34:22,340 INFO 21481 Process [name = worker, pid = 21483] Started successfully!
** 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 中可以使用如下判断启动来源:

if (pandora.dev) {
// pandora dev 启动的
} else {
// pandora start 启动的
}

# 内置的默认定义

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


// 定义默认 Service 的分配进程为 worker 进程
pandora.defaultServiceCategory('worker');

// 定义上面说到的 worker 进程
pandora.process('worker')
// 进程进程的横向扩展,如果 dev 模式只启动 1 个,生产环境启动时启动 CPU 数量个
.scale(pandora.dev ? 1 : 'auto')
// 定义一个环境变量
.env({worker: 'true'});

// 定义一个后台任务进程
pandora.process('background')
// 强制只启动 1 个
.scale(1)
.env({background: 'true'});

// 定义基础的 Logger Service
pandora.service('logger', LoggerService)
/**
* 进程不是定义了就会启动,比如有向其分配 Service 后才会启动
* 比如 .process('worker') 表示向 worker 进程分配
* 比如 .process('all') 表示向所有进程分配
* 比如 .process('weak-all') 表示向所有激活进程(有其他 Service 在这个进程的)分配
*/
.process('weak-all');