破解OceanBase存储引擎读放大难题的办法
首先给大家推荐一下OceanBase开源负责人老纪的公众号“老纪的技术唠嗑局”,这里会持续更新和#数据库、#AI、#技术架构相关的各类技术内容。感兴趣的朋友可以关注哦!
前言
OceanBase的LSM-Tree存储引擎本身有着高效的写入性能,它既能通过旁路导入来高效处理定期的批量数据同步,还能承接一些实时数据同步以及历史库数据修改的场景。
但任何事物都有两面性,LSM-Tree存储引擎虽然对写操作比较友好,可在数据读取的时候,必然会绕不开多级数据归并带来的读放大问题。
最近一段时间,在社区论坛和微信群里经常出现这样的情况:在高频插入、删除和更新的表中,读取性能没有达到预期。这篇文章就简单跟大家介绍一下应对的办法。
背景:OceanBase的存储引擎架构
OceanBase的存储引擎是基于LSM-Tree架构的,它把数据分成基线数据(放在基线SSTable里)和增量数据(放在MemTable/转储的SSTable里)两部分。其中基线数据是只读的,一旦生成就不会再被修改;增量数据支持读写。
OceanBase数据库的DML操作,像插入、更新、删除等操作,首先会写入内存里的MemTable,所以在写入性能上就类似于内存数据库的写入性能。当MemTable达到一定大小的时候就会转储到磁盘成为增量的SSTable(就是上图中的转储SSTable部分),转储到磁盘上的过程是批量的顺序写,和B+树架构离散的随机写相比,会大大提高写盘的性能。
当增量的SSTable达到一定规模的时候,就会触发增量数据和基线数据的合并,把增量数据和基线数据做一次整合,基线数据在合并完成之后就不会再变化了,直到下一次合并。而且每天凌晨业务低峰期的时候,系统也会自动进行每日合并。
不过LSM-Tree的架构也存在一个问题,就是读放大(上图中右侧箭头向上的部分)。在进行查询的时候,需要分别对SSTable和MemTable进行查询,然后把查询结果归并一次,再把归并后的查询结果返回给SQL层。OceanBase为了减小读放大带来的影响,在内存里实现了多级缓存,比如Block Cache和Row cache,来避免频繁对基线数据进行随机读。
问题:Buffer表效应
当用户在某张表上频繁执行插入并且同时进行批量删除,或者有大量的并发更新操作时,可能会出现这样一种现象:表中的数据行数并不大,但是查询和更新的性能明显下降。这种现象在OceanBase中被称为Queuing表(业务上有时候也叫Buffer表)效应。
Buffer表是LSM-Tree架构数据库都得面对的一类问题。在LSM-Tree架构下,删除操作在合并之前都只是逻辑上标记删除,并不是物理删除,当增量数据里存在大量标记删除的数据时,物理行数量会远多于逻辑行数,从而会严重加剧存储引擎的读放大现象,还会影响优化器对最优计划的选择。
分析:通过explain extended_noaddr
分析方法很简单,用EXPLAIN EXTENDED_NOADDR命令对SQL进行详细的计划展示就行(通常用户排查SQL性能问题的时候,会用这种展示模式)。需要重点关注physical_range_rows和logical_range_rows的值。
上图中,physical_range_rows和logical_range_rows分别表示t1表需要扫描的物理行数和逻辑行数。要是走了索引的话,意思就是t1表在索引上需要扫描的物理行数和逻辑行数。
一般情况下,physical_range_rows和logical_range_rows这两个指标是差不多的,看任意一个都行。只有在这个特殊的Buffer表(频繁更新)场景下,physical_range_rows可能会远大于logical_range_rows,所以可以通过physical_range_rows和logical_range_rows的数值来判断是不是出现了Buffer表问题。
比如说我向t1表插入33行数据,马上删除29行,那在增量数据里就会有33个物理行,其中29个物理行有Delete标记,查询真正的4个逻辑行的时候,就会出现大量无效扫描动作。
应对:Buffer表自适应合并优化
为了更灵活地解决Buffer带来的性能下降问题,从OceanBase 4.2.3开始,为大家带来了Buffer表自适应合并优化特性。
这个特性允许用户根据业务场景给每张表设置不同的表级配置项table_mode
来指定不同的快速冻结与自适应合并策略,以此应对Buffer表引起的读放大现象,从而提高系统长期运行下的QPS等性能指标。
针对Buffer表问题,OceanBase提供了5种档位的Table Mode支持。不同的Table Mode对应不同的统计信息阈值与合并策略。存储层每次转储的时候都会根据转储的统计信息以及Table Mode对应的阈值来判断是否需要执行一次针对Buffer表场景的特殊合并,把增量数据里的所有Delete标记消除掉,避免原有的大量无效扫描动作。
Table Mode | 转储后触发合并阈值 | 转储后触发合并概率 | 转储后合并类型 |
---|---|---|---|
Normal(默认值) | 较高(为基准1.0 ) | 极低 | 仅做Medium Compaction |
Queuing | 高(0.9) | 低 | |
Moderate | 中(0.8) | 中 | 优先做Medium Compaction, 若Medium冷却中,做Meta |
Super | 低(0.6) | 高 | |
Extreme | 极低(0.5) | 较高 | 仅做Meta Compaction |
备注:
合并是OceanBase数据库将动静态数据做归并的行为,能有效消除增量数据
- Medium Compaction是一类触发链路较长,需要保证多副本上Major一致性的合并,合并较慢 ;
- Meta Compaction是由各个Observer主动发起,生成只读Meta SSTable的合并,合并较快 ;
简单来说,默认场景(Normal)下转储后发起合并的门槛比较高(比如觉得转储中删除行超过30万才判断可能是Buffer表场景),所以转储后触发合并概率比较低,触发的也是合并速度较慢的Medium Compaction。
而随着Table Mode逐步上调,转储后发起合并的门槛逐渐降低,触发合并的概率也逐渐提高,并且更倾向于做合并速度较快的Meta Compaction,从而能及时通过合并消除增量数据,避免Buffer表带来的大量无效扫描。
具体来说,不同参数下的策略可以分为如下三个模块:
触发快速冻结
对创建时间>存活时间阈值,且memtable有热点行(不在本次优化考虑内)或墓碑现象的触发快速冻结:
Table Mode | 存活时间阈值 | 墓碑现象阈值 |
---|---|---|
Normal(默认) | 120s = 120s * 1.0 | (更新行 + 删除行)> 25w = 25w * 1.0 |
Queuing | 108s = 120s * 0.9 | (更新行 + 删除行)> 22.5w = 25w * 0.9 |
Moderate | 96s = 120s * 0.8 | (更新行 + 删除行)> 20w = 25w * 0.8 |
Super | 72s = 120s * 0.6 | (更新行 + 删除行)> 15w = 25w * 0.6 |
Extreme | 60s = 120s * 0.5 | (更新行 + 删除行)> 12.5w = 25w * 0.5 |
备注:
存活时间阈值的120s为4.2的阈值。在4.3及以上版本,此阈值为300s。
转储时收集统计信息并尝试触发分区合并
冻结后紧接着会做一次转储,若转储同时满足如下条件,将会进行tablet级别的信息汇报:
- dml数量 > 1000
- insert + update + delete行数 > 1000
当然,tablet信息的汇报并不止于转储,一些慢查询场景也会做相应处理,只是不在本任务范围内。
当memtable超过预设内存阈值或快速冻结触发后都会执行一次转储。如果触发了快速冻结,意味着墓碑现象可能已经出现了,因此根据上述快速冻结策略的墓碑策略再判断一次,如果命中,直接发起一次合并。
分区合并调度时调度分区合并
分区合并的调度由一个独立的调度线程负责,基本逻辑是通过自适应合并的策略来判断某个tablet是否需要执行一次分区合并。相应地,也会根据不同的table mode调整自适应策略。
与table mode有关的自适应合并策略如下:
备注:
这里引用一下OB官网的 自适应合并 内容:
其中第三点:根据统计增量数据的总行数这个并不受table mode控制,增量行数的阈值是超过10w或者增量行达到极限70%,对于非Normal表,大概率已经触发合并了。
导数场景
Table Mode | 导数场景条件(需同时满足) |
---|---|
根据统计的10分钟内信息 | |
热点tablet | |
且操作行数满足 (操作行数 = insert + delete + update) | |
Normal(默认) | 查询次数 + 转储次数 > 5 = 5 * 1.0 |
Queuing | 查询次数 + 转储次数 > 4.5 = 5 * 0.9 |
Moderate | 查询次数 + 转储次数 > 4 = 5 * 0.8 |
Super | 查询次数 + 转储次数 > 3 = 5 * 0.6 |
Extreme | 查询次数 + 转储次数 > 2.5 = 5 * 0.5 |
墓碑场景
Table Mode | 墓碑场景条件(需同时满足) |
---|---|
当转储次数 ≥ 2 | |
且根据统计的10分钟内信息 | 操作行数 = insert + delete + update |
Normal(默认) | 操作行数 > 1w = 1w * 1.0 |
Queuing | 操作行数 > 9k = 1w * 0.9 |
Moderate | 操作行数 > 8k= 1w * 0.8 |
Super | 操作行数 > 6k = 1w * 0.6 |
Extreme | 操作行数 > 5k = 1w * 0.5 |
与此同时,为了防止10分钟内的信息不够全面,统计了tablet自上次medium/meta时的删除行总数,并有满足如下条件时触发一次合并。
Table Mode | 总删除行数 |
---|---|
Normal(默认) | 总删除行 > 30w |
Queuing | 总删除行 > 20w |
Moderate | 总删除行 > 10w |
Super | 总删除行 > 5w |
Extreme | 总删除行 > 1k |
但是值得一提的是, 总删除行这个条件属于锦上添花类型,类似于cache,很有可能未满足条件时tablet统计信息就被别的tablet刷掉了。同时,按照最宽松的10分钟内1w的条件,如果每个10分钟内信息都是9999行,要达到30w的阈值也得近300分钟,作用是更多是在tablet数量少的时候留下来兜底。
低效读场景
Table Mode | 低效读场景条件(需同时满足) |
---|---|
根据统计的10分钟内信息 | |
热点tablet | |
Normal(默认) | 查询次数 + 转储次数 > 5 = 5 * 1.0 |
Queuing | 查询次数 + 转储次数 > 4.5 = 5 * 0.9 |
Moderate | 查询次数 + 转储次数 > 4 = 5 * 0.8 |
Super | 查询次数 + 转储次数 > 3 = 5 * 0.6 |
Extreme | 查询次数 + 转储次数 > 2.5 = 5 * 0.5 |
使用指南
创建表时指定Table Mode
用户可以在创建表的时候就显式指定Table Mode,要是不指定的话默认就是Normal模式。
# 语法
create table t1 (c1 int) table_mode = 'normal/queuing/moderate/super/extreme';
# 创建一张queuing模式的表
create table t1 (k int, v double) table_mode = 'queuing';
# 创建一张extreme模式的表
create table t1 (k int, v double) table_mode = 'extreme';
修改表的Table Mode
用户可以通过DDL语句显式修改Table Mode
# 语法
alter table t1 set table_mode = 'normal/queuing/Moderate/Super/Extreme';
# 修改表的table_mode为moderate
alter table t1 set table_mode = 'moderate';
比如说,当用户观测到某张表的读放大现象严重,出现Buffer表现象的时候,可以根据业务量规模与特点手动把该表的Table Mode调到更高档位,比如从normal调整到queuing,从queuing调整到super等。当Table Mode在存储层生效后,将会通过调度自适应合并的方式发起合并来解决Buffer表现象。
与此同时,更激进的Table Mode会更频繁地发起合并,从而消耗更多的计算资源,如果此时业务体量或场景能够容忍一定程度的Buffer表效应,也可以手动调低数据表的Table Mode。
Table Mode生效说明
值得说明的是,尽管Table Mode在Table Schema中是实时生效的,但是在存储层后台对tablet信息的统计是延迟更新的(2分钟刷新一次),所以当修改表的Table Mode后,平均得经过60s才能生效。
另外,存储层后台对每个租户下转储统计信息的覆盖范围是有限的,由于资源有限,并不是所有tablet上的转储信息都会被统计到,只有转储时满足某些特定条件的tablet
文章整理自互联网,只做测试使用。发布者:Lomu,转转请注明出处:https://www.it1024doc.com/12917.html