干货|ClickHouse 在UBA系统中的字典编码优化实践

ClickHouse UBA版本是字节跳动内部在开源版本基础上为火山引擎增长分析专门深度定制优化的版本。本篇文章介绍在字典编码方向上的优化实践。

image.png

文 |Jet He 字节跳动数据平台研发工程师,长期致力于OLAP引擎开发优化,在OLAP领域、用户行为在线分析等有丰富的经验。

背景

虽然ClickHouse列存已经有比较好的存储压缩率,但面对海量数据时,磁盘空间的占用跟常用的Parquet格式相比仍然有不少差距。特别是对于低基数列时,Parquet的存储空间会更加有优势。

同时,大多这类数据的事件属性都有低基数的特征,例如事件属性中的城市、性别、品牌等等。Parquet会自动对低基数列做字典编码,因此会获得更高的存储效率。

同时ClickHouse官方也提供了一种字典编码的解决方案即LowCardinality类型,网上也有一些测试Benchmark数据,效果不错,可以进一步降低存储空间和提升查询、IO性能。

image.png

上图是内部LowCardinality的存储结构,写入过程中,会构建一个字典,列数据通过Positions表示,数值是字典中每个Unique值的Index。其他更加详细的介绍可以参考官方文档。

但在内部环境中通过验证测试发现,原始的LowCardinality列存在以下两个致命问题:

  1. 在LowCardinality列比较多的情况下(平均300+),Part Merge耗时严重,在大量实时写入的场景下,Merge速度跟不上写入速度,最终会导致集群不可用;

  2. 用户数据中事件属性多种多样,UBA版本通过动态Map列实现用户属性的自由上报,也会导致某些属性基数非常大,不再适合做字典编码,否则会同时导致存储、计算性能下降。

如果以上两个问题得不到解决,那么字典编码功能就无法上线使用。需要一种解决方案,能够做到支持大量的列做字典编码的同时需要保证内部Part的Merge速度,另外就是面对高基数列时需要一个Fall back方案,让高基数列时不再做字典编码,改用原始列存储。原作者在做字典编码技术分享时也提到了针对高基数列时Fall back到原始列的构想,但社区版本中目前没有付诸实现。

解决方案

首先来看针对LowCardinality列Part Merge的优化方案。

这里先介绍下ClickHouse的Part Merge过程。ClickHouse的数据组织是以Part形式存在的,每个Part对应磁盘的一个数据目录,每次写入都会生成一个Part,Part目录下包含各个列的数据文件。因此每次写入的时候最好是大批量的写入,才能有较好的写入吞吐。

ClickHouse有常驻Worker线程不断的做Part的Merge,将小Part不断地Merge成大Part,从而提升查询性能。如果Part不能及时Merge会造成严重的性能问题,更有甚者还会造成Inodes耗尽。

当统一把事件属性列(Map列)改为LowCardinality列时,发现Part Merge耗时严重,Part数会不断增长,最终会导致集群不可用。通过Profile发现,在LowCardinality列Part Merge时,耗时主要发生在字典构造上,具体如下图灰色部分所示:

image.png

即在做Part Merge过程中,首先会通过Primary Key列做排序,然后从每个Part中获取对应的Row写入到一个新的Part中。例如一次从Part1中取3行写入到新Part中,下一次从Part2中取5行写入到新Part中,写入到新Part时,LowCardinality首先做构建新的字典,并生成好倒排索引,形成一个新的LowCardinality列,然后通过Column的Insert接口完成写入。另外在构建字典的过程中,是通过一个HashTable实现,这样在做Merge时这块的性能损耗较大,所以优化的关键点就是在于字典的构建过程。

这里实现了一种先构建字典后做具体Merge的思路,即多个Part的Merge过程中,词典只需要构建一次,然后接下来的Merge只需要将Index直接Append写入到新Part即可。

整个过程可以分为两个过程:

01 -Dictionary Merge

image.png

首先进行字典的Merge,在Merge的过程中,先将待Merge的几个Part中的字典部分做Merge,生成一个字典,同时记录下每个Part这个列中Index的变化,这个变化类似一个转换矩阵;

Index Merge过程中将这个转换矩阵逐个Apply到Part中的Index,有时这个转换矩阵为空,例如Unique值很少的列,基本可以保证每个Part的字典基本一样,如果转换矩阵为空这步操作会直接跳过。

02 -Index Merge

Index Merge过程跟之前的Merge过程一致,只不过这里不再做字典构建了,会直接将列中的Index Append 到新列的Index中,如下图所示:

