❝作者简介:荆佳颉,百度高级研发工程师 负责微服务治理,全链路监控和线上性能管控等方向。
❞
百度广告业务系统建立在分布式系统之上,面向商业服务,每天发生各类接口调用PV达到百亿次,产生TB级的监控数据,对监控系统的设计也提出了巨大的挑战。分位值对接口性能的敏感度高,在性能分析中具有很大价值。
分位值是一组数据中排名在某个百分比的值。如:开机时360提示“您的电脑击败了全国80%的用户”,即代表启动时间在全国所有电脑中排名20%分位值。
在接口性能分析中,分位值至关重要。因为许多极端请求都集中在99%分位值以上,数量少但影响大,通过平均值无法观察到。99%的正常请求会把1%的极端数字平均掉,导致看起来系统响应很快。但这些少数极端请求会造成1%用户极为不好的用户体验。
实时采集数据样本,上传到Spark,Flink等计算集群,进行流式计算。
离线计算,将数据直接导入数仓,再由定时进行任务批量计算分位值
对于APM线上监控的场景,对实时性要求较高,不适用离线计算架构。
使用JMeter,LoadRunner等压测工具,在压测过程中采集数据样本,实时排序,即时计算。
如Prometheus等监控工具,可收集各个实例数据样本,在需要查询分位值时,再即时计算。
分而治之是大数据计算的基本思路。但是分位值如何归并,是分而治之的重点难题。
分位值的特点决定了分位值不能简单地分治计算再归并。与平均值不能再求平均同理,同一个应用的多个实例求得的分位值再求平均没有数学意义,不能代表集群整体的分位值。
既然分位值本身不能归并,那么原始数据可以归并吗?当然是可以的。既然原始数据可以归并,那么原始数据的摘要可以归并吗?也是可以的。
❝解释:摘要是对原数据样本的一个数据分布的抽象,可以在一定误差内,等同于原数据样本。常见的数据摘要算法有,直方图,T-Digest,GK算法等。
❞
基于数据摘要,我们可以在各个实例本地维护一个数据分布(即摘要),每次得到一个请求耗时样本数据,就更新数据分布,在本地实现一次聚合。每隔固定周期将本地聚合的数据分布上传到数据仓库。
「这是第一次聚合」,可以将单个实例每小时数百万次请求,压缩成占用几十kb的数据分布,大幅降低数据规模,使承载百亿级场景成为可能。
在数据仓库中存储各个实例上传的数据分布,不进行计算。直到有用户查询某个接口的分位值数据时,才筛选出该接口下的所有数据分布,归并成一个数据分布,再用该数据分布推算出分位值。
「这是二次聚合」,假设一个接口有100台实例,每小时采集一次,则一天只有2400个数据分布。归并2400个数据分布,在0.1s内就可以完成。
通过两级聚合,可将计算开销分摊至数万实例上,对业务性能几乎没有影响,又无需引入额外计算资源。结合即席计算的优势,可实现秒级查询,和高度灵活性,高度符合APM场景。
总体架构如下图:
在接口拦截阶段,需要实现拦截应用的各个接口,在接口执行前后执行监测逻辑,从而计算出接口的响应时间,然后将响应时间发送到数据分布,以进行后续聚合逻辑。具体逻辑如下图:
接口拦截的具体技术可以通过手工埋点、Java字节码增强等方式来实现。
数据分布是对接口响应时间数据样本的摘要,用来在后面的环节中近似估算分位值。数据分布需要满足如下特征的数据结构:
数据分布的具体实现有很多种,如 直方图,T-Digest,Q-Digest,DK算法 等。分位值的精度取决于数据分布算法选型,我们在实践中选用T-Digest结构。
每次拦截到接口调用后,获取到本次调用的相应时间,然后更新本接口的数据分布。流程如下:
由于每个应用实例上会包含若干个(几十到上百个)接口。对于每个接口我们需要分别记录响应时间的数据分布。我们利用一个表格数据结构来存储各个接口的数据分布。聚合项存储数据结构:
接口(名字仅作示意) | 响应时间数据分布 |
---|---|
com.baidu.app1.Foo.play() | <数据分布> |
com.baidu.app1.Bar.play() | <数据分布> |
com.baidu.app1.Cool.play() | <数据分布> |
...... |
当采集到数据样本时,先根据接口名,在上述表格中找到接口对应的行(如果没有则新建一行),然后将数据样本(即本次调用响应时间)合并进接口名对应的数据分布中。流程如下图:
应用实例在本地分别记录各个接口和其响应时间的数据分布。每到达既定时间后,将上表全部内容发送到数据仓库,并清空本地表格。流程如下:
数据分布数据为内存中的二进制数据,在上传的过程中需要进行序列化。序列化的具体实现一般推荐如下处理:
数据仓库中存储了分布式系统中所有接口的响应时间直方图。大体表结构如下:
列名 | 类型 | 含义 |
---|---|---|
app | 字符串 | 所属应用 |
method | 字符串 | 所属接口 |
quantile_data | 数据分布 | 分布内容 |
log_date | 日期 | 记录时间 |
为了计算某个接口的响应时间分位值p,我们需要将各个实例上传的该接口的数据分布聚合项做二次聚合,用二次聚合后的数据分布聚合项来计算分位值p,则此分位值即代表该接口在整个应用集群(所有实例)上的总分位值。
这里分位值p是由用户查询时实时指定的。也就是说,每次计算分位值的时候,都可以支持任意分位值的计算,而不是预设的分位值。
我们的T-Digest结构在百亿数据下每日1GB左右数仓存储,最高精度0.1%左右。
总体架构简单,性能高,成本高度可控,易于维护,稳定,风险低,多快好省地满足线上系统监控分位值计算场景。
同时对于其它大数据分位值需求场景,也是一种非常优秀的参考方案。