数据统计的各个指标计算,简单的讲,就是求不同纬度数据的最大、最小、叠加、去重的过程。
目前Crasheye的崩溃统计报表基本为复合维度统计,而多维度统计的复杂度要远远高于单维度的运算,无论是在RDB还是NoSQL,瓶颈都会落在IO上面,造成IO瓶颈的根源很大部分来自于数据维度的划分与聚合,特别是对于去重统计类型的数据。
Crasheye的Crash监控统计数据,就有多维度去重统计的多个数据指标,这种需要实时显示的去重指标统计,对后台的设计实现都是一种挑战。
先看一部分crasheye的单一时间下的基本统计维度:
维度 |
APP |
APP版本 |
SDK版本 |
渠道 |
操作系统类型 |
操作系统版本 |
宕机 |
设备型号 |
网络环境 |
APP |
|
|
|
|
|
|
|
|
|
APP版本 |
|
|
|
|
|
|
|
|
|
SDK版本 |
|
|
|
|
|
|
|
|
|
渠道 |
|
|
|
|
|
|
|
|
|
操作系统类型 |
|
|
|
|
|
|
|
|
|
操作系统版本 |
|
|
|
|
|
|
|
|
|
宕机 |
|
|
|
|
|
|
|
|
|
设备型号 |
|
|
|
|
|
|
|
|
|
网络环境 |
|
|
|
|
|
|
|
|
|
比如下面这个统计:app启动次数在各个设备型号的分布情况,这个就是时间、app、设备型号三个维度下的增量统计。
如果换成受影响的用户统计呢 ?那就是时间、APP、crash(App崩溃)等多维度下的去重统计。这样的去重统计被称为求基数,先定义下面的三个概念:
目前统计指标的计算均采用redis实现,接下来介绍下,redis最常用的三种求基数方法。
基于set
Redis去重最常用的统计方式,set的概念就是一堆不重复值的组合,所以只需要将去重的数据插入一个set中,需要统计的时候直接使用scard统计该数据集的基数。对于时间、app、版本之类等维度信息,可以拼接放在key中,那么这个多个维度的去重数据列表就是这个set。
基于bitmap
Redis以(Key,Value)的形式存储数据,这里的Value支持二进制数据。而bitmap就是一串连续的2进制数字(0或1),每一位所在的位置为偏移(offset),在bitmap上可执行位操作(余、或、非等)。
所以就可以通过一个bit的0或1来记录某个元素是否存在信息,这样相同的数据就会映射到同一个bit上,这样只要统计bitmap中值为1的位的个数,就是去重统计项的值了。
基于hyperloglog
hyperloglog是基于概率统计的一种方法,既然是概率统计,那么其准确度就不是百分之百,存在少量误差。其特点是在输入元素的数量非常非常大时,计算基数所需的空间总是固定的、并且是很小的。该特性对于大数据的统计而言,其误差是可以容忍的。
redis提供的这三种去重统计方式各有优劣,确切的说各有使用场景,不同场景可以考虑使用不同的方式进行数据去重计算。
我们来看看三种方式的对比:
假设去重统计项的单个member的大小为10个字节,那么三种方式不同数量级下的内存消耗如下:
member总量/单日 |
Set |
Bit |
HyperLogLog |
100万 |
10M |
150K |
12K |
500万 |
50M |
750K |
12K |
1千万 |
100M |
1.5M |
12K |
由此可见,set的内存消耗是最大的,如果是大数据统计,比如单日上亿数据且数据失效时间比较长,那么内存的消耗就非常大,这个时候set的选择就要非常慎重,持续增长的数据+持续消耗的内存,往往会造成更多地问题。相反,如果数据量比较小,或者数据过期时间很短,内存消耗不大的情况,set也是不错的选择。
由此可见,如果你要计数结果十分精确的话,采用hyperloglog就要谨慎了。
使用复杂度
由此可见,在使用难易程度上,bit较为复杂,set与hyperloglog则容易操作的多。如果需要返回去重后的列表数据,那显然bit与hyperloglog是满足不了的。
单从时间复杂度上看时间消耗并不严谨,那么写了例子,简单测试下set与hyperloglog的耗时。
如下代码所示,测试连续写入100万数据下,对比set与hyperloglog:
测试结果如下,写入数据前后redis的内存变化及写入耗时对比:
HyperLogLog |
|
used_memory:1006248 |
used_memory:1022520 |
used_memory_human:982.66K |
used_memory_human:998.55K |
used_memory_rss:7229440 |
used_memory_rss:7200768 |
used_memory_peak:1200040 |
used_memory_peak:1200040 |
used_memory_peak_human:1.14M |
used_memory_peak_human:1.14M |
used_memory_lua:36864 |
used_memory_lua:36864 |
mem_fragmentation_ratio:7.18 |
mem_fragmentation_ratio:7.04 |
mem_allocator:jemalloc-3.6.0 |
mem_allocator:jemalloc-3.6.0 |
耗时 |
|
start |
2016-04-13 10:37:33.308076 |
end |
2016-04-13 10:40:04.704386 |
|
|
Set |
|
used_memory:1022520 |
used_memory:121413800 |
used_memory_human:998.55K |
used_memory_human:115.79M |
used_memory_rss:7213056 |
used_memory_rss:131321856 |
used_memory_peak:1200040 |
used_memory_peak:121433784 |
used_memory_peak_human:1.14M |
used_memory_peak_human:115.81M |
used_memory_lua:36864 |
used_memory_lua:36864 |
mem_fragmentation_ratio:7.05 |
mem_fragmentation_ratio:1.08 |
mem_allocator:jemalloc-3.6.0 |
mem_allocator:jemalloc-3.6.0 |
耗时 |
|
start |
2016-04-13 10:52:19.418109 |
end |
2016-04-13 10:55:18.209096 |
由上图可见,同样的数据量连续写入的耗时,两者是差不多的,但内存的消耗则有相当大的区别。
上边四方面对比,总结一下,精确度要求百分之百的,不建议使用hyperloglog,大数据量的不建议使用set。通常情况下,统计方式选择的参考,一般是由下面三者的平衡决定的。
后台优化中,往往有空间换时间的做法,例如以增加机器的方式,提高响应的速度。数据统计过程中,也同样有类似的做法。如为了提高性能,降低计算准确度、减少运算数据量或者增加内存等,上边介绍的hyperloglog就是用准确性换取数据空间的例子。
大数据量下的实时统计计算,对性能提出要求;统计结果准确度的要求,也往往会牺牲掉部分性能;不断累积的数据又对存储容量提出更高的要求。可见一种统计方式的选择就是要在计算性能、计算精度、数据量三个方面做一个平衡的选择。
crasheye官网:http://www.crasheye.cn