跨越时代的度量衡 - Pandora.js 的 Metrics 介绍

跨越时代的度量衡 - Pandora.js 的 Metrics 介绍

自秦始皇统一六国,天下归一,推行“一法度衡石丈尺,车同轨 ,书同文”,颁发统一度量衡诏书,制定了一套严格的管理制度,天底下的度量衡就变成了一套。而如今程序世界也是天下分崩离析,不同编程语言各占一隅,不过即使语法不同,但是分分合合,思路终归一致,想要度量代码的心情依旧是一样的。

度量的作用

很多同学表示怀疑,为什么要度量?

其实回答很简单,度量就像是身体健康的检查器,就像体检给的报告,没有这份报告,你是不是很担心自己的身体有没有出问题,要是好几年没体检,恐怕焦虑症都要犯了。

平常我们所说的监控报警,其实就是度量的一种具体应用和泛化,其实还有更大更广泛作用,比如:

当你上线了一个应用,你怎么知道应用是健康的? 当你发布了一个新功能,你怎么知道这个功能对线上有哪些影响,怎么去评估呢? 当你升级了一个包版本,你怎么知道他是稳定的呢?

一切的一切,尽在度量上。

Pandora.js 的度量体系参考了 spring-boot 的命名习惯,以及结合了阿里内部的 alimetrics 体系,加上业界的 opentracing 链路追踪模型,形成了一整套可检查,可度量,可追踪的完整方案。

我们今天来细说 Pandora.js 其中的一种度量机制,这不仅是 Node.js 的度量,也是程序通用的度量 - Metrics。

Metric 的命名

Metric 的目标是可自我描述,所以在命名上会尽可能的根据业务场景来命名。

在参考了业界的metrics规范,以及结合了阿里集团内使用场景之后,我们使用了基础的 MetricName 对象格式做为 Metric 的名字。

Metrics 是复数形式,目前这些度量在业界口头表述都可能叫 Metrics ,而实际在代码中对特定的单个指标叫 Metric,而普通的非特定单数形式继续沿用 Metrics 的表述 。

MetricName 大体分为两部分,key 和 tags。

key 代表着一个具体的项,比如有一个 metrics 指标是某个 HTTP 接口,那么这个指标的 key 就可能是 application.http.request.path,通过 . 将命令进一步的缩小,每个公司可以有自己不同的规范,根据部门、产品、功能来进行划分,尽量做到 key 可描述,可扩展,所有的单词用下划线'_'连接,字母采用小写形式。

tags 代表着一个指标的不同分类,它和 key 加起来唯一指定了一个 Metric。tags 是一个对象 {},通过不同的 kv 对来描述详情,比如区分不同请求的来源,继续以 HTTP 接口来举例,{"source":"shanghai"}{"source": "hangzhou"} 这样就是不同的 tags,结合 key,就用来表示不同的 Metric 了。

这样的好处就是 tags 可以无限扩展,不会影响到 key,同时,在后续的存储中,同一个 key 可以进行查询筛选,保证数据一致性和连贯性。

具体的实例我们将在之后的实例中介绍。

对外的数据格式

除了定义 Metric 的名字之外,我们还需要考虑对外输出的格式,既然是 Node 体系,我们首先考虑的自然是 JSON 结构的格式,便于阅读以及数据格式化存储。

基于 MetricName,我们将名字和值定义了成了通用的 MetricObject 格式。

一个标准的输出格式大概如下:

1
2
3
4
5
6
7
8
9
10
11
{
"metric": "sys.cpu.nice",
"timestamp": 1346846400,
"value": 18,
"type": "COUNTER",
"level": "CRITICAL",
"tags": {
"host": "web01",
"dc": "lga"
}
}

和 MetricName 类似,包含一些常用信息,包括 key,value,时间戳,tags 等。

