ClickHouse场景及其原理
ClickHouse是Yandex公司于2016年开源的一个列式数据库管理系统。Yandex的核心产品是搜索引擎,非常依赖流量和在线广告业务,因此ClickHouse天生就适合用户流量分析。
这里直接从原始数据开始消费,通过Flink清洗任务将数据洗入数据仓库存储,在数据仓库经过作业清洗并在ClickHouse生成用户行为明细,可以称作无模型化明细数据。利用ClickHouse原生的RoaringBitMap函数对参与计算的行为人群交并差集计算。
那么难点和挑战在哪里?主要是 3 个方面:
标签、人群圈选
行为分析平台、标签画像平台、AB实验人群包都是基于ClickHouse的RBM(RoaringBitMap)实现,此外RBM还有其他多项应用,比如事件分析标签人群圈选、预计算的路径分析、创建用户行为的用户分群等。
标签建群
实现要点
采用性能最好的稀疏位图索引 RoaringBitmap 来表示一个标签对应的人群包。在这样的情况下,集合的计算可以转换到对应位图的计算例如 A 交上 B 和 C 的并集可以转换为RoaringBitmap的计算。
RoaringBitmap用来存储稀疏的数据
https://www.ics.uci.edu/~goodrich/teach/cs165/notes/BinPacking.pdf
采用分桶的思想来将稀疏数据存储节约内存。
● RoaringBitmap64是由一系列RoaringBitmap32表示。实现方式有很多种,一种比较通用的做法用map存储,是把前 32 位存成 key,value是后32 所对应的 RoaringBitmap32,RoaringBitmap32 的实现如图中所示。第一层称之为 Chunk(高 16 位),如果该取值范围内没有数据就不会创建 Chunk。第二层称之为Container(低 16 位),会依据数据分布进行创建。
● RoaringBitmap32使用两种容器结构:Array Container和Bitmap Container。Array Container存放稀疏的数据,Bitmap Container存放稠密的数据。若一个Container 里面的元素数量小于4096,就使用Array Container;反之就用Bitmap来存储值。
uid是否需要映射递增?
uid通过特定的编码方式(高32位表示某种含义,低32位表示某种含义),并非连续递增生成,这样会加大uid的稀疏性。当数据比较稀疏的时候,一个人群包对应的RoaringBitmap64由很多个 RoaringBitmap32组成,每个RoaringBitmap32内部又由很多个array container组成。而对有序数组的交并补计算尽管也比较高效,但是相比于bitmap计算来说还是有明显的差异。这样导致计算性能提升不上去。因此能不能通过编码的方式,对区间内的数据进行编码,让数据更加集中,从而提升计算效率。
希望达到如下效果:
● 编码后同一个区间内的用户相对集中
● 不同区间的用户编码后同样在不同的区间内
● 编码后同一个人群包同一个区间内的用户 id 相对集中
● 通过编码,能够非常好地加速计算,计算速度提升 1~2 个量级
● 编码的过程是在引擎内部实现的,对用户是无感知的。当数据导入的时候,会自动完成编码。
有这几个问题需要解决:
组合建群
人群预估
人群画像
主要是对广告投放的用户群进行画像分析,也是在线的,同样对时间有一定的要求,因为是偏分析的场景,一般不能超过 20 秒,否则用户的体验就非常差了。
人群导入
基于ClickHouse构建的一套海量UBA技术解决方案,底层ClickHouse集群的稳定性 、读写性能、资源使用率均会影响上层业务的使用体验。与此同时,海量数据如何导入ClickHouse,以及数据导入过程的稳定性、导入效率、资源消耗在很大程度上决定了ClickHouse集群的整体稳定性和使用效率。所以,一个稳定高效的数据导入方案对于一套UBA解决方案来说是必不可少的。
统计分析
的使用场景比较多,在线、离线都有,包括一些搜索词统计分析,广告、投放收入数据的分析等等,应用的方面很多。
ClickHouse应用优化实践
在支持UBA场景各项功能模块的过程中,我们针对ClickHouse的查询,存储等方面做了大量应用优化工作。下面选取其中几个优化点做简单介绍。
查询下推
ClickHouse中的针对分布式表的查询会被改写成对local表的查询并发送到集群各个shard执行,然后将各个shard的中间计算结果收集到查询节点做合并。当中间计算结果很大时,比如countDistinct、 windowFunnel函数等,查询节点的数据收集和数据合并可能成为整个查询的性能瓶颈。
查询下推的思路就是尽量将计算都下推到各个shard执行,查询节点仅收集合并少量的最终计算结果。不过,也不是所有查询都适合做下推优化,满足以下两个条件的查询可以考虑做下推优化:
● 数据已经按照计算需求做好sharding:比如,UBA场景的数据已按user id做好了sharding,所以针对用户的漏斗分析,UV等计算可以下推到各个shard执行。否则,下推后的计算结果是不准确的。
● 计算的中间结果较大:sum,count等计算是无需下推的,因为其中间结果很小,合并计算很简单,下推并不能带来性能提升。
下面,我们以上文中提到的漏斗分析为例,阐述一下如何做查询下推。
上图是用windowFunnel函数实现漏斗分析的一个SQL,如图中“执行步骤”所示,该查询需要从各shard收集大量数据并在查询节点完成计算,会产生大量数据传输和单点计算量。
我们先使用配置distributed_group_by_no_merge做了一版下推优化:
优化SQL-V1将windowFunnel的计算下推到各个shard执行,仅在查询节点对windowFunnel的最终结果做聚合计算。在我们的场景下,该版本较上一版本性能提升了5倍以上。
为了更进一步做查询下推,我们利用cluster + view的函数组合,将聚合查询进一步下推:
优化SQL-V2的性能较优化SQL-V1进一步提升30+%.
Array和Map的跳数索引支持
UBA场景中的事件数据有很多公共属性和私有属性,公共属性被设计为表的固定字段,而私有属性因为各个事件不尽相同,所以采用Array/Map来存储。最初的设计是采用两个数组分别存储属性名和属性值,ClickHouse支持Map结构后,则在后续模块中采用Map来满足类似需求。无论是Array还是Map,最初都不支持创建跳数索引,所以在其他索引字段过滤效果有限的情况下,针对Array和Map的操作可能会成为查询的性能瓶颈。
针对这个问题,我们给Array和Map加上了Bloom filter等跳数索引支持,针对Map仅对其key构建索引。在某些出现频率较低的私有属性过滤场景下,Array/Map的跳数索引可以收获数倍的性能提升。
压缩算法优化
ClickHouse常用的数据压缩方式有三种,分别为LZ4、LZ4HC以及ZSTD。针对不同的数据类型,数据分布方式来使用特定的编码方式可以大大提高数据压缩率,以减少存储成本。
针对UBA场景,我们测试了不同压缩算法的压缩率,写入性能,查询性能。相较默认的LZ4,ZSTD(1)在压缩率上普遍可以节省30%以上的存储空间,查询性能方面未见明显差异,不过写入性能在某些场景下有20%左右的下降。由于UBA场景数据存储压力较大,同时对数据时效性要求不是很高,因此我们最终选择了ZSTD(1)作为主要的压缩方式。