应对 “读放大” 问题的新方法 —— OceanBase 中的 Merge-On-Write 表

背景
OceanBase 从 4.3.0 版本开始,推出了加速 AP 查询的列存引擎,具体包含:
新的列式编码
列预聚合信息
列存执行引擎
向量化内存格式
新的查询优化器,能根据规则和代价动态选择行存和列存引擎。
OceanBase 列存引擎发布之后,AP 分析能力得到了大幅提升,在与一系列竞品的对比测试中都有比较好的表现,正式踏入了 HTAP 领域。
为了节省存储成本和简化用户的运维,OceanBase 将 TP 和 AP 业务放在一套系统中,共享一份数据。实际的业务场景中,尤其是核心业务,往往有大量数据更新,这些业务也需要对数据做一些实时的数据分析,因此对 OceanBase 分析引擎提出了比较高的挑战。
在实战营(第三季)第一期的课程文档中,大家知道 OceanBase 采用的是 LSM Tree 的存储架构,数据分层存储,新数据追加写入最热层,并且只记录最新值。这种写入方式对 TP 系统比较友好(写多读少),但是因为查询采用的是 Merge-On-Read 的处理方式,所以当增量数据比较多时,对 AP 分析能力有一定的影响(读放大)。
为了消除增量数据对分析引擎的性能影响,OceanBase 在 4.3.5 版本中提出了 Merge-On-Write 表,将更新拆分成 delete / insert 写入增量数据中,查询时分别对增量和基线数据进行处理,从而显著地提高了更新频繁场景下 OceanBase 的实时分析能力。
Merge-On-Write 表特性
OceanBase 存储介绍
先简单来回顾和复习一下:
OceanBase 采用的是 LSM Tree 的存储架构,每张表的每个分区中,数据在磁盘上存储的逻辑单位是 SSTABLE,LSM Tree 的每层数据包含一个或者多个 SSTABLE,每个 SSTABLE 内部数据按主键顺序排列。
基线 SSTABLE 也被称为 Major SSTABLE,主要用来优化查询。基线 SSTABLE 是选取某个快照点,对提交版本在快照内的数据做全量合并之后生成的 SSTABLE。
为了优化写 IO,SSTABLE 内数据按固定 2MB 大小切割成宏块(MacroBlock),宏块是数据文件写 IO 的基本单位。
为了优化读 IO,每个宏块内的数据按 16KB 切割成一个个微块(MicroBlock),微块是数据读 IO 的基本单位。
Merge-On-Read 表查询及其痛点
OceanBase 当前存储架构中,为了优化写入性能和节省存储空间,增量 SSTABLE 只记录主键和更新列的值。
查询时为了获取主键的最新值,需要依次读取 MemTable、增量 SSTABLE、基线 SSTABLE,融合每层读到的数据获取完整行,这种在查询时对数据进行融合的流程叫做 Merge-On-Read。
Merge-On-Read 能较好地支持按主键点查,但是对于范围查询不太友好。当各层 SSTABLE 之间主键存在重叠和交叉时,对于每层 SSTABLE 吐出的行,需要用败者树(最小/大堆)来对各层吐出的数据进行融合和去重,这样执行引擎只能按行来计算下压谓词和投影,整体处理性能不高。
在当前版本中,OceanBase 对这种处理方式做了比较多的优化,例如在查询过程中能动态判断某层 SSTABLE 的宏块/微块内的数据与其它 SSTABLE 内的数据是否有交叉。如果某个宏块、微块内的主键和其它的微块没有交叉,说明这批主键只在这个宏块、微块内存在,可以不需要走败者树,只对这个 SSTABLE 进行扫描。
说明:上面这段内容,不理解也无妨。
大意就是说 OceanBase 存储引擎有一些实现上的优化,来缓解读放大的问题。
Merge-On-Read 查询流程中实现单边扫描优化之后,虽然 AP 能力得到了一定的增强,但是增数数据较多时,尤其增量与基线之间主键存在大量交叉时,整体的查询性能仍然会受到一定影响。
Merge-On-Write 表查询流程
与 merge-on-read 不同,merge-on-write 能更好地解决对基线数据做大量更新之后的查询性能问题。
为了不影响查询的性能,merge-on-write 会将更新数据的修改,移到写入阶段。业界中常用的做法是在旧行上标记 delete bitmap,然后在尾部追加写入新行。查询时,只需要读取原始数据和相应的 delete bitmap,便可以对相同主键的数据进行去重,获取最新值。
OceanBase 也参考了业界的经典方案,添加了 merge-on-write 表类型,将查询的部分热点的 merge 动作,前置到写入模块。
但考虑到 HTAP 业务中数据的更新量比较大,OceanBase 需要支持的业务比较复杂,需要维护每个快照点的多版本信息,这样对于 delete bitmap 的多版本信息维护将是一个比较大的挑战,并且在数据部分,OceanBase 已经有了一套比较成熟的 MVCC 多版本管理机制。
因此 OceanBase 对 merge-on-write 方案做了一些改进,写入时更新行会将 update 改写成 delete + insert,update 时会读出旧值,将融合之后的最新行的全列数据 insert 写入 memtable(即下图中的 mini),每行数据都会记录其提交版本信息。
OceanBase merge-on-write 表的查询过程中,将增量数据和基线数据分成了两个部分,而不是像原来一样放入一棵败者树中进行比较。
扫描时先选取提交版本号大于基线的增量数据进行扫描,计算下压谓词,再做投影。
若投影时,发现增量数据有对基线数据做修改,会将相应的更新信息缓存住,扫描基线时会跳过相应的主键行。相对于原有查询流程的主要优化点是:
增量部分存储了全列信息,可以预先计算下压谓词;
增量数据和基线数据不在一棵败者树中,当增量数据被下压谓词过滤之后,能大幅减少基线数据跳重复行的次数,从而大幅提高了增量和基线的批处理能力。
说明:
上面这一大段的内容,不理解也无妨。
大意就是说:在 MOW 表中,memtable 中不像以前一样,只存主键和被更新的列,而是会存储最新行的全部列的数据。这样在一定程度上,可以减少查询时增量数据和基线数据归并的实现复杂度,进而实现更好的查询优化。
Merge-On-Write 表的实验数据
OceanBase merge-on-write 表相对于 merge-on-read 表的区别主要是:
写入时会将 update 拆解为 delete + insert,insert 行会写入所有列的最新值。
增量部分原来因为没有填充旧值,不能预先计算下压谓词,需要将各层数据融合之后才能计算过滤条件;写入全列之后,增量部分也可以直接计算过滤条件。
原来增量数据和基线数据的查询会用一棵败者树,当有主键交叉或者重叠时,弱化了基线数据的批处理能力。在查询流程中将这两部分拆开之后,基线数据的批处理只有在过滤之后仍有 delete 行的条件下,才会按批去做主键去重,基线的批处理能力得到了很大的增强。
说明:
上面这一大段的内容,不理解也无妨。因为涉及到了底层实现,所以大家不用害怕,也不必花时间深究。
后面的这一小部分 “适用场景” 和 “使用方法”,才是本期课程中希望大家了解的重点内容(精华内容总是被浓缩的状态)~
详细的实验数据,先不在这里放了,推荐大家到本文最后的 “边学边练,效果拔群” 部分,通过在线体验来获取吧~
Merge-On-Write 表的适用场景
merge-on-write 表适用于 HTAP 和 AP 类的分析场景。如果是 TP 场景,则不建议开启,因为 merge-on-write 表的增量数据会记录查询融合后的全行信息,影响数据更新效率和存储空间。
业务中有大量数据更新并且需要实时做复杂的分析查询时,可以指定表的类型为 merge-on-write 表。
Merge-On-Write 表的使用方法
OceanBase 中可以指定 merge_engine = delete_insert 来使用 merge-on-write 表,它在 4.3.5.3 及以后的版本生效。
下面是具体的使用方法:
创建表时指定。
merge_engine = delete_insert : 表示采用 merge on write 的写入和查询模式;
merge_engine 不指定或者指定 merge_engine = partial_update,仍采用以前部分列更新的流程,即 merge-on-read 的模式。
使用示例:
租户配置项中指定。
如果不想修改业务代码,OceanBase 也支持指定租户新建表的默认 merge_engine。
租户配置项只在建表语句中没有显示指定 merge_engine 的情况下生效,若建表语句中显式指定了表的 merge_engine,则按建表语句中指定的格式进行解析。
未来展望
merge-on-write 表的增量数据包含了数据的全行信息,在生成增量 SSTABLE 时,对于频繁查询的列,可以在 SSTABLE 的中间层生成 skip index 预聚合信息(详见:列 Skip Index 属性[1])。
说明:
加上 skip index 之后,OceanBase 的 AP 性能,就可以直接上桌去和 StarRocks 等纯 AP 数据库掰掰手腕了。
处理增量数据时,可以像基线数据一样,基于 skip index 对访问数据进行裁剪。当增量 SSTABLE 中访问的宏块、微块数据可以用 skip index 进行裁剪时,整体的查询性能会有数量级的提升。
关于增量 SSTABLE 自适应添加 skip index,会在下一版本对外发布,欢迎大家持续关注~ (听寒晖和蒲华说,下一个版本,性能会在现在的基础上,继续再翻上个几倍。算了,这个秘密还是不提前泄露出去了)
What is more ?
如果您现在使用的 OceanBase 版本是低于 4.3.5 的老版本,可以考虑通过调整 table mode 这种更传统的方法,来缓解存储引擎的读放大问题。
具体方法详见 OceanBase 社区公众号上的这篇文章 ——《在 OceanBase 中,如何应对存储引擎的读放大问题?》。
Q & A
最后在这里记录用户在阅读之后,在技术交流群里提出的问题,以及群友的回复:
问: LSM Tree 架构下,对一个非主键字段 c1 一直更新不同的值, 那随着不断更新,select c1 from tab 的效率是不是一直在变慢?
答:没合并前,如果持续更新,理论上是会一直变慢的。
问:但我看 OB 在 trans node 的变更链上一直是在头上添加新增的最新值,为什么会原来越慢?
答:因为每次修改记录的都是变化的数据,你只改 c1,它就只记录 c1 的最新值。而查询往往要查询很多列,查询数据需要合并多个结果集。LSMTree 读就是这样,不只是 OB 这样。
问:查询其它列慢我想通的。每次只查 c1 列还会变慢不太理解。
答:如果只是 get(例如走主键)是不会影响的,buffer 表针对的是 scan 的查询慢。如果中间的多版本行或者 delete 行比较多,扫描的过程中需要跳过比较多的无效行。跳的无效行越多,查询就会越慢。
对本文内容有任何疑问的朋友,欢迎到评论区留言提问,小编会第一时间回复大家!(不限于 MOW 表和 buffer 表,其他任何和 Oceanbase 有关的问题都可以。知无不言,言无不尽~)
Commercial Break
0x00. 边学边练,效果拔群!
实战营中 MOW 表性能提升的在线体验地址:《Delete-Insert 存储引擎》[2]。
常规生产环境的测试结果:merge-on-write 表,查询性能相对于 merge-on-read 表,会有 5 倍以上的提升。
这个实验有导入数据(大概要 100 秒左右)之类耗时较长的操作,大家可以一次性把实验文档所有 SQL 语句全都复制到右边的实验环境里去执行,然后一边等待最后的实验结果,一边阅读下面的文章内容。
在实验环境是在纯列存表上做了一个小规模数据的测试,在这个测试中,性能差异可能没有那么明显(不过 2 ~ 3 倍应该还是有的,为确保测试准确性,建议在实验环境中多次重复执行最后的性能对比 SQL,并取平均值)。实际在更大规模数据或更高并发场景下(生产环境),才能真正体现出 merge-on-write 表的优势。
文档不可尽信,实践才出真知。
OceanBase 官网文档《创建表》[3]中的创建 MOW 表的语法如下(截至 2025.10.22):
语法中的 WITH COLUMN GROUP,看上去是建 MOW 表的必选项,所以貌似只有列存表和行列混存表才能被设置 MOW 属性。
大家可以试一试,去掉 WITH COLUMN GROUP xxx,看看能不能把纯行存表也设置为 MOW 表?
大家还可以尝试看看能不能把实验环境中最初的两个建表语句改成纯行存表,然后再对比一下行存 MOW 表相比普通行存表的性能提升(多次执行查询,取平均值),看看能不能达到蒲华大佬宣称的性能提升 5 倍以上?
小编的猜想:
因为行存表的查询性能一般会比列存表更差,所以如果行存表可以设置 MOW 属性的话,性能提升幅度大概率也会比列存表更显著。(换句话说就是:进步快,源于起点低~ 🤣)
大家可以在实验环境中通过测试,来验证下这个家伙的猜想是否正确?
如果不正确,可以再思考下为何不正确?
课后小测地址:【DBA 实战营】 Merge-On-Write 表[4],在第三季的实战营活动中,每通过一个课后练习,就会自动获得 10 个社区积分,并获得一次抽奖的资格。抽奖时有机会获得实体礼物或更高额的积分奖励。
小提示:
需要先登录 OceanBase 账号,才能初始化屏幕右边的实验环境进行实验。
在实验环境里,干什么都可以。大家不要受限于屏幕左边的实验手册,可以天马行空地做一些你感兴趣的事情,或者验证一些你对 OceanBase 官网文档的疑问、以及自己的猜想等等。
欢迎大家平时在学习 OceanBase 的过程中,也都能充分利用在线体验页面为您提供的一些实验环境,来体验 OceanBase 中您感兴趣的新特性。
参考资料
[1]
列 Skip Index 属性: https://www.oceanbase.com/docs/common-oceanbase-database-standalone-1000000003577906
[2]
《Delete-Insert 存储引擎》: https://www.oceanbase.com/demo/delete-insert-engine
[3]
《创建表》: https://www.oceanbase.com/docs/common-oceanbase-database-standalone-1000000003577967
[4]
【DBA 实战营】 Merge-On-Write 表: https://exam.oceanbase.com/pre?did=ha_vN5Zs3SP_5KD378wV1
版权声明: 本文为 InfoQ 作者【老纪的技术唠嗑局】的原创文章。
原文链接:【http://xie.infoq.cn/article/8a47baf7dc7f3520736dcbf85】。文章转载请联系作者。







评论