背景
在大数据场景下,我们常常会在传统的OLAP产品中进行视图预计算、数据挖掘、模型训练等过程,生成大量标签、纬度数据。这些结果数据需要向实时的线上业务提供低rt的查询服务,目前通用的做法是将这些结果数据批量导入到Hbase、Cassandra、Lindorm这些产品中做Serving层加速,这里的批量导入过程在大数据体系内是非常常见的。
很多场景中,比如名单表,营销活动表,人群标签表重刷等,需要用最新计算的数据覆盖掉全部的老数据,此时业务会对批量导入提出全量覆盖的需求。举个例子,假如用户a的一行记录有c1 = 1,c2 = 2,c3 = 3三个数据,导入c1 = 5, c3 = 6,希望c1和c3被更新,同时c2 = 2也被覆盖不再可见。
一般来说,业务对全量覆盖的批量导入过程常常会有以下要求:
1. 全量替换
新数据能够自动覆盖掉所有老的数据。
2. 数据一致性
新导入的一批数据尽可能同时可见,不一致的中间状态过长可能会有业务影响。一批数据导入最好具有原子
性,要么一起成功,要么一起回滚成老的数据,如果任务失败,脏数据进入到线上系统可能产生严重的后
果。
3. 数据恢复
如果上游出现计算错误,希望可以快速回退到全量覆盖之前,甚至几个版本之前的数据,作为系统的兜底能
力。
4. 速度
导入的速度要尽可能快,导入的速度直接决定了线上服务的效果和体验。
5. 稳定
导入的大量数据,覆盖老数据的同时,不能影响线上实时服务的稳定性。
当前痛点
目前有很多优秀的产品可以做数据的批量导入导出,但是在全量覆盖场景下,依然有很多痛点无法得到很好的解决:
1. 全量覆盖实现困难
覆盖掉老的数据一般有2种选择:
- truncate 表
最直观的,导入之前先调用类似truncate的接口清空线上表,然后再开始导入,可以达到要求的效果,导入
ODPS时就有这样的配置。但是问题也很明显,清空表之后,线上服务无数据,在线服务都无法接受。 - AB表切换
复杂一点的,用户侧控制AB表的切换,导入时写B表,用业务层逻辑控制新老数据表的切换。这里会大大增加业
务逻辑的复杂度,对上层侵入很多,在有些系统中,AB表的改造方案几乎不可行。在业务层控制表切换,切换的
一致性和容灾能力也很难保证保证,会增加更多不稳定因素,降低系统整体的可用性。
2. 数据一致性差
以DataX(一款阿里研发的非常流行的数据同步产品)为例,很多DataX的插件都是逐行写入,逐行可见,整个任务持续的过程,长时间处于新老数据不一致的中间状态,任务完成后,全部数据才完整可见。而且无原子性保障,逐行写入的数据直接进入线上系统,如果任务失败,脏数据无法回滚。
3. 数据无法快速恢复
普通的导入模式,数据进入系统后,历史数据被覆盖掉就无法再追回,一旦上游计算错误,只能重新导入覆盖,有些系统的逻辑下,甚至需要重跑完整的上游链路,错误的恢复速度基本需要数小时甚至天级别。
4. 速度
速度较慢,直写需要走完整的写入流程,占用线上系统写入吞吐,速度取决于线上系统的资源。
5. 稳定性
对线上系统稳定性有负面影响,批量导入如果速度过快,会有打爆线上系统的危险。被覆盖的老数据需要额外的compaction操作才能消化,这些都会带来额外的稳定性风险。
我们的方案
铺垫了这么多(why应该说清楚了),我们来看一下how的问题,我们的方式是如何解决上面这些痛点的呢?下文会涉及到一些Lindorm内部的原理和细节,感兴趣的同学欢迎交流。
1. 写入方式
首先与其他方案使用数据库系统的写入接口导入不同,本方案使用Bulkload的方式进行写入。Bulkload导入即跳过正常接口的RPC,MVCC,WAL等过程,直接对原数据集进行编码压缩生成底层文件,然后全部的文件一起旁路加载到在线数据库系统中。
这样的方式写入链路简单直接,速度快一个数量级。并且在离线集群进行编码压缩等消耗cpu的操作,旁路加载文件,对线上集群的服务节点几乎无影响,可以最大程度保障海量数据流入场景下的线上系统稳定性
2. 全量覆盖的实现
在这种写入方式下,我们可以通过数据库系统的表重定向功能,实现全量替换操作。数据导入过程写入一个全新的 表,写入完成后进行切换操作。可以简单理解成一个更强大的、native的AB表切换功能。
2.1 依赖表重定向的全量覆盖导入过程
如图所示,依赖表重定向的全量替换导入过程如下:
a. bulkload导入过程中创建一个新的用户不可见表版本v3,导入过程依赖Bulkload的特性,对当前服务的版
本v2不造城任何影响。
b. v3版本写完之后,调用表的原数据接口,可以在非常短的时间内,将访问v2的访问,原子性的切换到新的v3版
本
c. 写入完毕后可以异步的选择清理历史版本,并且如果新版本的表出现问题(如数据错误),支持回退到历史版
本。
为了达到上述的全量替换方案,数据库系统的表重定向能力是关键,因此本方案设计了一种数据库表重定向能力的实现。
2.2 核心概念
实体表:指向一张逻辑表下的一个物理实表
逻辑表名:指向一个逻辑的主表,可能包含多个实体表
实体表的状态机:
○ active:当前生效的实体表,唯一,一定存在,ddl/dml都会对此实体生效
○ building:当前正在构建数据的实体表,新create的实体表具有此状态。
○ archived:已回档的实例实体表,将building状态的实体表至为active时,之前的active实体会被 置为归档状态。归档状态的实体表可以随时被切换为active。
○ disabled:已禁用,准备删除的实体表。只有此状态的实体表可以被删除
说明:
• building -> active是单向的
• 新建的实体默认是building状态,完成初始数据构建之后,显式切换为active。过去的active实体自动变成archived
• archived实体可随时切换为active,以启用历史版本
• archived实体也可以切换为building,以对历史版本进行数据构建(如数据订正场景,但不推荐)
• building实体如果不想要了,可以直接archive,然后offline,再删除(3步操作)
• 存量表在创建第一个实体表时,即开启此功能
• active实体一定存在,且保证唯一,但其他状态的实体可以不存在,或者有多个
2.3 支持的重定向切换模式
模式 | 约束 | 优缺点 |
---|---|---|
强一致切换 | 对所有的业务client来说,要么访问(读写)老实体,要么访问新实体 | 切换过程中,存在不可访问的时间窗口。表越大,集群规模越大,窗口期越长,秒级 |
最终一致切换 | 读写都不做任何保证,都可能会同时访问到新老实体的数据。同一个client也会如此 | 业务对切换操作几乎无感知(读操作因切换新表,缓存命中率低,可能会有RT升高的问题) |
总结
按照前文中对比的维度,这个方案可以达到的效果:
- 全量覆盖
表重定向到新表之后,全量覆盖功能自然实现,无需任何删除操作。旧的数据在旧的实体表中,可以异步的直接全表删除,不需要额外付出一次LSM tree结构的compaction操作。 - 一致性
有强一致和最终一致两种可选的一致性策略:强一致以秒级的禁读写代价,可以保证新老数据无交叉的强一致切换;最终一致在用户无感的情况下可以最终保证全部切换成新数据,并且新老数据交叉的中间过程通常只在秒级,对比普通模式整个任务过程都不一致,有本质区别。新数据都在新版本的表中,表重定向切换操作有原子保障,且支持失败回滚,可以确保无脏数据进入线上系统。 - 数据恢复
支持秒级一键回切历史版本,保留的历史版本数量也可以按需配置,真正成为用户数据的“后悔药”。 - 速度
具备Bulkload的优势,链路短,速度快一个数量级,这一点Lindorm Bulkload的老用户们应该都有体会了。 - 稳定性
具备旁路导入的优势,离线集群生产数据文件,不占用线上集群资源,几乎无影响。且老数据无需compaction消化,大幅节约系统资源开销。
该功能目前已经正式发布,已有不少业务同学正在测试、上线中,欢迎使用~