构筑未来数据网 - Pandora.js 的信息通路

构筑未来数据网 - Pandora.js 的信息通路

上一篇我们介绍了 Pandora.js 度量体系中的 Metrics,通过 Metrics 我们能将应用运行中的数据生成出来,但是有时候我们不仅仅需要这些指标数据,更可能需要信息数据。

这里的指标数据表示数字值,为了和信息数据(字符串)做区分。

所谓的信息数据是什么?

在 Pandora.js 中,我们定义为应用的信息聚合,这里分为静态信息和动态信息两种类型,举个例子,应用的应用名,应用启动路径,应用的配置,应用的模块版本这些都是固定的静态信息,而进程列表,接口参数这些就是属于动态信息,当然,这些信息也包括我们上一篇文章提到的 Metrics 信息。

Pandora.js 通过 EndPoint 机制,将这些信息统一暴露出来,通过 Restful 接口,让用户或者外部的调度系统使用。

目前 Pandora.js 内置一些信息接口,包括:

健康检查的信息 进程的信息 Daemon 的信息 Node.js 运行时的信息 当前 Metrics 指标结果的信息 链路监控的信息 最近的错误日志的信息 等等。。。

如何运作

所有的接口目前都有在 文档中 提及,这里就不在多加叙述输出的内容,不然就太多余了,我们希望在这里能看到更多细节和运转方式的内容。

放一张数据结构图,这里可以很清楚的看到结构的分布。

Pandora.js 把信息通路的机制划分为两部分,monitor 端和其他进程端,monitor 端处于业务进程之外,和其他进程通过 IPC 进行通信数据交互,以保持稳定和隔离。

Pandora.js 内置了最基础的 IPC 服务,通过简单的方式就可以进行数据传输,这里不做过多说明,这里就当已经完成了,如果有兴趣,可以参考代码,我们的关注点还是在数据怎么进行交互的部分。

基础数据格式

数据格式是所有数据传输的基础,虽然除了 Metrics 我们并没有限定输出什么格式,但是在一定程度上进行约束有助于可读性和扩展性的提升。

对于大多数 JavaScript 信息结构来说,JSON 是最容易被解析和扩展的格式,所以我们在考虑格式的时候第一就选择了 JSON 格式。

以基础信息为例,很容易理解。

1
2
3
4
5
6
{
"appName": "hello-world",
"baseDir": "/home/admin/hello-world",
"nodeVesion": "v8.3.9",
"nodePath": "/home/admin/hello-world/node_modules/"
}

但是这样仅仅是单一的一个维度,每个进程,每个应用至少需要两个维度,基础的信息以及信息的作用域,恰好在 JavaScript 中对象扩展非常容易,只需要简单加一个 key 就行,在这种情况下,把基础结构变化一下就得出了最简单的结构。

1
2
3
4
5
6
7
8
9
10
{
"app": {
"appName": "hello-world",
"baseDir": "/home/admin/hello-world"
},
"node": {
"nodeVesion": "v8.3.9",
"nodePath": "/home/admin/hello-world/node_modules/"
}
}

这样,我们就至少有 appnode 两个维度了,也方便后期做展示和筛选。

但是单靠规范,其实并不能有效的执行起来,所以基于这些我们制定了 builder 机制,通过传入的 builder 对象来格式化暴露的参数。

1
2
3
4
5
6
7
8
function invoke(args, builder) {
builder.withDetail("app", {})
.withDetail("node", {});

// other code

return builder.getDetails();
}

基本上 invoke 这个函数就是客户端最核心的数据采集方法了。

数据传输

在 Pandora.js 的设计中,我们参考了 spring-boot 的命名习惯,将数据披露的客户端称为 Indicator,每个 Indicator 都会连接到对应的服务端 EndPoint 中,这其中是多对一的关系。

这样形成的树状结构,可以有效的将数据聚合到一起。

那他们是怎么关联的呢?

很简单,我们使用的是 IPC 广播,在初始化的时候,Indicator 会广播自己初始化的信息,接受到的 EndPoint 就会注册到自己名下,将客户端的信息保存起来。

这个时候又会有一个问题,EndPoint 怎么知道哪个 Indicator 是我名下的?

