百亿级监控场景大数据分位值计算实践
The following article is from 商业平台技术实践 Author 荆佳颉
全文3286字,预计阅读时间7分钟
一、背景
百度广告业务系统建立在分布式系统之上,面向商业服务,每天发生各类接口调用PV达到百亿次,产生TB级的监控数据,对监控系统的设计也提出了巨大的挑战。分位值对接口性能的敏感度高,在性能分析中具有很大价值。
1.1 分位值是什么
分位值是一组数据中排名在某个百分比的值。如:开机时360提示“您的电脑击败了全国80%的用户”,即代表启动时间在全国所有电脑中排名20%分位值。
1.2 Why分位值?
在接口性能分析中,分位值至关重要。因为许多极端请求都集中在99%分位值以上,数量少但影响大,通过平均值无法观察到。99%的正常请求会把1%的极端数字平均掉,导致看起来系统响应很快。但这些少数极端请求会造成1%用户极为不好的用户体验。
二、分位值常见计算方案
2.1 流式计算
实时采集数据样本,上传到Spark,Flink等计算集群,进行流式计算。
优点:最高可以得到100%精确的分位值; 缺点:在百亿级场景下,将全量数据导入计算集群,消耗资源巨大,成本极高。不适用监控场景。
2.2 离线计算
离线计算,将数据直接导入数仓,再由定时进行任务批量计算分位值。
对于APM线上监控的场景,对实时性要求较高,不适用离线计算架构。
2.3 压测端计算
使用JMeter,LoadRunner等压测工具,在压测过程中采集数据样本,实时排序,即时计算。
优点:精度高,实时性强; 缺点:但局限于单个应用,压测阶段。规模有限,且没有长周期的变化趋势等分析能力。
2.4 即席查询计算
如Prometheus等监控工具,可收集各个实例数据样本,在需要查询分位值时,再即时计算。
优点:节约计算资源,可按需计算,灵活性强; 缺点:耗费存储资源,尤其是在百亿级场景下,存储数据结构的选取大幅影响资源开销和即席查询响应时间,对技术可行性有决定性影响。
三、分而治之的计算架构
3.1 分而治之
分而治之是大数据计算的基本思路。但是分位值如何归并,是分而治之的重点难题。分位值的特点决定了分位值不能简单地分治计算再归并。与平均值不能再求平均同理,同一个应用的多个实例求得的分位值再求平均没有数学意义,不能代表集群整体的分位值。
既然分位值本身不能归并,那么原始数据可以归并吗?当然是可以的。既然原始数据可以归并,那么原始数据的摘要可以归并吗?也是可以的。
解释:摘要是对原数据样本的一个数据分布的抽象,可以在一定误差内,等同于原数据样本。常见的数据摘要算法有,直方图,T-Digest,GK算法等。
3.2 采集端聚合
基于数据摘要,我们可以在各个实例本地维护一个数据分布(即摘要),每次得到一个请求耗时样本数据,就更新数据分布,在本地实现一次聚合。每隔固定周期将本地聚合的数据分布上传到数据仓库。
「这是第一次聚合」,可以将单个实例每小时数百万次请求,压缩成占用几十kb的数据分布,大幅降低数据规模,使承载百亿级场景成为可能。
3.3 汇聚层归并
在数据仓库中存储各个实例上传的数据分布,不进行计算。直到有用户查询某个接口的分位值数据时,才筛选出该接口下的所有数据分布,归并成一个数据分布,再用该数据分布推算出分位值。
「这是二次聚合」,假设一个接口有100台实例,每小时采集一次,则一天只有2400个数据分布。归并2400个数据分布,在0.1s内就可以完成。
通过两级聚合,可将计算开销分摊至数万实例上,对业务性能几乎没有影响,又无需引入额外计算资源。结合即席计算的优势,可实现秒级查询,和高度灵活性,高度符合APM场景。
四、具体实现
总体架构如下图:
原理具体分为如下步骤:
「接口拦截」 在接口调用时进行拦截,获取接口响应时间; 「端上聚合」 将拦截到的响应时间聚合进数据分布中,每到达聚合时间区间后,将本地的数据分布上传到数据仓库,然后清空数据分布; 「二次聚合」 在数据仓库端,对各个实例上传的数据分布按接口名进行二次聚合,按每个接口分别合并成接口总数据分布,从而推算接口总分位值。
Step1. 接口拦截
在接口拦截阶段,需要实现拦截应用的各个接口,在接口执行前后执行监测逻辑,从而计算出接口的响应时间,然后将响应时间发送到数据分布,以进行后续聚合逻辑。具体逻辑如下图:
接口拦截的具体技术可以通过手工埋点、Java字节码增强等方式来实现。
Step2. 端上聚合
数据分布是对接口响应时间数据样本的摘要,用来在后面的环节中近似估算分位值。数据分布需要满足如下特征的数据结构:
描述原数据样本的分布结构 可接收新的数据样本,调整数据分布 符合结合律,即多个数据分布可以合并成一个总的数据分布 可以用数据分布估算出原数据样本的近似分位值 使用常数大小内存空间,且精度可控
数据分布的具体实现有很多种,如 直方图,T-Digest,Q-Digest,DK算法 等。分位值的精度取决于数据分布算法选型,我们在实践中选用T-Digest结构。
每次拦截到接口调用后,获取到本次调用的相应时间,然后更新本接口的数据分布。流程如下:
由于每个应用实例上会包含若干个(几十到上百个)接口。对于每个接口我们需要分别记录响应时间的数据分布。我们利用一个表格数据结构来存储各个接口的数据分布。聚合项存储数据结构:
实例元信息:
「应用名:app1」 「实例:instance1 (10.20.30.40)」 「聚合开始时间:2020-10-10 18:00:00」 「聚合周期:1h」 「下次提交时间:2020-10-10 19:00:00」
聚合信息:
接口(名字仅作示意) | 响应时间数据分布 |
com.baidu.app1.Foo.play() | <数据分布> |
com.baidu.app1.Bar.play() | <数据分布> |
com.baidu.app1.Cool.play() | <数据分布> |
...... |
当采集到数据样本时,先根据接口名,在上述表格中找到接口对应的行(如果没有则新建一行),然后将数据样本(即本次调用响应时间)合并进接口名对应的数据分布中。流程如下图:
应用实例在本地分别记录各个接口和其响应时间的数据分布。每到达既定时间后,将上表全部内容发送到数据仓库,并清空本地表格。流程如下:
数据分布数据为内存中的二进制数据,在上传的过程中需要进行序列化。序列化的具体实现一般推荐如下处理:
「采用小端格式」数据仓库多为C++实现。如果选择C/C++实现的数据仓库,则需采用小端格式序列化。因为x86架构基于小端格式,序列化数据保持一致格式,方便C/C++解析。如果采用Java实现或其它大端格式的数仓(如Hive),则无需采用小端格式。实践中,我们采用百度自研的Apache Doris数仓,并使用C++开发分位值二次聚合算子。 「压缩」由于接口响应时间样本数据往往具有强偏向性,数据分布数据结构内部会存在大量零值,启用压缩可以显著压缩数据体积,减小网络传输和存储资源占用。具体压缩算法可以采用GZIP,BZIP2等。实践中我们采用GZIP算法,压缩比率在30%-40%左右。 「编码」由于直方图数据本身是二进制格式,通过例如BASE64等算法编码成文本格式,更加易于传输、存储和调试。实践中,BASE64编码对体积有20%左右增加,但存储空间相对廉价,相比带来的优势,可以接受。
Step3. 二次聚合
数据仓库中存储了分布式系统中所有接口的响应时间直方图。大体表结构如下:
列名 | 类型 | 含义 |
app | 字符串 | 所属应用 |
method | 字符串 | 所属接口 |
quantile_data | 数据分布 | 分布内容 |
log_date | 日期 | 记录时间 |
为了计算某个接口的响应时间分位值p,我们需要将各个实例上传的该接口的数据分布聚合项做二次聚合,用二次聚合后的数据分布聚合项来计算分位值p,则此分位值即代表该接口在整个应用集群(所有实例)上的总分位值。
这里分位值p是由用户查询时实时指定的。也就是说,每次计算分位值的时候,都可以支持任意分位值的计算,而不是预设的分位值。我们的T-Digest结构在百亿数据下每日1GB左右数仓存储,最高精度0.1%左右。
五、技术优点
总体架构简单,性能高,成本高度可控,易于维护,稳定,风险低,多快好省地满足线上系统监控分位值计算场景。同时对于其它大数据分位值需求场景,也是一种非常优秀的参考方案。
参考阅读:
技术原创及架构实践文章,欢迎通过公众号菜单「联系我们」进行投稿。