9-10双月的用户数等于9月和10月用户数的累加吗?
在日常的大数据开发工作中,不同维度条件下的uv等QoE指标的聚合与拆分需要耗费大量的计算与存储资源。
背景
QoS与QoE指标的计算方法差异
维度:数据不同的属性,如时间、地区、场景、编码格式等
指标:数据的具体量级表征,如播放量、播放时长、用户数等
粒度:根据维度对指标进行拆分,拆分的维度越多,数据的粒度越细,比如可以把播放量拆app_id、os、编码方式等等
聚合:把细粒度的指标汇总成粗粒度的指标:比如把24个小时粒度的指标汇总,得到了天粒度的指标。拆分的含义则相反。
在数据建设过程中,我们常常会遇到两类指标:QoS(Quality of Service)与QoE(Quality of Experience)指标。其中QoS注重服务质量的衡量,比如播放量、播放时长;而QoE指标注重用户体验指标的衡量,比如用户量、人均播放时长等。这两类指标不仅在意义上有差别,在计算方式上也不同。
QoS指标:不同维度下的拆分与聚合只需简单的数学累加。
QoE指标:不同维度下的拆分与聚合不能依靠简单的累加。
遇到的问题
不同维度下的QoE指标无法进行拆分与聚合,所以每次为了获得不同维度下的QoE指标,均需要从明细中获取。这意味着大量的计算与存储资源消耗。
-
QoE大盘指标建设的问题
- QoE指标常常需要在app_id、os、tag这三个维度下拆分与聚合分析,因此需要分别通过明细去计算着3个维度不同组合情况下的QoE指标。
-
xxx数据建设的问题
- xxx需要对客户的QoS和QoE指标进行展示。QoS指标可以通过聚合后的指标进行累加得到,而QoE指标必须通过明细计算,因此需要维护两套数据流以产出QoS和QoE数据:计算与存储资源浪费大,查询效率低。
传统的数据结构与算法随着数据量级的增加,对应的空间复杂度与时间复杂度均爆炸增长,无法满足大数据场景下不同维度QoE指标的灵活聚合与拆分。
HLL算法的原理简介
为了解决上述问题,我们探索了以下方法。
bitmap
bitmap可以理解为通过一个bit数组来存储特定数据的一种数据结构,bit是数据的最小存储单位,每一个bit位都能独立包含信息,因此能大量节省空间。如果定义一个很大的bit数组,基数统计中每一个元素对应到bit数组的其中一位,例如8位的bit数组 00110101代表实际数组[2,3,5,7]。每新加入一个元素,只需要将已有的bit数组和新加入的数字做按位或 (or)计算。
bitmap中1的数量就是集合的基数值。
- 存储成本
如果要统计1亿个数据的基数值,大约需要内存:100000000/8/1024/1024 ≈ 12M
- 合并性能
bitmap有一个很明显的优势,就是可以轻松合并多个统计结果,只需要对多个结果进行或运算即可。
- 存在的问题
bitmap对于内存的节约量是显而易见的,但还是不够。比如统计一个链接的访问UV需要12M,如果对10000个链接进行统计,就需要将近120G了,同样不能广泛用于大数据场景。
在不熟悉数据规模的时候,如何设置BitMap的大小也是一个问题。
Linear Counting
是一种早期的基数估计算法。
首先定义一个hash函数:hash(x): -> [0,1,2,…,m-1],假设该hash函数的hash结果服从均匀分布。
接着定义一个长度为m的bit数组,开始每一位上都初始化为0。
然后对集合中的每个元素进行hash得到k,如果bitmap[k]为0则置1。
最后统计bitmap数组里为0的位数u。
设集合基数为n,则有:,且其为的最大似然估计。
集合中共有12个元素,hash函数映射到[0,7]中(m=8)。如图hash结果后共有2个bit位为0,即u=2。代入上述公式可得估计结果为11.1(实际值为12)。
为了避免当m设置太小,出现u=0,n̂ 为无穷大的情况,合理的m设置十分关键。
- 存储成本
LC在空间复杂度方面并不算优秀。
可以看出精度要求越高,则bitmap的长度越大。可以看出对于N是几万以内是很有优势的。但是,随着m和n的增大,m大约为n的十分之一。因此,LC所需要的空间只有传统的bitmap直接映射方法的1/10,但是从渐进复杂性的角度看,空间复杂度仍为O(Nmax);
- 合并性能
Linear Counting非常方便于合并,直接通过按位或的方式即可。
LogLog Counting & HyperLogLog Counting
抛硬币回合次数的估计
硬币拥有正(1)反(0)两面,每一次上抛至落下,最终出现正反面的概率都是1/2。
那么需要多少回合才能出现0001这样的记录呢?通过概率计算可知,这样的概率为(1/2)^4=1/16。也就是说,平均需要16回合,才会出现0001这样的序列。换言之,如果出现了0001这样的序列,可以期望已经进行了16回合的抛硬币。
进一步地,目前出现了这样一个序列:w=w1w2w3...wk...wn,其中wk=1,w1w2w3...wk-1均为0,不考虑wk+1~wn,则出现这样的序列其概率为(1/2)^k,则一共进行了2^k回合。
分桶减小误差
受到抛硬币回合次数估计的启发,对UV统计有了一个粗略的设想:将visitor_id进行hash函数处理成均匀分布的的01序列,找到序列第一次出现1的位数k,则该序列出现的概率为(1/2)^k,总的UV数量(抛硬币的回合数)为2^k。同样对其他的visitor_id进行处理,获得序列第一次出现1的位数k,最终比较所有k的大小,记录kmax,则估算总UV为2^kmax。
这种算法看起来有一定的道理,但是忽略了极端情况所带来的影响:仍然以抛硬币为例,可能只需要1个回合就可以抛出0001序列,与估算的16回合相差甚远。为了减小这种极端情况带来的误差,进行了分桶策略。
已知这样一个序列:x=xn...xb+2xb+1xb...x3x2x1,首先依据低位xb...x3x2x1对x进行分桶,再根据高位xn...xb+2xb+1计算k值估算基数,最终每个分桶中均会得到一个估计的基数值,进一步取得均值作为最终的基数值。
当分桶数量为m时,基数估计为:
\hat n=m*2^{{\frac 1 m}{\sum{max(k_{i})}}
仍然以1个回合抛出0001序列为例,在不同的分桶数下,估算的回合数也发生变化。
1个桶->16回合,
2个桶->8回合,
4个桶->8回合,
也就是说,增加桶数在一定条件下可以有效提高基数的估算精度。
通过数学分析进一步修正公式,增加修正系数αm,在不同的分桶数量下选择合适的αm对结果进行修正:
\hat n=α_m *m* 2^{{\frac 1 m}{\sum{max(k_{i})}}
修正系数常常采取以下近似值:
以上即为LogLog Counting算法的基本思路与实现,标准误差为:
当m=1024时,误差约为4%。
存储成本
空间复杂度与Hash 长度L和分桶数m有关。
- 设基数空间n ->n=2^->
- -> 每个桶需要bit的内存 -> 每个桶需要bit内存
- 桶数m -> m*p -> 一共需要 bit的内存大小。
可以看到,LLC算法需要的空间仅仅是基数空间n的两次log的大小。这也是loglog Counting算法的命名来源。
假设n为40亿,m为1024,L值为32bit,则每个桶需要5bit空间存储,一共需要5×1024 = 5120 bit = 640字节。
合并性能
合并操作非常简单,只需要比较多个相同桶编号的k值并取最大者为合并后此桶的k值,再进行计算即可。
以上为LogLog Counting算法的介绍。
优化为HyperLogLog Counting
- 使用调和平均代替几何平均数
- 分段偏差修正
HLLC中,为了解决LLC在基数较小时偏差大的问题,在小基数时,选择LC估计。设E为估计值:
- 当时,使用LC进行估计。
- 当 \frac 5 2m \le E \le {\frac 1 {30} 2^{32}时,使用HLLC公式计算。
- 当时,估计公式为。
工程实践与成果
实现架构
Hive:一种低成本、高延迟的存储介质,用于存储大量的历史数据
ClickHouse:一种高成本、低延迟的存储介质,用于存储少量的热数据
使用方法
解决的问题与取得的成果
根据上述架构方案,解决了前述的问题。
总结
在大数据场景下,采用传统的数据结构对UV进行多维度聚合及统计的时间、空间代价极大,而我们采用了具有查询速度快、占用内存小、合并效率高、估算精度准等特点的HyperLogLog算法,大量节约了时间与空间成本。
参考文档
Loglog counting of LARGE Cardinalities
HyperLogLog: The analysis of a near-optimal cardinality estimation algorithm