image.png

经过这个Merge优化后,LowCardinality的Merge性能有明显提升,在大量写入的场景也能应付自如,写入的Part可以得到及时Merge。

具体的性能优化测试数据如下表所示,Merge速度的是在表写入过程中统计得出,写入大量大概10亿左右:

Merge优化前Merge优化后
float64 类型 (distinct 200)6~7 MiB/sec37 ~ 45 MiB/sec
string 类型 (distinct 100000)6 ~ 8 MiB/sec12 ~ 40.53 MiB/sec
string 类型 (distinct 1M)~ 25 MiB/sec~ 28 MiB/sec
string 类型 (distinct 10M)~ 44.99 MiB/sec~ 28 MiB/sec

可以看出在基数10万以内时性能提升非常明显,当基数100万+时,性能提升不明显,并且在1000万时还会导致性能回退。这里也不难理解,因为当基数变大时,Merge过程中转换矩阵会变得很大,转换矩阵的Apply的过程就会变成一个新的瓶颈点。解决这一问题的只有Fall back方案,即将高基数列自动不做字典编码。

Fall back方案在内部做了很多讨论,也跟原作者讨论了可能的实现方案。

最终通过LowCardinality内部封装的方式实现。

如下图所示:Stream可以理解为文件流,通过Version值标识该列是否是已经是Fall back的列。

image.png

内部复用了Index Stream,如果发生了Fall back那么这个Stream里面的值便是原始列的值。Fall back可以发生在实时写入过程中和Part Merge过程中。如果此列发生了Fall back后续的所有Part都将是Fall back的。

Fall back后,一个高基数列的Merge速度和存储性能对比,连续写入1亿条记录的统计:

LowCardinality ColumnLowCardinality Fall back ColumnNative Column
Merge速度28M/s ~ 70M/s125M/s ~ 190M/s200M/s ~ 210M/s
存储空间1063M1013M1013M

从表中可以看出,Fall back后的列基本跟原始列性能接近,至少保证Merge和存储性能没有退化。如果不做Fall back,存储空间占用会比原始列还要多,Merge性能无法支撑实时写入。

通过Merge优化和自动Fall back解决了LowCardinality列的两大绊脚石,接下来看下我们在内部一些大应用上的测试验证效果。

性能验证

下面是在内部某些大APP上的验证结果。

磁盘占用

image.png

数据表是内部某些APP某个时间段的数据。从上表中可以看出,列越多,数据量越大,存储空间下降就会越明显,最高可以节省一半的数据存储空间。在数据量非常的大APP场景下,上线LowCardinality后可以节省大量的存储资源。

针对某个APP,获取其典型的10个业务SQL,做查询性能测试,下面是两个数据表分别查询的对比测试结果:

image.png

从上图可以看出,有两个SQL导致查询性能有回退现象,其余SQL都是LowCardinality的表查询性能更优,耗时更短。

10个查询对应的磁盘数据读取量:

image.png

可以看出,基本上所有SQL读取的数据量都有明显的减少,对磁盘IO的压力会降低很多。SQL8对应的查询列已经做了Fall back,所以跟原始列读取数据量持平。

下图是查询时对应的内存使用量:

image.png

其中除了SQL8发生了Fall back外,其他查询均是LowCardinlity表内存使用量较大。由于LowCardinality列计算过程中,如filter,需要读取的Part字典并将列反解出来,每个Part的字典是独立存在的,这样在计算过程中会多占用些内存。这块也是后续优化的重点。

小结

目前ClickHouse UBA版已经全面启用了字典编码列,并且在火山引擎增长分析(DataFinder)服务的多个客户环境中已经上线。从实践反馈看,我们为客户节省了大量存储资源,同时在大多数场景下查询性能也有提升明显。总体上由于字典位于每个Part中独立存储,查询过程中无法做到在压缩域直接计算,因而会造成个别场景下查询性能不佳,并且内存使用量上会增加。

下一步工作的重点将是优化LowCardinality的计算过程,例如把字典做成Part间共享的,可以减少计算过程中内存占用,进一步扩展复杂场景在可以直接在压缩域做计算。

参考文献:

相关产品:

火山引擎增长分析: 一站式用户分析与运营平台,为企业提供数字化消费者行为分析洞见,优化数字化触点、用户体验,支撑精细化用户运营,发现业务的关键增长点,提升企业效益。

163
1
0
0
相关产品
评论
未登录
看完啦,登录分享一下感受吧~
暂无评论