对服务进行实时监控,了解服务当前的运行指标和健康状态,是微服务体系中不可或缺的环节。Metrics 做为微服务的重要组件,为服务的监控提供了全面的数据基础。近日,Dubbo Metrics 发布了2.0.1版本,本文将为您探秘 Dubbo Metrics 的起源,及 7 大改进。java
Dubbo Metrics(原Alibaba Metrics)是阿里巴巴集团内部普遍使用的度量埋点基础类库,有 Java 和 Node.js 两个版本,目前开源的是 Java 版本。内部版本诞生于2016年,经历过近三年的发展和双十一的考验,已经成为阿里巴巴集团内部微服务监控度量的事实标准,覆盖了从系统、JVM、中间件到应用各层的度量,而且在命名规则、数据格式、埋点方式和计算规则等方面,造成了一套统一的规范。git
Dubbo Metrics 的代码是基于 Dropwizard Metrics 衍生而来,版本号是3.1.0,当时决定 fork 到内部进行定制化开发的主要缘由有两个。github
一是社区的发展很是缓慢,3.1.0以后的第3年才更新了下一个版本,咱们担忧社区没法及时响应业务需求;另外一个更重要的缘由是当时的3.1.0还不支持多维度的 Tag,只能基于 a.b.c 这样传统的指标命名方法,这就意味着 Dropwizard Metrics 只能在单维度进行度量。而后,在实际的业务过程当中,不少维度并不能事先肯定,并且在大规模分布式系统下,数据统计好之后,须要按照各类业务维度进行聚合,例如按机房、分组进行聚合,当时的 Dropwizard 也没法知足,种种缘由使得咱们作了一个决定,内部fork一个分支进行发展。spring
相对于 Dropwizard Metrics ,Dubbo Metrics 作的改进主要有如下几个方面:数据库
1、引入基于 Tag 的命名规范
如前文所描述,多维度 Tag 在动态埋点,数据聚合等方面相对于传统的 metric 命名方法具备自然的优点,这里举一个例子,要统计一个 Dubbo 服务 DemoService 调用次数和 RT,假设这个服务叫作 DemoService,那么按照传统的命名方式,一般会命名为dubbo.provider.DemoService.qps
和dubbo.provider.DemoService.rt
。若是只有1个服务的话,这种方法并没有太大的问题,可是若是一个微服务应用同时提供了多个 Dubbo 的 Service,那么要聚合统计全部Service 的 QPS 和 RT 就比较困难了。因为 metric 数据具备自然的时间序列属性,所以数据很是适合存储到时间序列数据库当中,要统计全部的 Dubbo 服务的 QPS,那么就须要查找全部名称为dubbo.provider.*.qps的指标,而后对他们进行求和。因为涉及到模糊搜索,这对于绝大部分数据库的实现都是比较费时的。若是要统计更加详细的 Dubbo 方法级别的 QPS 和 RT,那么实现上就会更加复杂了。缓存
同时,考虑到一个公司全部微服务产生的全部指标,都会统一汇总到同一个平台进行处理,所以Metric Key 的命名方式为应当遵循同一套规范,避免发生命名冲突,其格式为appname.category[.subcategory]*.suffix
性能优化
在上述例子中,一样的指标能够命名为dubbo.provider.service.qps{service="DemoService"}
,其中前面部分的名称是固定的,不会变化,括号里面的Tag,能够动态变化,甚至增长更多的维度,例如增长 method 维度dubbo.provider.service.qps{service="DemoService",method="sayHello"}
,也能够是机器的 IP、机房信息等。这样的数据存储是时间序列数据库亲和的,基于这些数据能够轻松实现任意维度的聚合,筛选等操做。多线程
P.s. 2017年12月底,Dropwizard Metrics4.0 开始支持 Tag,Dubbo Metrics 中 ag 的实现参考了Dropwizard。spring-boot 2.0中提供的 MicroMeter 和 Prometheus 也均已引入了 Tag 的概念。并发
2、添加精准统计功能
Dubbo Metrics 的精准统计是和 Dropwizard,或者其余开源项目埋点统计库实现不太同样的地方。下面分别经过时间窗口的选择和吞吐率统计方式这两个纬度进行阐述。app
在统计吞吐率(如 QPS)的时候,Dropwizard的实现方式是滑动窗口+指数加权移动平均,也就是所谓的EWMA,在时间窗口上只提供1分钟、5分钟、15分钟的选择。
固定窗口 vs 滑动窗口
在数据统计的时候,咱们须要事先定义好统计的时间窗口,一般有两种确立时间窗口的方法,分别是固定窗口和滑动窗口。
固定时间窗口指的是以绝对时间为参考坐标系,在一个绝对时间窗口内进行统计,不管什么时候访问统计数据,时间窗口不变;而滑动窗口则是以当前时间为参考系,从当前时间往前推一个指定的窗口大小进行统计,窗口随着时间,数据的变化而发生变化。
固定窗口的优势在于:一是窗口不须要移动,实现相对简单;二是因为不一样的机器都是基于相同的时间窗口,集群聚合起来比较容易,只需按照相同的时间窗口聚合便可。其缺点是:若是窗口较大,实时性会受到影响,没法当即获得当前窗口的统计信息。例如,若是窗口为1分钟,则必须等到当前1分钟结束,才能获得这1分钟内的统计信息。
滑动窗口的优势在于实时性更好,在任意时刻,能都看到当前时刻往前推演一个时间窗口内统计好的信息。相对而言,因为不一样机器的采集时刻不一样,要把不一样机器上的数据聚合到一块儿,则须要经过所谓的 Down-Sampling 来实现。即按照固定时间窗口把窗口内收集到的数据应用到某一个聚合函数上。举个例子来讲,假设集群有5台机器,按照15秒的频率按照平均值进行 Down-Sampling,若在00:00~00:15的时间窗口内,在00:01,00:03,00:06,00:09,00:11各收集到一个指标数据,则把这5个点的加权平均认为是00:00这个点的通过 Down- Sampling 以后的平均值。
但在咱们的实践过程当中,滑动窗口仍会遇到了如下问题:
为了解决这些问题,Dubbo Metrics 提供了 BucketCounter 度量器,能够精确统计整分整秒的数据,时间窗口能够精确到1秒。只要每台机器上的时间是同步的,那么就能保证集群聚合后的结果是准确的。同时也支持基于滑动窗口的统计。
瞬时速率(Rate) vs 指数移动加权平均(EWMA)
通过多年的实践,咱们逐渐发现,用户在观察监控的时候,首先关注的实际上是集群数据,而后才是单机数据。然而单机上的吞吐率其实并无太大意义。怎么理解呢?
好比有一个微服务,共有2台机器,某个方法在某一段时间内产生了5次调用,所花的时间分别是机器1上的[5,17],机器2上的[6,8,8](假设单位为毫秒)。若是要统计集群范围内的平均 RT,一种方法能够先统计单机上的平均 RT,而后统计总体的平均 RT,按照这种方法,机器1上平均 RT 为11ms,机器2的平均 RT 为7.33ms,二者再次平均后,获得集群平均 RT 为9.17ms,但实际的结果是这样吗?
若是咱们把机器1和机器2上的数据总体拉到一块儿总体计算的话,会发现实际的平均 RT 为(5+17+6+8+8)/5=8.8ms,二者相差很明显。并且考虑到计算浮点数的精度丢失,以及集群规模扩大,这一偏差会越发明显。所以,咱们得出一个结论:单机上的吞吐率对于集群吞吐率的计算没有意义,仅在在单机维度上看才是有意义的。
而 Dropwizard 提供的指数加权移动平均其实也是一种平均,同时考虑了时间的因素,认为距离当前时间越近,则数据的权重越高,在时间拉的足够长的状况下,例如15分钟,这种方式是有意义的。而经过观察发现,其实在较短的时间窗口内,例如1s、5s,考虑时间维度的权重并无太大的意义。**所以在内部改造的过程当中,Dubbo Metrics 作了以下改进:
**
3、极致性能优化
在大促场景下,如何提高统计性能,对于 Dubbo Metrics 来讲是一个重要话题。在阿里的业务场景下,某个统计接口的 QPS 可能达到数万,例如访问 Cache 的场景,所以这种状况下 metrics 的统计逻辑极可能成为热点,咱们作了一些针对性的优化:
高并发场景下,数据累加表现最好的就是java.util.concurrent.atomic.LongAdder
,所以几乎全部的操做最好都会归结到对这个类的操做上。
避免调用 LongAdder#reset
当数据过时以后,须要对数据进行清理,以前的实现里面为了重用对象,使用了LongAdder#reset
进行清空,但实测发现LongAdder#reset
实际上是一个至关耗费cpu的操做,所以选择了用内存换 CPU,当须要清理的时候用一个新的 LongAdder 对象来代替。
去除冗余累加操做
某些度量器的实现里面,有些统计维度比较多,须要同时更新多个 LongAdder,例如 Dropwizard Metrics的 meter 实现里面计算1分/5分/15分移动平均,每次须要更新3个 LongAdder,但实际上这3次更新操做是重复的,只须要更新一次就好了。
RT为0时避免调用Add方法
大多数场景下对 RT 的统计都以毫秒为单位,有些时候当 RT 计算出来小于1ms的时候,传给metrics的 RT 为0。当咱们发现 JDK 原生的 LongAdder 并无对add(0)
这个操做作优化,即便输入为0,仍是把逻辑都走一遍,本质上调用的是sun.misc.Unsafe.UNSAFE.compareAndSwapLong
。若是这个时候,metrics 判断 RT 为0的时候不对计数器进行 Add 操做,那么能够节省一次 Add 操做。这对于并发度较高的中间件如分布式缓存颇有帮助,在咱们内部某个应用实测中发现,在30%的状况下,访问分布式缓存的 RT 都是0ms。经过这个优化能够节约大量无心义的更新操做。
QPS 和 RT 合并统计
只须要对一个Long的更新,便可实现同时对调用次数和时间进行统计,已经逼近理论上的极限。
通过观测发现,一般对于中间件的某些指标,成功率都很是高,正常状况下都在100%。为了统计成功率,须要统计成功次数和总次数,这种状况下几乎同样多,这样形成了必定的浪费,白白多作了一次加法。而若是反过来,只统计失败的次数,只有当失败的状况才会更新计数器,这样能大大下降加法操做。
事实上,若是咱们把每一种状况进行正交拆分,例如成功和失败,这样的话,总数能够经过各类状况的求和来实现。这样能进一步确保一次调用只更新一次计数。
但别忘了,除了调用次数,还有方法执行 RT 要统计。还能再下降吗?
答疑是能够的!假设 RT 以毫秒为单位进行统计,咱们知道1个 Long 有64个bits(实际上Java里面的Long是有符号的,因此理论上只有63个 bits 可用),而 metrics 的一个统计周期最多只统计60s的数据,这64个 bits 不管怎样用都是用不掉的。那能不能把这63个 bits 拆开来,同时统计 count 和 RT 呢?其实是能够的。
咱们把一个 Long 的63个 bits 的高25位用来表示一个统计周期内的总 count,低38位用于表示总 RT。
------------------------------------------ | 1 bit | 25 bit | 38 bit | | signed bit | total count | total rt | ------------------------------------------
当一次调用过来来的时候,假设传过来的 RT 是n,那么每次累加的数不是1,也不是n,而是
1 * 2^38 + n
这么设计主要有一下几个考虑:
若是真的overflow了怎么办?
因为前面分析过,几乎不可能overflow,所以这个问题暂时没有解决,留待后面在解决。
无锁 BucketCounter
在以前的代码中,BucketCounter 须要确保在多线程并发访问下保证只有一个线程对 Bucket 进行更新,所以使用了一个对象锁,在最新版本中,对 BucketCounter 进行了从新实现,去掉了原来实现中的锁,仅经过 AtomicReference 和 CAS 进行操做,本地测试发现性能又提高了15%左右。
Dubbo Metrics 全面支持了从操做系统,JVM,中间件,再到应用层面的各级指标,而且对统一了各类命名指标,能够作到开箱即用,并支持经过配置随时开启和关闭某类指标的收集。目前支持的指标,主要包括:
操做系统
支持Linux/Windows/Mac,包含CPU/Load/Disk/Net Traffic/TCP。
JVM
支持classload, GC次数和时间, 文件句柄,young/old区占用,线程状态, 堆外内存,编译时间,部分指标支持自动差值计算。
中间件
Tomcat: 请求数,失败次数,处理时间,发送接收字节数,线程池活跃线程数等;
Druid: SQL 执行次数,错误数,执行时间,影响行数等;
Nginx: 接受,活跃链接数,读,写请求数,排队数,请求QPS,平均 RT 等;
更详细的指标能够参见这里, 后续会陆续添加对Dubbo/Nacos/Sentinel/Fescar等的支持。
Dubbo Metrics 提供了基于 JAX-RS 的 REST 接口暴露,能够轻松查询内部的各类指标,既能够独立启动HTTP Server提供服务(默认提供了一个基于Jersey+ sun Http server的简单实现),也能够嵌入已有的HTTP Server进行暴露指标。具体的接口能够参考这里:
https://github.com/dubbo/metrics/wiki/query-from-http
数据若是彻底存在内存里面,可能会出现由于拉取失败,或者应用自己抖动,致使数据丢失的可能。为了解决该问题,metrics引入了数据落盘的模块,提供了日志方式和二进制方式两种方式的落盘。
使用方式很简单,和日志框架的Logger获取方式一致。
Counter hello = MetricManager.getCounter("test", MetricName.build("test.my.counter")); hello.inc();
支持的度量器包括:
参考资料
Dubbo Metrics @Github:
https://github.com/dubbo/metrics
Wiki:
https://github.com/dubbo/metrics/wiki (持续更新)
本文做者:
望陶,GitHub ID @ralf0131,Apache Dubbo PPMC Member,Apache Tomcat PMCMember,阿里巴巴技术专家。
子观,GitHub ID @min,Apache Dubbo Commiter,阿里巴巴高级开发工程师,负责 Dubbo Admin 和 Dubbo Metrics 项目的开发和社区维护。