最终这个数据格式会被内置的 Reporter 体系输出到不同的对外接口中,包括文本文件,HTTP 接口等等,这样外部系统根据这样的内容进行存储,计算,分发,来进行最后的监控,可视化工作,这个不在我们这个体系内,暂时不做过多的介绍了。

Metrics 的类型

Metric 除了有名字,还有类型,目前最常用的就是瞬时值和计数器,此外还有其他的一些。

在了解业界实践并结合集团内部的实践基础上,我们抽象出以下几种度量场景:

累加型度量:对指标的数据进行累加,反映的是数据随着时间单调递增的关系,应用接受到的 HTTP 请求的总次数 瞬态型度量:表示指标在当前时间点的瞬时情况,反映的是数据随着时间上下波动的关系,如系统的load,内存使用率,堆信息等 变化速率度量:表示指标在某个时间段内变化的速率,反映的是数据随时间的增长快慢关系,如某个接口的 QPS 数据分布度量:表示某一些指标在某个时间段内的分布情况,反映的是数据随时间的统计学分布关系,如某段时间内,某个接口的 RT 的最大,最小,平均值,方差,95% 分位数等

基于这些场景,结合业界的 Metrics 实现,我们目前提供四种最基础的指标,即:

Gauge 瞬态的度量指标 Counter 累加计数型指标 Histogram 分布度量指标 Meter 速率度量指标

目前最常用的是 Gauge 瞬态值以及 Counter 累加值,80% 的场景都可以覆盖

这样在不同的场景下,我们都可以找到相应的 Metric 类型了。在某些场景下,我们还做了额外的一些指标类型(在阿里内部还有两种聚合的类型,等机会开源)

Metrics 数据生产

目前 Pandora.js 全部使用 TypeScript 来编写,有些代码必须带类型定义。

所有的 Metric 类型都继承与 Metric 接口

瞬态型度量

大部分的度量都从瞬态值 Gauge 介绍起,因为它最简单,最直观的表示数据的真实情况,也不涉及时间间隔的问题。

Gauge 只包含一个 getValue 方法,只需要实现这个方法即可,比如,你想要知道当前进程的 CPU 使用情况,就可以一句话解决。

1
2
3
4
5
6
<BaseGauge> {
getValue() {
const startUsage = process.cpuUsage();
return startUsage.user;
}
}

注意,所有的 Metrics 最终输出的一定是数字形式,这样才可度量,如果你希望输出的是字符串类的信息,我们有另一套输出体系,这将在之后的文章介绍。

累加型度量

Counter 是第二个介绍的类型,计数器和 Gauge 不太一样,它是累加型,适用于记录调用总量等类型的数据,比如某个接口的调用次数。

如下图是计数器的继承接口和实现类。

除了基础的 BaseCounter 实现之外,我们提供了 BucketCounter 分桶计数器。

分桶计数的原理是定义一个时间间隔,将一段时间按照时间间隔分割为几个桶,每个桶保存当前时间间隔的计数。

比如时间间隔为 5s ,桶的总数为 10 个,那么 0~5s 为一个桶,5~10s 为下一个,以此类推。当计数的执行的时间为 2s 时,那么将在第一桶中累加,如果为 7s 时,那么将在第二个桶累加,非常容易理解。

在实际场景中,因为内存限制,不宜保存过多,桶的量会有限制,采用环形队列存储同时避免数据的挪动。

举个常用例子,记录 Koa 服务的请求数。

1
2
3
4
5
6
7
8
// 实际使用需要从 MetricsClient 拿到 BucketCounter
let counter = new BucketCounter();

app.use(async (ctx, next) => {
// 累加 1 counter.inc(1);
counter.inc();
await next()
});

分布度量

第三个介绍的是 Histogram,直方分布指标,Pandora.js 包含一个基础实现类 BaseHistogram, 通过它可以用于统计某个接口的响应时间,可以展示 50%, 70%, 90% 的请求响应时间落在哪个区间内,通过这些你可以计算出 Apdex。

这边的分布暂时只考虑单机分布,在集群维度上不能这样计算。

