dubbogo中metrics的设计

最近因为要在dubbo-go里面实现类似的这个metrics功能,于是花了很多时间去了解现在dubbo里面的metrics是怎么实现的。该部分,实际上是被放在一个独立的项目里面,即metrics

总体上来说,dubbometrics是一个从设计到实现都非常优秀的模块,理论上来说,大部分的java项目是可以直接使用metrics的。但也因为兼顾性能、扩展性等各种非功能特性,所以初看代码会有种无从下手的感觉。

今天这篇文章将会从比较大的概念和抽象上讨论一下dubbogo中的metrics模块的设计——实际上也就是dubbo中的metrics的设计。因为我仅仅是将dubbo里面的相关内容在dubbogo中复制一份。

目前dubbogometrics刚刚开始起步,第一个PR是https://github.com/apache/dubbo-go/pull/278

总体设计

Metric

要想理解metrics的设计,首先要理解,我们需要收集一些什么数据。我们可以轻易列举出来在RPC领域里面我们所关心的各种指标,诸如每个服务的调用次数,响应时间;如果更加细致一点,还有各种响应时间的分布,平均响应时间,999线……

但是上面列举的是从数据的内容上划分的。metrics在抽象上,则是摒弃了这种划分方式,而是结合了数据的特性和表现形式综合划分的。

从源码里面很容易找到这种划分的抽象。

metrics设计了Metric接口作为所有数据的顶级抽象:

6dae190da9527bf9000e1d2c82b0020d

dubbo里面,其比较关键的子接口是:

9d44e49f63a43f00a08b9b5b001fa18e

为了大家理解,这里我抄一下这些接口的用途:

  • Gauge: 一种实时数据的度量,反映的是瞬态的数据,不具有累加性,例如当前JVM的线程数;
  • Counter: 计数器型指标,适用于记录调用总量等类型的数据;
  • Histogram: 直方分布指标,例如,可以用于统计某个接口的响应时间,可以展示50%, 70%, 90%的请求响应时间落在哪个区间内;
  • Meter: 一种用于度量一段时间内吞吐率的计量器。例如,一分钟内,五分钟内,十五分钟内的qps指标;
  • Timer: Timer相当于Meter+Histogram的组合,同时统计一段代码,一个方法的qps,以及执行时间的分布情况;

目前dubbogo只实现了FastCompass,它也是Metric的子类:

9efb3dcbcd85fc297c64498d1441fcf2

这个接口功能很简单,就是用于收集一段时间之内的subCategory执行的次数和响应时间。subCategory是一个比较宽泛的概念,无论是在dubbo还是在dubbogo里面,一个典型的subCategory就会是某个服务。

这里的设计要点在于,它是从什么角度上去做这些数据的抽象的。

很多人在开发这种采集数据的相关系统或者功能的时候,最容易陷入的就是从数据内容上做抽象,例如抽象一个接口,里面的方法就是获得服务的调用次数或者平均响应时间等。

这种抽象并非不可以,尤其是在简单系统里面,还非常好用。唯独在通用性和扩展性上要差很多。

MetricManager

在我们定义了Metric之后,很容易就想到,我要有一个东西来管理这些Metric。这就是MetricManager——对应到dubbo里面的IMetricManager接口。

MetricManager接口目前在dubbogo里面还很简单:

0a7c0bb19836c6ee485a53a1374a4a63

本质上来说,我在前面提到的那些Metric的子类,都可以从这个MetricManager里面拿到。它是对外的唯一入口。

因此无论是上报采集的数据,还是某些功能要用这些采集的数据,最重要的就是获得一个MetricManager的实例。例如我们最近正在开发的接入Prometheus就是拿到这个MetriManger实例,而后从里面拿到FastCompass的实例,而后采集这些数据:

6020a2720fe234939e9e33822e363b22

MetricRegistry

MetricRegistry是一个对Metric集合的抽象。MetricManager的默认实现里面,就是使用MetricRegistry来管理Metric的:

d161a02a2139d2cdc8320ee2f0f1bd20

所以,本质上它就是提供了一些注册Metric然后再从里面捞出来的方法。

