在特定场景里,Delta的upsert/delete性能会非常的关键。比如我最近一直在鼓吹的实时增量同步方案:
https://zhuanlan.zhihu.com/p/93744164
既然这个方案名称里提到了实时,那么更新速度就变得非常重要了。我在专栏里也介绍了Delta做Upsert/Delete的机制,大家可以翻阅下之前的文章。大致就是我们需要快速的找到需要被更新的记录在哪些文件里,一个方案是做join(delta-plus的默认实现),一个方案是使用布隆过滤器(delta-plus里的可选实现)。无论如何,他们本质上都是去做文件过滤的工作,尽可能减少找到被影响文件的的计算,以及尽可能减少需要被删除的和新增的文件。
join的话,计算量比较大,布隆过滤器有额外构建布隆过滤器自身的成本。但是无论他们有多少,如果待更新的数据分布在所有的文件里的话,那么他们基本上就没有价值了,因为这意味着所有的文件都需要被读取,排除掉需要被更新的,然后重新写入,本质上退化到了"全量删除再全量重建"这么一个极端情况了,而且前面的join/布隆过滤器额外带来了成本。从这里,我们可以知道,如何保证每个批次待更新的数据不会发生覆盖全表所有的文件的情况,是最最重要的一件事。在数据查询时,面临的问题是一致的,这里给出一篇延伸阅读:
https://zhuanlan.zhihu.com/p/93392187
所以,为了解决这个问题,我在文档里多次强调,一定需要设计数据的分布(在文件中的分区),一个比较简单的技巧就是通过partitionByRange来完成。 partitionByRange有两个参数,一个是fileNum,一个是用来构建区间的列。
文件数很重要,如果文件数是1,那么区间就没有意义了。如果文件数过大,而你的资源有限,意味着需要很多轮处理才能处理完数据,每一轮意味着额外的调度时间,打开文件,获取结果的时间。所以一个合适的文件数很重要。在增量同步的场景里,我们建议你的文件数是你核数的2的N次方。N可正可负。假设你有100cores,那么文件数可以是25,50,100,200,400等。接着,你需要拿全表数据量去除以你的文件数,得到每个文件包含的行数。一个文件通常被一个核处理,比如4000万rows,200个文件,那么一个文件20w, 一个核处理应该也会非常快,所以这个数字大致是可行的。
构建区间则更重要,这意味着每次更新会影响到多少个文件。比如,对于订单类的业务数据库,最新的订单会被创建,而老的订单会被锁定,基本不会更新。所以这个时候,我们选择订单时间作为range,那么每个文件都会覆盖一个时间区间。最新的数据可能分布在特定的几个文件里,这样Delta每次更新只会触发最近的几个文件的操作。这样相比"全量删除再全量重建",我们可能可以获得几倍几十倍甚至上百倍的性能提升。
通常而言,使用表的自增主键(或者联合主键)会是一个不错的选择。delta-plus 目前默认对每次受到更新影响的文件采用用户自定义的联合主键来进行分区。
所以,如果你是在流式计算里使用delta,并且使用到了更新删除操作,那么请务必在初始化delta表全量数据的时候,对数据进行partitionByRange操作。