AddThis(前身为Clearspring)的数据分析副总监Matt Abrams在High Scalability上发表了一篇文章,介绍了他们公司如何应对大数据。在这篇文章中,AddThis仅仅用了1.5KB内存的内存就计算了十亿个不同的对象,Matt Abrams主要向我们详解了他们公司在处理过程中使用的方法。
以下为文章全文:
在AddThis,我们喜欢统计数据。对一组中不同元素(也称为“基数”)的数量进行计数,当其数量很大时,这是一个挑战。
为了更好地理解确定的大型成套基数的挑战,让我们想象一下,在你的日志中有一个16个字符的ID,并且你想计算不同ID的数量。这里有一个例子:
4f67bfc603106cb2
16个字符需要用128位来表示。6万5千个ID将需要1MB的空间。我们每天收到30多亿个事件,每个事件都有一个ID。这些ID需要3840亿位或45GB的存储。而这仅仅是ID字段需要的空间!为了得到我们日常活动中的ID基数,我们可以采取一个简单的方法。最简单的想法是使用内存中的哈希集合,其中包含在输入文件中看到的独特的ID列表。即使我们假设3条记录中有1个是唯一的,哈希集合仍需要119GB的RAM,不包括Java需要在内存中存储对象的开销。你需要一台配备几百GB内存的机器来计算不同的元素,并且这只是计算一天的独特ID的成本。如果我们想要数周或数月的数据,这个问题只会变得更加困难。我们身边当然不会有一台配备几百GB内存的空闲机器,所以我们需要一个更好的解决方案。
解决这一问题的常见办法是使用位图。位图可以用来快速、准确地获取一个给定的输入的基数。位图的基本思路是使用哈希函数映射数据集到一个位字段,在位字段里输入的独特元素映射到该字段中的一个位。这产生零碰撞,并减少需要计算每个独特元素到1位的空间。虽然位图大大减少了对空间的要求,但当基数是非常高或你对非常多的不同的组进行计数时,它们仍然有问题。例如,如果我们想要使用位图计数十亿,你将需要十亿位,或需要每个约120 MB的计数器。稀疏的位图可以被压缩,以获得更多的空间效率,但也并不总是有帮助的。
幸运的是,基数估计是一个热门的研究领域。我们已经利用这项研究提供了一个开源实现的基数估计、集员资格检测和top-k算法。
为了了解算法占用的空间与精确度之间的关系,我们用三种不同的计算方法在所有莎士比亚的作品计数了不同单词的数量(90万)。请注意,我们的输入数据集有额外的数据,所以基数高于这个问题答案的标准参考。我们使用的三种技术是:Java HashSet、Linear Probabilistic Counter以及一个Hyper LogLog Counter。这里是结果:
该表显示,我们只使用512字节的空间就可以进行计数,并且误差在3%以内。相比之下,HashMap的计数准确度最高,但需要近10MB的空间,你可以很容易地看到为什么基数估计量是有用的。在实际应用中精度并不是很重要的,这是事实,在大多数网络规模和网络计算的情况下,用概率计数器可能会节省巨大的空间,这是真的。
线性概率计数器
线性概率计数器是高效的空间,并且允许实现者指定所需的精度水平。该算法在注重空间效率时是很有用的,但你需要能够控制你结果里的误差。该算法运行需要两步:第一步,在内存中分配一个初始化为都为0的位图,随后哈希函数被应用于输入数据中的每个条目,哈希函数的结果将映射条目到位图的一个位上,该位设置为1;第二步算法对空位的数量进行计算,并使用这个数字输入到下面的公式来获得估计。
n=-m ln Vn
在方程中,m是位图的大小,n是空位和映射的大小的比率。需要重点注意的是原始位图的大小,可以远小于预期的最大基数。小多少取决于你能忍受多少错误的结果。因为位图的大小、m,小于不同元素的总数,将会有碰撞。这些碰撞都可以节省空间,但同时也造成了错误的估计。所以通过控制原始映射的大小,我们可以估算碰撞的次数,因此我们将在最终结果中看到误差量。
Hyper LogLog
顾名思义,Hyper LogLog计数器的特点是,你仅需要使用loglog(Nmax)+一个常数那么多位就可以对Nmax进行计数。如线性计数器的Hyper LogLog计数器使设计人员能够指定所需的精度公差,在Hyper LogLog的情况下,这是通过定义所需的相对标准偏差和你期望要计数的最大基数。大部分计数通过一个输入数据流、M,并应用一个哈希函数设置h(M)来工作。这将产生一个S = h(M) of {0,1}^∞字符串的可观测结果。通过分割成m子字符串的哈希输入流,并为每个子数据保持m的观测值扩展了Hyper LogLog。使用额外的观测值的平均值,产生一个计数器,其精度提高为m的大小增长,只需要一个在输入集的每个元素上恒定要执行的操作数目。其结果是,这个计数器可以仅使用1.5 kb的空间计算精度为2%的十亿个截然不同的项。与执行 HashSet所需的120 兆字节进行比较,这种算法的效率变得很明显。
合并分布式计数器
我们已经证明了使用上面描述的计数器我们可以估算大集合的基数。但是,如果你的原始输入数据集不适合于单台机器,你能做些什么?这正是我们在AddThis所面临的问题。我们的数据分散在数百台服务器上,并且每个服务器只包含整个数据集子集的一部分。这就是事实,我们可以合并一组分布式计数器的内容,这是至关重要的。这个想法有点令人费解,但如果你花费一些时间去思考这个概念,就会发现其与基本的基数估计值相比并没有太大的不同。因为计数器代表映射中的位作为基数,我们可以采取两个兼容计数器并将其位合并到单一的地图。这个算法已经处理碰撞,所以我们可以得到一个基数估计所需的精密,即使我们从来没有把所有的输入数据到一台机器。这是非常有用的,节省了我们在网络中移动数据的大量时间和精力。
总结
希望这篇文章能帮助你更好地理解这个概念和概率计数器的应用。如果估计大集合的基数是一个问题,而你又碰巧使用一个基于JVM的语言,那么你应该使用stream-lib项目——它提供了其他几个流处理工具以及上文所述的算法的实现。