ClickHouse 在微信有着广泛应用,如何保障其自身查询性能,并能在新场景中结合应用成为了关键问题。基于该背景,开发团队首先针对ClickHouse 的性能问题,开发了相应的性能观测工具,并在数据查询、策略实验等场景针对性进行了湖仓读取、bitmap 计算等方面的探索优化,最后将 ClickHouse 在 AI 场景进行落地应用,沉淀了融合 OLAP 能力的成熟数据管线。
一、ClickHouse 在微信的应用
1. ClickHouse 在微信的应用
ClickHouse 在微信团队有着广泛应用,如实时报表、AB 实验和实时计算等,通过将 Hadoop 相关生态集成到 ClickHouse 中,性能得到了十倍到百倍的提升,能够做到万亿级数仓、亚秒级响应和稳定高可用。
ClickHouse 在微信的集群规模有数千台,Top50 响应时长约 0.34 秒,平均响应时长为 4 秒,查询量级为每天百万级。当前的主要版本是基于社区的 22.8,少量版本对应社区 23.3。
2. 新场景应用
过去一年我们探索了 ClickHouse 的一些新的应用场景。在湖仓读取方面,基于 Iceberg/Hive 进行读取和湖上数据加工,来缓解数据孤岛问题;在实验新场景上,进行画像分析、人群圈选,支撑实时可见的在线实验系统;另外,也与 AI 进行结合,通过成熟的 OLAP 数据管线为近/离线模型推理进行提效。
二、ClickHouse 的性能观察工具
作为一个用户,需要感知查询的资源消耗;作为一个运维同学,需要知道如何优化集群负载;作为一个开发同学,需要快速定位慢查询的原因。这些都离不开性能观察工具。ClickHouse 提供了一系列便捷的性能观测工具,如 Query Log、Query Thread Log、Sampling Query Profiler 和 Flame Graph 等。
首先是最常用的 Query Log 和 Query Thread Log,通过查询的 query id,可以对这条查询性能进行观察分析。我们还可以在代码中增加自定义的 Profile Event,方便定制一些观测指标。
第二个是 Sampling Query Profiler 和 ClickHouse Flame Graph,通过可视化的火焰图能够直观地对内存和 CPU 进行分析,在 CH 可以对指定查询进行 profile,支持的最细粒度为 query 级别。它有一个缺点,会将一个查询涉及到的多个线程汇聚到一起,导致无法对单个线程的情况进行分析。我们针对这个问题也做了改进优化,使它能支持线程级的单个展示和查询聚合。
第三个是 Processors Profile Log,它可以帮助我们清晰地看到每个算子的耗时,判断算子间是否均衡、是否存在倾斜情况,也可以帮助我们看到算子间的依赖关系。
WeOLAP 团队还自研了性能分析工具 Profile Engine,从事前和事后两个场景进行优化。在事前对用户提交的 SQL 结合集群信息和表信息进行分析,并基于索引、分区等给出相应可视化改进建议;在事后基于制定的规则对大查询和慢查询进行分析,给出优化建议。通过这个工具,既可以给使用者提出优化建议,也可以帮助使用者平衡集群负载。该工具上线后的使用效果很不错。
三、湖仓读取优化
ClickHouse 在湖仓链路中既是存储组件又是计算组件,跨层的存在会导致一些问题:
- ClickHouse 中的数据有孤岛化倾向,不能被 Spark、Presto 等引擎查询。
- 数据冗余,Shared-nothing 带来昂贵的机器成本。
- 繁琐的数据 ETL。
我们的改进目标是让 ClickHouse 作为计算组件,直接读取湖仓数据。
其中存在一些挑战:
- ClickHouse 目前只支持单机读取 Hive。
- ClickHouse 支持读取 Iceberg,但仅限 S3 存储。
- Iceberg 没有 C++ 的 API。
- 现在只支持 Hive/Iceberg 外表,一旦表 schema 变化,需要手动同步 DDL 修改。
- 部分场景的 ORC 读取性能不佳。
针对上述问题,我们采取了如下优化措施:
- 新增外置 HTTP 协议的 Iceberg API server,使用 Java 绕开 C++ 限制,实现外置 server。
- 通过一致性 hash 分发文件路径到各节点实现分布式读取。
- 对元信息和数据文件进行 cache。
- 读取集群和计算集群分离。
增加外库实现,避免手动繁琐的建表和元信息不一致问题。
ClickHouse 在读取某些 ORC 文件时会很慢,例如示例的 select * 和 select count(1)。
通过火焰图分析,我们发现 Apache Arrow 库读取 ORC 有大量的 memcpy,十分影响性能。我们切换到了 Apache ORC 库进行读取,整体性能提升了 0.5 到 1 倍。
在某些场景会出现 IO 浪费,如图中的 select 一列,在 stripe size 为 4MB 和 64MB 时,对应解压后的大小相等,但 HDFS 读取量差异很大。
ReadBuffer 在读取时很容易 cache 大量我们不需要的数据,帮我们缓存很多不需要的列,造成大量 IO 浪费。此外,在读取时会先读 stripe footer,再读 row data,导致频繁地 HDFS seek。以上这两点是造成 IO 浪费的主要原因。
我们采用 IO 预读机制对 ORC 的读取性能进行优化。首先,ORC 文件可以提前计算文件中哪些 range 是需要被读取的,基于此,我们将读取规则改为当读命中某个 range 时,按照 range 粒度执行预读,并将临近 range 进行合并,减少HDFS seek 次数。
在应用该读取优化后,性能提升十分明显,以图中的读取 6 列为例,原有的 40 秒查询缩短至 3.7 秒,提升了 10 倍。
此外,我们还做了 HDFS 优化、元信息优化和资源并发链接限制,基于这些优化,在典型场景性能提升了 5 到 10 倍。
四、实验场景 Bitmap 优化
在命中分析、画像圈选中可以使用 bitmap 进行查询加速,将原有的交并补逻辑转换为位图操作,相比明细表的聚合或 join 查询,通常可以取得数倍的性能提升。
ClickHouse 数据按行进行拆分运算,在 bitmap 场景中,不用批数据的行数,即使行数相同,其代表的计算工作量也存在很大差异,造成了数据倾斜,其中某个 pipe 的工作量显著高于别的 pipe,以至拖慢了整个查询。
我们的解决方案是在执行引擎新增 repartition 阶段,重新进行数据均衡,并将数据分发到所有后续 pipe。在大 bitmap 计算中,数据倾斜场景性能提升约 10%~20%。
我们通过 ClickHouse Flame Graph 对三个线程的执行过程进行分析,发现有两个执行线程长时间等待,而另一个执行线程耗时在读取 bitmap,读取开销远大于计算。
ClickHouse 在 mark 级以下没有任何并行化机制,我们针对性优化成支持行级并行读取,对于大 bitmap 异步进行反序列化读取,并减少内存拷贝操作。
另外,我们通过对原有字段编码进行压实,既节省了存储空间,又提升了性能。
新增内核特性可编码字典 Encode Dictionary,支持单机字典和副本同步字典,支持所有原生 ClickHouse 字典函数,支持 value to key 反查,以及 bitmap to bitmap 编码。
在经过以上优化后,我们在测试数据集上的性能提升很明显。在 bitmap32 上,求并集和交集有 10 倍的性能提升,在 bitmap64 上,有百倍的提升。
在实际业务应用上,bitmap64 场景从查不了变为查得快,bitmap32 场景从快到更快,在画像分析、实验留存分析和表存储等方面优化效果都很不错。
五、ClickHouse with AI
随着机器学习的兴起,图片或文本通过 embedding 高维向量的方式表达,求解相似度会转换为计算向量间的距离。在离线加工场景使用 OLAP 有很多优势,比如可以基于元数据过滤、做一些聚合操作,以及配合 UDF 进行加工等等。此外,我们也在精确距离运算、ANN 索引等方面做了一些探索性的优化。
我们基于 ClickHouse 对整套算法链路做了重构,融合 OLAP 成熟数据管线,实现了推理、加工和检索一体化。当有复用需求时,可以直接修改数据管线中的 SQL 配置或 UDF,从而大大降低了使用成本。
我们还做了向量精确检索查询优化,将其封装为 SQL,对于后续的需求可以方便地进行修改迭代。并且对查询 SQL 进行了性能优化:
- 通过 SQL 改写,采用 with 代替 join,减少冗余计算;prefilter 提前过滤不必要元素。
- 使用 ZSTD 压缩,优化数据结构。
- 加入 repartition 阶段,解决线程间数据倾斜问题。
另外,我们还优化了 embedding 计算相关函数,在业务场景中取得了 4 倍的性能提升:
- 我们在内核中新增了一个向量距离计算函数 NormalizedCosineDistance,它可以在归一化场景下减少整体计算量。
- 同时我们也根据业务场景定制了 embedding vector distance 函数,通过大幅减少计算的过程中的 memcpy,性能有了很大的提升。
以上就是本次分享的内容,谢谢大家。