HyperLogLog在Presto和ClickHouse中的兼容及性能差异

前言

当前HyperLogLog是一种主流的算法,用于估算海量同类型数据的不同值,因此几乎所有的计算/查询引擎都有了想关的实现,当然虽然可能其它的优化算法,但算法主体相同,然而不同引擎实现的存储过程大同小异,如果想要在不同引擎之前共享中间结果,就需要深入了解不同引擎的存储实现。

Presto是Facebook开源的,完全基于内存的并⾏计算,分布式SQL交互式查询引擎是一种Massively parallel processing (MPP)架构,多个节点管道式执⾏⽀持任意数据源(通过扩展式Connector组件),数据规模GB~PB级。

ClickHouse 是一个真正的列式数据库管理系统(DBMS)。在 ClickHouse 中,数据始终是按列存储的,包括矢量(向量或列块)执行的过程。只要有可能,操作都是基于矢量进行分派的,而不是单个的值,这被称为«矢量化查询执行»,它有利于降低实际的数据处理开销。

但碍于Presto在实际工作中会出现不稳定性的情况,便推动了Presto HLL计算的迁移,以期更加快速地响应用户的查询请求,这便是这篇博客的背景。

HLL数据共享流程

Presto/Hive上的基数预估的中间结果,实际上是一组格式化的字节数组,而ClickHouse并不支持直接加载HLL的字节数组,因此就需要扩展ClickHouse的聚合函数,即开发UDAF(自己写C++代码啦,目前CH并不是很方便地编写UDF/UDAF),完成Presto引擎上计算产生的中间结果的读取加载过程。至于CH上的内存数据存储格式,依然采用CH本身的实现,以减少工作量。

但需要注意的是,CH即使已经能够直接加载来自Presto的HLL数据,考虑到Presto和CH在计算HyperLogLog时采用的Hash逻辑不同,一个是小端一个大端,依然不能直接聚合两个引擎各自产生的中间数组,因此目前的模式是单向的,未来考虑双向聚合。

简言之,整个过程就是Presto HLL序列化 => 自定义数据格式 => 写入CH => 调用CH UDAF聚合,最终完成Presto HLL数据在CH上的聚合过程。

HLL聚合性能比较

Presto

数据总量(条) HLL数据读取以及merge耗时(单位秒)
100000 4
200000 9
500000 23
1000000 44

ClickHouse

数据总量 HLL数据读取及Merge耗时(单位秒)
100000 5
200000 18
500000 46
1000000 91

性能差异分析

源表(viprpdm.ads_mer_high_dim_detail_df)静态统计信息:

select count(*) from test_table where dt=‘20210111’;

dt分区的数据总量N_total=2675032

select count(*) from test_table where dt=‘20210111’ and length(test_hll) <=2060;

dt分区中字节数组长度小于等于2060的总量N_2060=2670580

select count(*) from test_table where dt=‘20210111’ and length(test_hll) <= 1024;

dt分区中字节数组长度小于等于1024的总量N_1024=1112470

其中rate_1024 = N_1024 / N_total = 0.416,大约占总量的一半

由上的统计信息可以看到,源表中的HLL数据的字节数组长度,基本上所有的字段值的长度都约为4096个桶的一半,这里以Dense类型的存储格式为主说明性能差异的原因。

一般地Dense类型的数据,会使用一个与桶数相同长度的数据,来保存每一个value,举例4096个桶,就对应了4096个slot的数组,其中每个slot会存入一个6位长度的数来表示不同值。

应用程序中最本的计算单元是按字节来处理,而一个字节需要8位的比特表示,因此Presto为了更能高效地来存储和处理HyperLogLog计算的中间结果,会将桶中应该存放的6位值对齐到

4位,即VALUE_BITS >> 1,这样桶数也会降低到原来的一半BUCKET_BITS >> 1,那么在不考虑溢出的情况下,就只需要[2048 * 4 / 8 = 1024, 4096 * 6/8]范围内的存储就可以完成计算。

由于对6位的值对齐到4位,必然会出现实际值溢出问题,即某个桶的值超过了2^4,Presto内部会创建一个溢出数组,来保存溢出的值,这样依然在溢出的场景能够很好的工作,虽然这样的做法相对于直接使用一个字节来表示桶值复杂,但通常情况下溢出的现象不会很频繁,因此这种设计在更普遍的情况下会带来存储和计算的性能提升。

总结

综上,当前从Presto HLL数据到CK的中间结果的存储格式,采用一个字节来存放完整的桶值,因此抛却必要的头部信息外,需要4096*6/8=3072 bytes。

实际上CK本身在自己的HyperLogLog实现中,也是采用了6位表示的存储格式,但总的数组大小为(4096*6 + 7)/8,因此可能在大部分情况下,HLL计算性优劣如下:

中间转换格式 < CK存储格式 < Presto存储格式

因此,如果想要更好地打通ClickHouse与Presto之前的HyperLogLog数据,在应用层后续重点工作当是完善ClickHouse侧加载Presto HLL数据的过程。

参考项目地址:

你可能感兴趣的:(数据存储,算法/数据结构,clickhouse,presto,hyperloglog,算法)