字节跳动基于 ClickHouse 优化实践之 Upsert
更多技术交流、求职机会、试用福利,欢迎关注字节跳动数据平台微信公众号,回复【1】进入官方交流群
相信大家都对大名鼎鼎的 ClickHouse 有一定的了解,它强大的数据分析性能让人印象深刻。但在字节大量生产使用中,发现了 ClickHouse 依然存在了一定的限制。例如:
缺少完整的 upsert 和 delete 操作
多表关联查询能力弱
集群规模较大时可用性下降(对字节尤其如此)
没有资源隔离能力
因此,我们决定将 ClickHouse 能力进行全方位加强,打造一款更强大的数据分析平台。
本篇将详细介绍我们是如何为 ClickHouse 补全更新删除能力的。
实时人群圈选场景遇到的难题
在电商业务中,人群圈选是非常常见的一个场景。字节原有的离线圈选的方案是以 T+1 的方式更新数据,而不是实时更新,这很影响业务侧的体验。现在希望能够基于实时标签,在数据管理平台中构建实时人群圈选的能力。整体数据链路如下:
为了保证实时数据和离线数据同时提供服务,在标签接入完毕后,在 ClickHouse 中完成宽表加工任务。但是原生 ClickHouse 只支持追加写的能力,只有 ReplacingMergeTree 这种方案。但是选用 ReplacingMergeTree 引擎的限制比较多,不能满足业务的需求,主要体现在:
性能下降严重,ReplacingMergeTree 采用的是写优先的设计逻辑,这导致读性能损失严重。表现是在进行查询时性能较 ClickHouse 其他引擎的性能下降严重,涉及 ReplacingMergeTree 的查询响应时间过慢。
ReplacingMergeTree 引擎只支持数据的更新,并不支持数据的删除。只能通过额外的定制处理来实现数据清除,但这样会进一步拖慢了查询的性能。
ReplacingMergeTree 中的去重是 Merge 触发的,在刚导入的数据时是不去重的,过一段时间后才会在分区内去重。
解决方案:UniqueMergeTree
在这种情况下,字节在 ByteHouse(字节基于 ClickHouse 能力增强的版本)中开发了一种支持实时更新删除的表引擎:UniqueMergeTree。UniqueMergeTree 与以往的表引擎有什么差别呢?下面介绍两种支持实时更新的常见技术方案:
原生 ClickHouse 选择的技术方案
原生 ClickHouse 的更新表引擎 ReplacingMergeTree 使用 Merge on Read 的实现逻辑,整个思想比较类似 LSMTree。对于写入,数据先根据 key 排序,然后生成对应的列存文件。每个 Batch 写入的文件对应一个版本号,版本号能用来表示数据的写入顺序。
同一批次的数据不包含重复 key,但不同批次的数据包含重复 key,这就需要在读的时候去做合并,对 key 相同的数据返回去最新版本的值,所以叫 merge on read 方案。原生 ClickHouse ReplacingMergeTree 用的就是这种方案。
大家可以看到,它的写路径是非常简单的,是一个很典型的写优化方案。它的问题是读性能比较差,有几方面的原因。首先,key-based merge 通常是单线程的,比较难并行。其次 merge 过程需要非常多的内存比较和内存拷贝。最后这种方案对谓词下推也会有一些限制。大家用过 ReplacingMergeTree 的话,应该对读性能问题深有体会。
这个方案也有一些变种,比如说可以维护一些 index 来加速 merge 过程,不用每次 merge 都去做 key 的比较。
面向读优化的新方案
UniqueMergeTree 使用的技术方案 Mark-Delete + Insert 方案刚好反过来,是一个读优化方案。在这个方案中,更新是通过先删除再插入的方式实现的。
Ref “Enhancements to SQLServer Column Stores”
下面以 SQLServer 的 Column Stores 为例介绍下这个方案。图中,每个 RowGroup 对应一个不可变的列存文件,并用 Bitmap 来记录每个 RowGroup 中被标记删除的行号,即 DeleteBitmap。处理更新的时候,先查找 key 所属的 RowGroup 以及它在 RowGroup 中行号,更新 RowGroup 的 DeleteBitmap,最后将更新后的数据写入 Delta Store。查询的时候,不同 RowGroup 的扫描可以完全并行,只需要基于行号过滤掉属于 DeleteBitmap 的数据即可。
这个方案平衡了写和读的性能。一方面写入时需要去定位 key 的具体位置,另一方面需要处理 write-write 冲突问题。
这个方案也有一些变种。比如说写入时先不去查找更新 key 的位置,而是先将这些 key 记录到一个 buffer 中,使用后台任务将这些 key 转成 DeleteBitmap。然后在查询的时候通过 merge on read 的方式处理 buffer 中的增量 key。
Upsert 和 Delete 使用示例
首先我们建了一张 UniqueMergeTree 的表,表引擎的参数和 ReplacingMergeTree 是一样的,不同点是可以通过 UNIQUE KEY 关键词来指定这张表的唯一键,它可以是多个字段,可以包含表达式等等。
下面对这张表做写入操作就会用到 upsert 的语义,比如说第 6 行写了四条数据,但只包含 1 和 2 两个 key,所以对于第 7 行的 select,每个 key 只会返回最高版本的数据。对于第 11 行的写入,key 2 是一个已经存在的 key,所以会把 key 2 对应的 name 更新成 B3; key 3 是新 key,所以直接插入。最后对于行删除操作,我们增加了一个 delete flag 的虚拟列,用户可以通过这个虚拟列标记 Batch 中哪些是要删除,哪些是要 upsert。
UniqueMergeTree 表引擎的亮点
对于 Unique 表的写入,我们会采用 upsert 的语义,即如果写入的是新 key,那就直接插入数据;如果写入的 key 已经存在,那就更新对应的数据。
UniqueMergeTree 表引擎既支持行更新的模式,也支持部分列更新的模式,用户可以根据业务要求开启或关闭。
ByteHouse 也支持指定 Unique Key 的 value 来删除数据,满足实时行删除的需求。支持指定一个版本字段来解决回溯场景可能出现的低版本数据覆盖高版本数据的问题。
最后 ByteHouse 也支持数据在多副本的同步,避免整体系统存在单点故障。
在性能方面,我们对 UniqueMergeTree 的写入和查询性能做了性能测试,结果如下图(箭头前是 ReplacingMergeTree 的消耗时间,箭头后是 UniqueMergeTree 的消耗时间)。
可以看到,与 ReplacingMergeTree 相比,UniqueMergeTree 的写入性能虽然略有下降,但在查询性能上取得了数量级的提升。我们进一步对比了 UniqueMergeTree 和普通 MergeTree 的查询性能,发现两者是非常接近的。
增强后的实施人群圈选
经过 UniqueMergeTree 的加持,在原有架构不变的情况下,完美的满足了实时人群圈选场景的要求。
1、通过 Unique Key 配置唯一键,提供 upsert 更新写语义,查询自动返回每个唯一键的最新值
2、性能:单 shard 写入吞吐可以达到 10k+行/s;查询性能与原生 CH 表几乎相同
3、支持根据 Unique Key 实时删除数据
此外,ByteHouse 还通过 UniqueMergeTree 支持了一些其他特性:
1、唯一键支持多字段和表达式
2、支持分区级别唯一和表级别唯一两种模式
3、支持自定义版本字段,写入低版本数据时自动忽略
4、支持多副本部署,通过主备异步复制保障数据可靠性
不仅在实时人群圈选场景,ByteHouse 提供的 upsert 能力已经服务于字节内部众多应用,线上应用的表数量有数千张,受到实时类应用的广泛欢迎。
除 Upsert 能力外,ByteHouse 在为原生 ClickHouse 的企业级能力进行了全方位的增强。
立即跳转火山引擎BytHouse官网了解详情!
评论