让进程间通信更容易 - Pandora.js 的 IPC-Hub

让进程间通信更容易 - Pandora.js 的 IPC-Hub

我们前面的两篇介绍了怎么定义多个进程,怎么利用进程这一资源。我们在淘宝业务中也是这样实践的,有一个问题也越发明显,进程间如何通信呢?

起初我们有一个比较简单的 IPC 实现,通过 Domain Socket 进行通信。也是传统的 C/S 架构的,两个进程间进行比较基础的消息通信(比较类似 Node-IPC 这个包)。

不过实在是太基础了,时不时地在想:

在同一台计算机上的同一个语言,为什么要搞得这么痛苦?是 Node.js 太弱了吗?
我要有线程的话,需要活的这么累吗?

之前的大致样子:

1
2
3
4
5
// 在进程 A
const ipcServer = createServer('/path/of/domain/socket/file');
ipcServer.on('request_resource', (client) => {
client.send('dispatch_resource', { ...resource });
});
1
2
3
4
5
6
// 在进程 B
const ipcClient = createClient('/path/of/domain/socket/file');
ipcClient.on('dispatch_resource', (resource) => {
console.log('Got resource', resource);
});
ipcClient.send('request_resource');

都是一些简单消息传递,只有 send()boardcast() 这样简单的元语。想做点什么,难度可想而知。

我们在 Pandora.js 中系统化地解决了这个痛点,就是接下来要介绍的 IPC-Hub。

进程间通信 IPC-Hub

经过一些思考和讨论,最简洁的莫过是:

我发布一个对象,这台计算机上的哪个 Node.js 进程都能调用。
这个对象上的方法我也不用额外地修饰,发布出去其他进程就能调用,就像调用一个普通对象上的方法一样。

你只需要用 Pandora.js 启动应用,下面这些能力都是标配的。

简洁的进程间对象代理

经过努力,我们提出了『简洁的进程间对象代理』,看下面的例子会清楚得多:

在 PID 为 1 的进程中:

1
2
3
4
5
6
7
8
const {publishObject} = require('pandora');
const processInfo = {
async getPid() {
return process.pid; // 假定 PID 是 1
}
}
// 发布 processInfo 在 IPCHub 中,并命名为 processInfo
await publishObject('processInfo', processInfo);

在 PID 为 2 的进程中:

1
2
3
4
5
6
7
8
9
10
11
12
13
const {getProxy} = require('pandora');

// 获得 processInfo 的代理对象
const processInfo = await getProxy('processInfo');

// 调用 getPid() 方法,当然这里需要增加 await,
// 每个代理过来的方法都会变成 async 形式。
// 不过好在 await 一个非 async 的方法也不会出错,
// 所以一般业务逻辑都可以良好的兼容原始对象和代理对象。
const pid = await processInfo.getPid();

// 将会打印 PID 为 1 的进程的 PID: 1
console.log(pid);

强大到,就连 Node.js 内置的对象都可以!

—— 作者语:虽然没什么实际用处。

1
2
3
// 进程 A
const {publishObject} = require('pandora');
publishObject('math', Math);
1
2
3
4
5
6
// 进程 B
const {getProxy} = require('pandora');
const mathProxy = getProxy('math');

// 将会打印 4
console.log(await mathProxy.pow(2, 2));

Service 发布到 IPC-Hub

上一篇介绍到了 Service,正是可以利用的绝佳材料,可以很方便的发布到 IPC-Hub 中:

procfile.js

1
2
3
4
5
6
7
8
module.exports = function(pandora) {
pandora
.service('serviceName', './serviceImpl.js')

// 直接把这个 Service 发布到 IPC-Hub 中去
// ,当然名字就叫 serviceName 啦~
.publish();
}

一个向任务进程投递网页截图任务的例子

为什么我们要把一些任务隔离进程?因为有些任务计算量大(比如大规模的定时任务)、或者不稳定因素太多(比如集成了不可靠的库),希望隔离于提供基础 Web 服务的进程组。

我们下面的例子中将介绍:

Web 服务收到用户的请求后,直接将任务通过 IPC-Hub 传递给任务进程,任务进程完成后再返回用户。

我们其中用到了 GoogleChrome/puppeteer,一个 Chrome Headless 的 Node.js Lib。

实现细节

下面讲到的例子在:https://github.com/midwayjs/pandora-example/tree/master/pageSnapshot 。

我们先将 procfile.js 写好:

下面最重要的是 service('pageSnapshot', './services/PageSnapshot').publish(),表示将 pageSnapshot 这个 Service 发布到 IPC-Hub 中去,任何进程都可以调用了~

procfile.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
module.exports = function(pandora) {
/**
重要前提:
Pandora.js 中已经默认包含 worker 和 background 的进程定义
,只要往里面注册 Service 就能激活。
pandora.process('worker').scale(pandora.dev ? 1: 'auto');
pandora.process('background').scale(1);
*/


// 调整后台任务进程(background)的内存限制
pandora
.process('background')
.argv(['--max-old-space-size=512']);

// 将截图服务放到 background 进程
pandora
.service('pageSnapshot', './services/PageSnapshot')
.process('background')

// *** 重要:表示发布到 IPC-Hub 中
.publish();

// 将 Web 服务放到 worker 进程
pandora
.service('web', './services/Web')
.process('worker')
.config({
// 配置监听端口号
port: 5511
});
};

