[TOC] # 转储和合并 ## 转储(Minor Compaction) 当 MemTable 的内存使用达到一定阈值时,就需要将 MemTable 中的数据存储到磁盘上以释放内存空间,这个过程称之为“转储”。在转储之前首先需要保证被转储的 MemTable 不再进行新的数据写入,这个过程称之为“冻结”(Minor Freeze),冻结会阻止当前活跃的 MemTable 再有新的写入,并同时生成新的活跃 MemTable。 在对冻结 MemTable 进行转储时,会扫描冻结 MemTable 中的数据行,并将这些数据行存储到 SSTable 中,当一条数据被多个不同事务反复修改时,可能会有多个不同版本的数据行存储到转储 SSTable 中。 转储发生在 MemTable 的大小满足一定条件时,任何分区副本可以独立决定冻结当前 MemTable,并转储到磁盘上。转储出的 SSTable 只与相同大版本的增量数据做数据归并,不与全局静态数据合并。这样设计是基于增量数据远小于全局数据的考虑,使得转储会比较快。 ## 转储触发 转储有两种触发方式:自动触发与手动触发。 当一个租户的 MemTable 内存使用量达到`memstore_limit_percentage`时,就会自动触发转储。您也通过以下的运维命令手动触发转储。 ~~~ ALTER SYSTEM MINOR FREEZE [zone] | [server_list] | [tenant_list] | [replica] tenant_list: TENANT [=] (tenant_name_list) tenant_name_list: tenant_name [, tenant_name ...] replica: PARTITION_ID [=] 'partition_idx%partition_count@table_id' server_list: SERVER [=] ip_port_list ~~~ 示例: ~~~ /*集群级别转储 */ alter system minor freeze; /*Server 级别转储 */ alter system minor freeze server='192.168.1001:2882' /*租户级别转储 */ alter system minor freeze tenant='prod_tenant' /*Replica 级别转储 */ alter system minor freeze alter partition_id = '8%1@1099511627933' ~~~ 需要注意的是,尽管允许只针对单个分区手动触发 Minor Freeze,但由于多个不同的分区可能共用相同的内存块,因此对单个分区的 Minor Freeze 可能并不能有效地释放内存,而针对租户的 Minor Freeze 可以有效地释放对应租户 MemTable 的内存。 ## 合并(Major Compaction) 合并操作(Major Compaction)是将动静态数据做归并,会比较费时。当转储产生的增量数据积累到一定程度时,通过 Major Freeze 实现大版本的合并。 转储和合并的最大区别在于,合并是集群上所有的分区在一个统一的快照点和全局静态数据进行合并的行为,是一个全局的操作,最终形成一个全局快照。 转储和合并的对比如下表所示: <table data-tag="table" id="table-17j-uy2-8t8" class="table"><colgroup width="350" span="1" data-tag="col" id="col-sju-ivx-mwe" colwidth="1*" colnum="1" colname="col1" style="width:50%" class="col"></colgroup><colgroup width="372" span="1" data-tag="col" id="col-phn-ocb-qxh" colwidth="1*" colnum="2" colname="col2" style="width:50%" class="col"></colgroup><thead id="thead-dre-uh8-raj" class="thead"><tr id="tr-ibl-siw-mmm"><th id="td-dsv-vma-x7l"><p id="p-l1j-2ca-z73"><b>转储(</b>Minor Compaction<b>)</b></p></th><th id="td-eze-ydb-wcq"><p id="p-p80-a8r-2hb"><b>合并(Major </b>Compaction<b>)</b></p></th></tr></thead><tbody data-tag="tbody" id="tbody-zmi-8ql-m0j" class="tbody"><tr data-tag="tr" id="tr-u48-6mk-joa" class="tr"><td data-tag="td" id="td-xiz-njk-0ov" class="td"><p data-tag="p" id="p-2c6-z7q-kw8" class="p">Partition 或者租户级别,只是 MemTable 的物化。</p></td><td data-tag="td" id="td-gru-762-k96" class="td"><p data-tag="p" id="p-91a-8ny-31z" class="p">全局级别,产生一个全局快照。</p></td></tr><tr data-tag="tr" id="tr-vzr-mdg-kta" class="tr"><td data-tag="td" id="td-qlz-jyr-k63" class="td"><p data-tag="p" id="p-vnm-124-qo2" class="p">每个 OBServer 的每个租户独立决定自己 MemTable 的冻结操作,主备分区不保持一致。</p></td><td data-tag="td" id="td-w41-yrk-g9h" class="td"><p data-tag="p" id="p-d7k-tye-gvo" class="p">全局分区一起做 MemTable 的冻结操作,要求主备 Partition 保持一致,在合并时会对数据进行一致性校验。</p></td></tr><tr data-tag="tr" id="tr-xh5-05d-kze" class="tr"><td data-tag="td" id="td-3if-ozl-feu" class="td"><p data-tag="p" id="p-e53-a6c-ez0" class="p">可能包含多个不同版本的数据行</p></td><td data-tag="td" id="td-53t-mx0-gxv" class="td"><p data-tag="p" id="p-iul-9p8-m7q" class="p">只包含快照点的版本行</p></td></tr><tr data-tag="tr" id="tr-hyd-cg2-b0v" class="tr"><td data-tag="td" id="td-sxz-wjb-a7e" class="td"><p data-tag="p" id="p-a01-nv3-fc4" class="p">转储只与相同大版本的 Minor SSTable 合并,产生新的 Minor SSTable,所以只包含增量数据,最终被删除的行需要特殊标记。</p></td><td data-tag="td" id="td-xmk-zj2-r6x" class="td"><p data-tag="p" id="p-off-yer-uyc" class="p">合并会把当前大版本的 SSTable 和 MemTable 与前一个大版本的全量静态数据进行合并,产生新的全量数据。</p></td></tr></tbody></table> 合并有很多种不同的方式,具体的描述如下。 **全量合并** 全量合并是 OceanBase 数据库的一种合并算法,和 HBase 与 Rocksdb 的 Major Compaction 过程是类似的。顾名思义,在全量合并过程中,会把当前的基线数据都读取出来,和增量数据合并后,再写到磁盘上去作为新的基线数据。在这个过程中,会把所有数据都重写一遍。全量合并会极大的耗费磁盘 IO 和空间,如非必要或者 DBA 强制指定,OceanBase 数据库一般不会主动做全量合并。 OceanBase 数据库发起的全量合并一般发生在列类型修改等 DDL 操作之后。DDL 变更是实时生效的,不阻塞读写,也不会影响到多副本间的 Paxos 同步,将对存储数据的变更延后到合并的时候来做,这时就需要将所有数据重写一遍。 **增量合并** 增量合并是相对于全量合并而言的概念,同样是 OceanBase 数据库的一种合并算法。大多数情况下,当需要进行合并时并不是所有的宏块都需要被修改,当一个宏块没有增量修改时,直接重用它,而不是重写它,这种方式称之为增量合并。相对于全量合并的把所有的宏块的重写一边而言,增量合并只重写发生了修改的宏块。增量合并极大地减少了合并的工作量,也是 OceanBase 数据库目前默认的合并算法。 更进一步地,对于宏块内部的微块,很多情况下也并不是所有的微块都会被修改。当发现宏块有行被修改过时,在处理每一个微块时,会先判断这个微块是否有行被修改过,如果没有,只需要把这个微块的数据直接拷贝到新的宏块上,这样没被修改过的微块就省去了解析行、选择编码规则、对行进行编码以及计算列 Checksum 等操作。微块级增量合并进一步减少了合并的时间。 **渐进合并** 在执行某些 DDL 操作时,例如执行表的加列、减列、修改压缩算法等操作后,可能需要将数据重写一遍。OceanBase 数据库并不会立即对数据执行重写操作,而是将重写动作延迟到合并时进行。基于增量合并的方式,无法完成对未修改数据的重写,为此 OceanBase 数据库引入了“渐进合并”,即把数据的重写分散到多次合并中去做,在一次合并中只进行部分数据的重写。 通过以下命令可以控制一张表的渐进轮次: ~~~ ALTER TABLE [table_name] set default_progressive_merge_num=? ~~~ 例如: ~~~ obclient> ALTER TABLE mytest set default_progressive_merge_num=60; ~~~ 将表`mytest`的渐进轮次设置为`60`,当执行加列或减列操作后的 60 次合并过程中,每一次合并会重写 60 分之一的数据,在 60 轮合并过后,数据就被整体重写了一遍。 当未对表的`progressive_merge_num`进行设置时,其默认值为`0`,目前语义为在执行需要重写数据的 DDL 操作之后,做渐进轮次为 100 的渐进合并。当表的`progressive_merge_num`被设置为 1 时,表示强制走全量合并。 **轮转合并** 一般来说合并会在业务低峰期进行,但并不是所有业务都有业务低峰期。在合并期间,会消耗比较多的 CPU 和 IO,此时如果有大量业务请求,势必会对业务造成影响。为了规避合并对业务的影响。借助 OceanBase 数据库的多副本分布式架构,引入了轮转合并的机制。 一般配置下,OceanBase 数据库会同时有 3 个数据副本,当一个数据副本在进行合并时,可以将这个副本上的查询流量切到其他没在合并的集群上面,这样业务的查询就不受每日合并的影响。等这个副本合并完成后,再将查询流量切回来,继续做其他副本的合并,这一机制称之为轮转合并。 为了避免流量切过去后,Cache 较冷造成的 RT 波动,在流量切换之前,OceanBase 数据库还会做 Cache 的预热,通过参数可以控制预热的时间。 **合并触发** 合并触发有三种触发方式:自动触发、定时触发与手动触发。 * 当集群中任一租户的 Minor Freeze 次数超过阈值时,就会自动触发整个集群的合并。 * 也可以通过设置参数来在每天的业务低峰期定时触发合并。 ~~~ obclient> ALTER SYSTEM SET major_freeze_duty_time = '02:00'; ~~~ * 也通过以下的运维命令手动触发合并。 ~~~ obclient> ALTER SYSTEM MAJOR FREEZE; ~~~