对于分布计算,核心就是维护一个数据集 Reservoir ,数据集用来提供数据存储以及获取当前快照的能力。这其中最重要的就是数据更新的策略,目前 Pandora.js 只实现了随机采样(UniformReservoir)和 指数衰减随机采样(ExponentiallyDecayingReservoir)的实现,由于随机采样并不能很好的表现权重问题,默认的是指数衰减随机采样,其他的采样算法没有实现,有兴趣的同学可以补充。

举个常用例子,记录 Koa 服务的成功比率,采用随机采样算法,间隔 1s,2个分桶,展示获取了平均数等信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 实际使用需要从 MetricsClient 拿到 BaseHistogram
let histogram = new BaseHistogram(ReservoirType.UNIFORM, 1, 2);

app.use(async (ctx, next) => {
histogram.update(10);
histogram.update(20);

// other biz
});

// let snapshot = histogram.getSnapshot();
// expect(snapshot.getMean()).to.equal(15);
// expect(snapshot.getMax()).to.equal(20);
// expect(snapshot.getMin()).to.equal(10);
// expect(snapshot.getMedian()).to.equal(15);

变化速率度量

第四个介绍的是 Meter,是一种用于度量一段时间内吞吐率的计量器。例如,一分钟内,五分钟内,十五分钟内的qps指标。

这里要指出,变化的速率,我们一般情况下会关心两个地方,一个是瞬时爆发,超出平常正常值非常高的这样的波动变化,另一个是一段时间内的趋势,从平均的角度来看整体度量的一种方式,这种方式会将高低点进行平均来看。

前一种在 Metrics 中使用 Rate 的概念,只记录事件的累计总次数,有外部系统来通过前后两次采集,来计算瞬时速率,这里我们称之为Rate

在 rate 的计算中,我们认为数据的增长是线性的。其计算方式为:rate = (v2 - v1) / (t2 - t1),其中时间的单位是 s。

这样的好处是,通过调整采集频率,可以支持任意时间间隔的瞬时速率计算。但缺点是,当两次采样之间系统重启的时候,会计算出负数,同时会有一部分数据丢失。

后一种通过指数移动加权平均(Exponential Weighted Moving Average, EWMA)来计算。

针对速率型度量指标,我们提供了 1 分钟(m1),5 分钟(m5),15分钟的EWMA(m15),分别用于反映距离当前时间点 1 分钟,5 分钟,15 分钟的速率变化。

其具体的计算方法,和 Linux 系统中 load1, load5, load15 的计算方法完全一致。即,每 5 秒钟统计一次瞬时速率,并应用于如下的递推公式:

1
EWMA(t) = EWMA(t-1) + alpha * (instantRate - EWMA(t-1))

其中 alpha 取值范围为 0~1, 称为衰减系数,该系数越大,则距离当前的时间点越老的数据权重衰减的越快。

举个常用例子,记录 Koa 某个路由的调用比率。

1
2
3
4
5
6
7
8
9
10
// 实际使用需要从 MetricsClient 拿到 BaseMeter
let meter = new BaseMeter();

router.get('/home', async (ctx) => {
// 接口调用埋点
meter.mark(1);
});

// meter.getMeanRate(); 总数除以时间
// meter.getOneMinuteRate(); // 一分钟的 EWMA

本文最后

以上只是 Pandora.js 的度量体系的一部分,结合了阿里自己的 Metrics 体系,只能管中窥豹,简单的介绍一下几种最基本的度量指标类型,通过这本的度量器,我们可以将数据从业务代码中产生出来。

不过这仅仅是数据生成,除此之外,数据采集和加工也非常的重要,下一篇我们将会讲到,Pandora.js 的数据采集和加工部分。

Pandora.js 项目地址:https://github.com/midwayjs/pandora 欢迎社会各界前来 Star ~

文章来源:

Author:Taobao FED
link:http://taobaofed.org/blog/2018/01/05/pandora-metrics/