然后实现截图的 PageSnapshot ./services/PageSnapshot

services/PageSnapshot.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
const puppeteer = require('puppeteer');

module.exports = class PageSnapshot {
/**
* @param ctx - 构造时会传递一个上下文对象,这个具体可以参考:
* http://www.midwayjs.org/pandora/api-reference/pandora/classes/servicecontextaccessor.html
*/

constructor(ctx) {

// 标准的日志对象
this.logger = ctx.logger;

}

/**
* 标准的启动接口:
* http://www.midwayjs.org/pandora/api-reference/pandora/interfaces/service.html#start
*/

async start() {
// 启动一个 Headless Chrome
this.browser = await puppeteer.launch();

// 启动完毕
this.logger.info('Service PageSnapshot Started');
}

/**
* 标准的停止接口:
* http://www.midwayjs.org/pandora/api-reference/pandora/interfaces/service.html#stop
*/

async stop() {
// 关闭 Headless Chrome
await this.browser.close();

// 启动完毕
this.logger.info('Service PageSnapshot Stopped');
}

async take(url) {
// 新建一个页面
const page = await this.browser.newPage();

// 跳转到目标地址
await page.goto(url);

// 截图
const buf = await page.screenshot({type: 'jpeg', quality: 60});

// 关闭页面
await page.close();

// 返回
return {
// 现在 IPC Hub 不能直接传递 Buffer,需要 base64。
base64: buf.toString('base64')
}
}
};

然后实现前台 Web 服务 ./services/Web

下面的服务继承了一个基础类 SimpleHTTP,这里就不再引出了,基本上就是封装了一个 HTTP Server 上线下线逻辑。

下面的重点是获得 pageSnapshot 对象代理那里。

services/Web.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
const querystring = require('querystring');
const url = require('url');
const SimpleHTTP = require('./SimpleHTTP');

module.exports = class Web extends SimpleHTTP {
/**
* @param ctx - 构造时会传递一个上下文对象,这个具体可以参考:
* http://www.midwayjs.org/pandora/api-reference/pandora/classes/servicecontextaccessor.html
*/

constructor(ctx) {
super(ctx);
this.ctx = ctx;
}

/**
* 实现父类要求的接口,处理 HTTP 请求
*/

async onRequest(req, res) {
// 标准的日志对象,父类中通过 this.logger = ctx.logger 获得
this.logger.info('Got a request url: ' + req.url);

// 从 query 中获得 method 和 params
const query = querystring.parse(url.parse(req.url).query);
const targetUrl = query.url;
if(!targetUrl) {
throw new Error('Query [url] is required');
}

// *** 重点:获得 pageSnapshot 对象代理
// getProxy,在 serviceContext 中同样有暴露
const pageSnapshot = await this.ctx.getProxy('pageSnapshot', {
// 默认 5 秒超时,截取网页,还是需要加大点超时时间
timeout: 10 * 1000
});

// 调用截图。
// 现在 IPC Hub 不能直接传递 Buffer,需要 base64。
const snapshot = await pageSnapshot.take(targetUrl);
const jpg = new Buffer(snapshot.base64, 'base64');

// 返回给客户端
res.writeHead(200, {'Content-Type': 'image/jpeg'});
res.end(jpg);
}

/**
* 实现父类要求的接口,提供 TCP 端口
*/

getPort() {
// 通过配置获取
return this.ctx.config.port;
}
};

启动一个试试看

可以直接 Clone 例子试试看。

1
2
3
4
5
6
$ pandora dev # 本地前台启动项目
2017-12-20 21:49:51,318 INFO 94498 [serviceName: web, processName: worker] Service Web Listens On http://127.0.0.1:5511
2017-12-20 21:49:51,320 INFO 94496 Process [name = worker, pid = 94498] Started successfully!
2017-12-20 21:49:51,877 INFO 94499 [serviceName: pageSnapshot, processName: background] Service PageSnapshot Started
2017-12-20 21:49:51,879 INFO 94496 Process [name = background, pid = 94499] Started successfully!
** Application start successful. **

可以看到 background 和 worker 进程都已经启动了。

浏览器访问看看

访问 http://127.0.0.1:5511/?url=https://www.taobao.com/ 看看。

taobao | center

嗯,已经可以用了。

未完待续

这样 Pandora.js 的进程间通信能力就介绍完了,和进程模型相关的能力基本介绍完毕了。

下周开始介绍业务度量能力啦~ 比如 Metrics、全链路 Trace 等,大家敬请期待。

最后,不要忘了给点个 Star 喔~

https://github.com/midwayjs/pandora/

最后的最后,我们招人。我们有超过一半的淘宝前台访问在 Node.js 上,也有做开源 Node.js 软件的机会,挑战不小,当然回报也不小。

题图:https://unsplash.com/photos/kRLXoi3Dtqs By @Ian Simmonds

文章来源:

Author:Taobao FED
link:http://taobaofed.org/blog/2017/12/21/pandora-ipc/