这个时候就引出了 group(分组)的概念。

每个 EndPoint 都有一个 group 的字段,通过这个字段,我们确定唯一的 EndPoint ,以及该 EndPoint 下所属的 Indicator。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class HealthEndPoint {

private group = 'health';

registerIndicator() {
if (this.group !== data.group) {
// 不匹配则忽略
return;
}

// 把配置回写给所有 indicator
reply(this.config);

let indicatorProxy = new IndicatorProxy(client);
// 构建指标
indicatorProxy.buildIndicator(data);
this.indicators.push(indicatorProxy);
}
}

class HealthIndicator {
private group = 'health';
}

很明显,当两边的 group 相等的时候,通路就完成了。

当然,实际情况还要更复杂一些,比如需要考虑多个同名 Indicator 的情况,这个时候我们会定义是否是单例。

1
2
3
4
5
6
7
8
9
10
11
if (data.type === 'singleton') {
// 单例下,每个应用只允许一个实例存在
let indicators = this.indicators.filter((indicator: IndicatorProxy) => {
return indicator.match(data.appName, data.indicatorName);
});

if (indicators.length) {
this.debug('indicator type singleton=' + data.appName, data.indicatorName);
return;
}
}

分组、端聚合、远程调用,这就是 Pandora.js 整个数据披露生态的三个核心关键词。

逆向通路

实际使用过程中,我们发现,单一的靠远端调用获取信息不一定能满足需求,只能完成类似

获取应用信息 获取进程信息 业务状态 端口检查

这些已经存在的或者实时性不高的数据的获取,但是还有一类像链路调用,错误日志等这些数据,因为数据量大,实时性高,如果使用 invoke 这样的调用方式,势必会增加复杂度或者额外的性能开销。

通过大量的实践,我们觉得这类数据有一个共同特点,除了数据量大之外,数据都是横跨多个进程,这个时候就有两种方案来解决。

第一种是将数据缓存在客户端,每次调用 invoke 的时候,从客户端缓存拿数据聚合,这样的好处是原有代码结构比较统一,但是多端缓存可能会影响稳定性和数据的一致性。

第二种是将数据缓存在服务端(EndPoint 端),客户端就不是被动调用,而是主动上报了。

权衡之下,我们考虑的是第二种方案,逆向通路就上场了。

所谓的逆向通路,就是客户端采集数据时,先主动上报给服务端缓存,这样服务端拿数据的时候可以直接从缓存中拿取,就不存在数据不一致,或者后期需要聚合的问题,而且缓存的大小可以统一在服务端控制。

为此,我们实现了一个 DuplexIndicator 以及配套的 DuplexEndPoint,还有加了缓存的 CacheDuplexEndPoint。

逆向通路增加了一个 report() 方法,可以主动进行上报,这样错误日志和其他内部这类场景的信息就可以这样上报啦。

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
class DuplexIndicator extends Indicator {

transferType = 'duplex';

messengerClient;

// ... 省略部分代码

/**
* 发送上行消息
* @param data
*/

report(data: any) {
this.messengerClient.report(this.getClientUplinkKey(), data);
}

}


class DuplexEndPoint extends EndPoint {

/**
* 登记指标
* @param data
* @param reply
* @param client
*/

protected registerIndicator(data, reply, client) {
if(this.group !== data.group) {
// 不匹配则忽略
return;
}

// ... 省略部分代码

// 构建上行通路
indicatorProxy.buildUplink(this.processReporter.bind(this));

}

abstract processReporter(data, reply?);
}

这样,只需要在恰当的时候,执行客户端的 report 方法即可,而服务端则通过覆盖 processReporter 对传输过来的数据进行相应的处理,比如写入缓存,这里可以查看 Error 采集的源码 来理解。

并没有结束

在所有的信息传递中,Metrics 是属于比较特殊的一种,他的数据传输方式虽然是基于 IPC 但是有一些不同,受限于篇幅我们留到下期再介绍。

除此之外,下期我们还将介绍对外的接口设计部分,尽情期待。

题图:https://unsplash.com/photos/RkJF2BMrLJc By @William Bout

文章来源:

Author:Taobao FED
link:http://taobaofed.org/blog/2018/01/12/pandora-information-channel/