于是,这就有一个问题了:为什么我在有了MetricManager之后,还有有一个MetricRegistry?似乎这两个功能有些重叠?

答案大概是两个方面:

  1. MetricManager除了管理所有的Metric之外,还承担着额外的功能,这些功能典型的就是IsEnabled。而实际上,在未来我们会赋予它管理生命周期的责任,比如说在dubbo里面,该接口就还有一个clear方法;
  2. metrics里面还有一个group的概念,而这只能由MetricManager来进行管理,至少交给MetricRegistry是不合适的。

metricsgroup说起来也很简单。比如在dubbo框架里面采集的数据,都会归属于dubbo这个group。也就是说,如果我想将非框架层面采集的数据——比如纯粹的业务数据——分隔出来,就可以借用一个business group。又或者我采集到的机器自身的数据,可以将其归类到system这个group下。

所以MetricMangerMetricRegistry的关系是:

31c5315418d23cb5ccdbf8221d3adf73

Clock

Clock抽象是一个初看没什么用,再看又会觉得太机智了的抽象。Clock里面就两个方法:

c82039a24971f891eb8f7fd0e9902de2

一个是获得时间戳,另外一个则是获得时间周期(Tick)。比如通常采集数据可能是每一分钟采集一次,所以你得知道现在处在哪个时间周期里面。Clock就提供了这种抽象。

很多人在实现自己的这种metrics的框架的时候,大多数都是直接使用系统的时钟,也就是系统的时间戳。于是所有的Metic在采集数据或者上报数据的时候,不得不自己去处理这种时钟方面的问题。

这样不同的Metric之间就很难做到时钟的同步。比如说可能在某个Metric1里面,采集周期是当前这一分钟,而Metric2是当前这一分钟的第三十秒到下一分钟的第三十秒。虽然它们都是一分钟采集一次,但是这个周期就对不上了。

另外一个有意思的地方在于,Clock提供的这种抽象,允许我们不必真的按照现实时间的时间戳来处理。比如说,可以考虑按照CPU的运行时间来设计Clock的实现。

例子

就用这一次PR的内容来展示一下这个设计。

dubbogo里面这次实现了metricsFilter,它主要就是收集调用次数和响应时间,其核心是:

1e9a3a60dbddaeb20f8c7da7278c9703

report其实就是把metrics reportsMetricManager

34ac7e839dca05cf554343928896fdfe

所以,这里面可以看出来,如果我们要收集什么数据,也是要先获得MetricManager的实例。

FastCompass的实现里面会将这一次调用的服务及其响应时间保存下来。而后在需要的时候再取出来。

所谓的需要的时候,通常就是上报给监控系统的时候。比如前面的提到的上报给Prometheus

所以这个流程可以抽象表达为:

d80c427a270fd3c6ae3e19042c5251b6

这是一个更加宽泛的抽象。也就是意味着,我们除了可以从这个metricFilter里面收集数据,也可以从自身的业务里面去收集数据。比如说统计某段代码的执行时间,一样可以使用FastCompass

而除了Prometheus,如果用户自己的公司里面有监控框架,那么他们可以自己实现自己的上报逻辑。而上报的数据则只需要拿到MetricManager实例就能拿到。

总结

本质上来说,整个metrics可以看做是一个巨大无比的provider-conumer模型。

不同的数据会在不同的地方和不同时间点上被采集。有些人在读这些源码的时候会有点困惑,就是这些数据什么时间点会被采集呢?

它们只会在两类时间点采集:

  1. 实时采集。如我上面举例的metricsFilter,一次调用过来,它的数据就被采集了;
  2. 另外一个则是如同Prometheus。每次Prometheus触发了collect方法,那么它就会把每种(如Meter, Gauge)里面的数据收集过来,然后上报,可以称为是定时采集;

dubbo里面采集了非常多的数据:

f0ade34fc26bf7d561d32010ba69fe30

这些具体的实现,我就不一一讨论了,大家有兴趣可以去看看源码。这些数据,也是我们dubbogo后面要陆续实现的东西,欢迎大家持续关注,或者来贡献代码。