本文主要探索 OB 的LSMTree写放大和合并特点、数据文件空间特点、SSD的空间管理和写放大特点。



内存中的MEMTABLE


接前面文章《OB 内存分配概述》,里面总结了 OB 会尽可能的将数据增量缓存在内存中。这部分数据又叫 MEMTABLE,在内存中按分区粒度管理,通过 BTree 组织在一起。这部分数据可以通过视图 gv$memstore_info 查看。以(table_id, partition_id)作为条件去定位一个具体的表(分区)的 MEMTABLE 信息。


select table_id, PARTITION_ID ,version,BASE_VERSION ,MULTI_VERSION_START ,SNAPSHOT_VERSION ,IS_ACTIVE ,round(used/1024/1024) USED_MB, HASH_ITEMS ,BTREE_ITEMS
from `gv$memstore_info` gmi
where table_id in (1100611139453777, 1100611139453779)
order by table_id, PARTITION_ID , BASE_VERSION ;
+------------------+--------------+---------+------------------+---------------------+---------------------+-----------+---------+------------+-------------+
| table_id | PARTITION_ID | version | BASE_VERSION | MULTI_VERSION_START | SNAPSHOT_VERSION | IS_ACTIVE | USED_MB | HASH_ITEMS | BTREE_ITEMS |
+------------------+--------------+---------+------------------+---------------------+---------------------+-----------+---------+------------+-------------+
| 1100611139453777 | 0 | 0-0-0 | 1644827477979191 | 1644827477979191 | 9223372036854775807 | 1 | 2 | 21760 | 15051 |
| 1100611139453779 | 0 | 0-0-0 | 1644827477978137 | 1644827477978137 | 9223372036854775807 | 1 | 12 | 10496 | 15163 |
+------------------+--------------+---------+------------------+---------------------+---------------------+-----------+---------+------------+-------------+
2 rows in set (0.02 sec)


按照 LSM Tree 数据分层存放的说法,这部分数据叫 C0,是没有压缩的。为了提升数据唯一性判断效率,在内存中还维护了一个 HASH TABLE。

OB 空间分配分析_sed

通常当 MEMSTORE 内存使用率达到参数  memstore_limit_percentage 时,触发 minor freeze 事件。随后该 MEMTABLE 会立即被冻结(immutable memtable, is_active=0),同时生成一个新的 MEMTABLE(is_active=1)。被冻结的MEMTABLE很快就会被后台线程转储到OB数据文件中。

mysql> alter system minor freeze;
Query OK, 0 rows affected (0.07 sec)
mysql> select table_id, PARTITION_ID ,version,BASE_VERSION ,MULTI_VERSION_START ,SNAPSHOT_VERSION ,IS_ACTIVE ,round(used/1024/1024) USED_MB, HASH_ITEMS ,BTREE_ITEMS
from `gv$memstore_info` gmi where table_id in (1100611139453777, 1100611139453779) order by table_id, PARTITION_ID , BASE_VERSION;
+------------------+--------------+---------+------------------+---------------------+---------------------+-----------+---------+------------+-------------+
| table_id | PARTITION_ID | version | BASE_VERSION | MULTI_VERSION_START | SNAPSHOT_VERSION | IS_ACTIVE | USED_MB | HASH_ITEMS | BTREE_ITEMS |
+------------------+--------------+---------+------------------+---------------------+---------------------+-----------+---------+------------+-------------+
| 1100611139453777 | 0 | 0-0-0 | 1644827477979191 | 1644827477979191 | 1644843569974204 | 0 | 10 | 28928 | 26895 |
| 1100611139453777 | 0 | 0-0-0 | 1644843569974204 | 1644843569974204 | 9223372036854775807 | 1 | 0 | 256 | 710 |
| 1100611139453779 | 0 | 0-0-0 | 1644827477978137 | 1644827477978137 | 1644843569968264 | 0 | 18 | 22784 | 26873 |
| 1100611139453779 | 0 | 0-0-0 | 1644843569968264 | 1644843569968264 | 9223372036854775807 | 1 | 2 | 256 | 711 |
+------------------+--------------+---------+------------------+---------------------+---------------------+-----------+---------+------------+-------------+
4 rows in set (0.01 sec)



OB数据文件空间特点一


OB的数据文件是在进程 observer 第一次启动的时候初始化,大小根据参数 datafile_disk_percentage 或 datafile_size 指定。生产环境多用前者,指定数据盘所在目录一定比例的空间分配给 OB 作为数据文件大小。测试的时候多用后者,直接指定具体的大小。


MySQL [oceanbase]> show parameters like '%datafile%';
+-------+----------+-----------------+----------+--------------------------+-----------+-------+-------------------------------------------------------------------------------+---------+---------+---------+-------------------+
| zone | svr_type | svr_ip | svr_port | name | data_type | value | info | section | scope | source | edit_level |
+-------+----------+-----------------+----------+--------------------------+-----------+-------+-------------------------------------------------------------------------------+---------+---------+---------+-------------------+
|zone1 | observer | 192.168.110.152 | 2882 | datafile_disk_percentage | NULL | 90 | the percentage of disk space used by the data files. Range: [5,99] in integer | SSTABLE | CLUSTER | DEFAULT | DYNAMIC_EFFECTIVE |
| zone1 | observer | 192.168.110.152 | 2882 | datafile_size | NULL | 1600G | size of the data file. Range: [0, +∞) | SSTABLE | CLUSTER | DEFAULT | DYNAMIC_EFFECTIVE |
+-------+----------+-----------------+----------+--------------------------+-----------+-------+-------------------------------------------------------------------------------+---------+---------+---------+-------------------+
2 rows in set (0.00 sec)


OB 的这种数据文件生成调用的是 fallocate 方法,快速生成一个大的空文件。


ob_local_file_system.cpp
int ObLocalFileSystem::open(bool& exist)
int64_t falloc_size = lower_align(data_file_size_, macro_block_size_);
if (OB_FAIL(fallocate(fd_.fd_, 0 /*MODE*/, 0 /*offset*/, falloc_size))) {
ret = OB_IO_ERROR;
STORAGE_LOG(ERROR, "allocate file error", K(store_path_), K(falloc_size), K(errno), KERRMSG, K(ret));
}


fallocate 的 mode 默认是0,会在文件系统中为数据文件预分配指定大小的空间,然后划分为 N个宏块(MacroBlock)。宏块是定长的,大小为 2MB。


select now(), svr_ip, round(total_size/2048/1024) macro_count,  round(total_size/1024/1024) total_size_mb, round(free_size/1024/1024)  free_size_mb, round(used_size/1024/1024) used_size_mb
from __all_virtual_disk_stat;
+---------------------+-----------------+-------------+---------------+--------------+--------------+
| now() | svr_ip | macro_count | total_size_mb | free_size_mb | used_size_mb |
+---------------------+-----------------+-------------+---------------+--------------+--------------+
| 2022-02-14 21:24:47 | 192.168.111.188 | 819200 | 1638400 | 1638388 | 12 |
+---------------------+-----------------+-------------+---------------+--------------+--------------+
1 row in set (0.00 sec)


本实例数据文件初始化为 1600GiB,一共 819200 个宏块。


查看磁盘上文件信息,在文件系统里分配的大小是 1.6T。(1717986918400/1024/1024/1024=1.5625,保留1位小数是1.6 。)


[root@localhost sstable]# ls -ls block_file
1677721768 -rw-r--r-- 1 admin admin 1717986918400 Feb 14 21:20 block_file
[root@localhost sstable]# ls -lsh block_file
1.6T -rw-r--r-- 1 admin admin 1.6T Feb 14 21:20 block_file


在 SSD 内部,逻辑空间和已分配空间是 1.56T (1.56T=1.56*1024G),文件对应的物理空间很小,大概 1.33MB。此时数据文件大部分都是空。


[root@localhost sstable]# sfx-filesize block_file -h
Logical Alocated Physical Ratio File
1.56T 1.56T 1.33M 1236289.12 block_file



SSD 空间使用特点


文件预分配占用的是 LBA ,LBA (全称是 Logical Block Address)是数据在文件系统上地址。LBA 对应磁盘上的 PBA(全称 Physical Block Address)。在机械盘里 LBA 跟 PBA 是一一对应关系,但是 SSD 里不是。应用修改一个 LBA 的数据,很可能会更换 PBA 地址(原因后面解释)。


OB 的文件系统通常使用 ext4 或 xfs,格式化时默认 Block Size 选 4K。SSD 的一个页大小默认也是 4K 。SSD 的主控会在磁盘的 DRAM 里维护 LBA到PBA的映射关系,这个是映射表(FTL,全称FLASH TRANSLATION LAYER)。LBA和PBA的单位都是4K,所以是 1:1 映射。LBA 地址是连续的,所以映射表只需要保留 PBA 地址。映射表的大小就是 SSD 磁盘的 Block 数量,也近似 SSD 的 PAGE 数量。实际上后者远远大于前者,通常 SSD 有保留约 7% 或 28% 的 PAGE 用于应付写需求,这个空间也叫 OP 空间(全称 Over Provision)。


SSD 的写单位是页(PAGE),每个页就 2 个状态:空、有数据。SSD 写只能在空页上做,因为页所在的 NAND 介质有数据后就不支持写入(这是由硬件特性决定的)。如果一个页的地址不在映射表里,这个页就是无效 PAGE (或垃圾 PAGE)。垃圾页擦除掉变成空页就可以继续用于写入。SSD 擦除的单位是块(BLOCK,这也是硬件特性决定的),一个 SSD 块大小通常是 128 或 256 个页。SSD的介质NAND的擦除次数是有限的,这就是SSD的寿命问题。当要修改一个页内容又没有空页可以写入的时候,会将当前要写入的页所在块读到 SSD 内部寄存器里直接修改,同时将该块的所有页都擦除掉,然后再将块的最新数据写回到块对应的页里。也有可能是先触发GC再写入,取决于SSD主控的逻辑。所以,为了修改一个4K的页结果写入了512K的数据,这个就是SSD的写放大。


这个是极端情况,SSD 设计了一个垃圾回收(GC,全称 Gabage Collection)在后台异步的将多个包含无效页的块的数据合并到新的块中(新块不包含无效页)并且擦除老的块。这样就又有了很多空页。GC 本身也有写放大。不过由于GC 都是低峰期做,可以降低业务写带来的写放大,所以还是很有意义的。


其他降低写放大的策略有:

  • 更大的OP空间。通常4T的盘默认7%, 8T的盘默认28%。
  • 对SSD发TRIM 指令。需要操作系统、文件系统、SSD主控都要支持TRIM 指令。
  • 更大的剩余可用空间。如应用删除不用的文件,然后结合TRIM指令。
  • 持续的顺序写。


TRIM指令是SSD 特有的。文件在文件系统里被删除后,实际只是删除了对文件块的指针,文件块的内容(LBA对应的PBA指向的物理页)还在(所以文件误删除还是有可能找回来)。在文件系统里文件是被删除了,在 SSD 里这个数据块却还是有效的。SSD GC 的时候可能还要搬迁这些无效数据。TRIM指令就是声明一段LBA对应的PBA对应物理页无效,SSD GC的时候就可以直接回收这些页,降低写放大。TRIM命令需要文件系统支持,ext4和xfs都支持,在mount时增加选项“discard”。不过即使没有TRIM动作,如果应用在删除文件的同时也在不断新增文件,原来的LBA地址很快就复用,则对应的老的PBA地址也很快就成为无效数据,逃不过SSD GC。



转储 minor freeze


接前面内存MEMTABLE内容继续。MEMTABLE会写入到数据文件中,写入时是按照主键值(没有主键就用内部隐藏主键)顺序写入,这个格式简称SSTABLE(sorted string table)。OB的SSTABLE再细分就是宏块(MacroBlock),定长2M。宏块是由很多微块(MicroBlock)组成,微块大小是建表语句中的BLOCK_SIZE,默认值是16KB。每个微块里有很多行(ROWS)。OB表默认是一个分区,分区表会有多个分区。每个分区对应一个SSTABLE。分区的内容可以很大,但是SSTABLE不能跨节点存储。每个SSTABLE有很多宏块,就是从OB的数据文件里分配的。
OB建表时还可以指定压缩,默认压缩算法是 zstd。OB还支持其他算法,如 snappy、lz4 等。当表开启压缩时,OB会在微块满16KB的时候压缩微块,然后填充到宏块里。所以开启压缩后,实际微块大小多不足16KB,宏块可以存放更多的微块。这个压缩会消耗一定CPU,不过由于宏块可以读写更多数据,综合下来OB开启压缩对性能的影响并不大。


宏块的逻辑就是上面这样。内存中的MEMTABLE数据转储为数据文件中的SSTABLE。这个事件称之为“mini merge”,这个数据称之为C1,这个数据块中微块压缩算法选的是LZ4(目前没有找到哪个参数配置,应该代码写死的逻辑,除非表不开启压缩)。TiDB的TiKV的LEVEL1的压缩也是选择LZ4,主要是LZ4的压缩是最快的(自然压缩效果不是最好的)。

OB 空间分配分析_sed_02

当同一个表的数据改动多次,并且发生多次mini merge的时候,相同的主键(KEY)对应的增量数据可能会存在多个C1数据块中(宏块),这个就是LSMTree的空间放大的一个体现点。通常LSMTree会把多个C1的数据进一步合并为C2,然后C2也有多个时,再合并为C3等等。层层推进。最后的就是全部的业务数据。如果是这样,空间放大会更严重。最早RocksDB就是这么做的,空间放大比超过20。OB的LSMTree在磁盘上最多三层。当相同的KEY出现的C1数量达到3个时,在mini merge完成后会发起 mini minor merge,将多个C1合并为C2。C2跟C1一样,如果表开启压缩,C2宏块的压缩算法也是LZ4。不一样的地方是,每个SSTABLE的多个C2之间KEY值不重叠,即每个KEY对应的数据只会出现在一个宏块里。这个能一定程度降低空间放大。


随着表的数据反复变化(增删改查),相同的KEY会有多个C1,然后不断的合并(compaction)到C2。为了维持同一个SSTABLE的C2的KEY不重叠,这个会导致很多C2 宏块被改写。当然LSMTree模型没有原地更新(IN-PLACE UPDATE)的设计,都是生成新的宏块,然后删除老的宏块。这个也是写放大的一个体现。LSMTree跟SSD的原理相似的一个点。


通过OB视图 __all_virtual_partition_sstable_macro_info 可以观察到C1、C2的每个宏块信息(KEY范围、宏块大小、微块总数量、微块里总记录数等),通过视图__all_virtual_partition_sstable_merge_info 可以知道每个SSTABLE的变化信息(修改了多少宏块,复用了多少宏块,记录数变化了多少)。



合并 major freeze


数据的读取可能需要综合内存中MEMTABLE的C0数据、数据文件中的C1和C2,这个是LSMTree的读放大。为了降低读放大,OB每天默认低峰期(+2:00)会将C1和C2再合并到C3。C3是业务数据全量的一个快照(最新的数据在C0/C1/C2)。合并时,C3中相应KEY对应内容没有变化的宏块会继续保留,C1/C2中KEY涉及到的宏块在C3中都会被改写(新增加删除)。合并也会导致一些宏块实际大小不足2M,不足的部分会用0x0补齐。删除老版本宏块的时候也有讲究,默认会保留两个全量版本(data_version),这个是参数max_kept_major_version_number定义的。尽管是2个,实际指向的数据文件块却是同一个,所以这里不会有额外的空间消耗。可能两个全量版本之间的CLOG是要额外保留的。如下面查询是表的所有宏块。


select concat(table_id,' : ', partition_id) partition_id,major_table_id,base_version, concat(data_version, '-', macro_data_version) data_version, macro_idx_in_data_file macro_idx,data_seq, row_count, occupy_size, micro_block_count, macro_range, compressor_name,row_count_delta
from __all_virtual_partition_sstable_macro_info
where major_table_id in (1100611139453788)
order by table_id, partition_id, data_seq;
+----------------------+------------------+--------------+--------------+-----------+----------+-----------+-------------+-------------------+-------------------+-----------------+-----------------+
| partition_id | major_table_id | base_version | data_version | macro_idx | data_seq | row_count | occupy_size | micro_block_count | macro_range | compressor_name | row_count_delta |
+----------------------+------------------+--------------+--------------+-----------+----------+-----------+-------------+-------------------+-------------------+-----------------+-----------------+
| 1100611139453788 : 0 | 1100611139453788 | 0 | 10-9 | 29531 | 1048576 | 14970 | 2089598 | 126 | (MIN ; 49970] | zstd_1.3.8 | 0 |
| 1100611139453788 : 0 | 1100611139453788 | 0 | 11-9 | 29531 | 1048576 | 14970 | 2089598 | 126 | (MIN ; 49970] | zstd_1.3.8 | 0 |
| 1100611139453788 : 0 | 1100611139453788 | 0 | 10-9 | 29550 | 1048577 | 14898 | 2089894 | 126 | (49970 ; 94868] | zstd_1.3.8 | 0 |
| 1100611139453788 : 0 | 1100611139453788 | 0 | 11-9 | 29550 | 1048577 | 14898 | 2089894 | 126 | (49970 ; 94868] | zstd_1.3.8 | 0 |
| 1100611139453788 : 0 | 1100611139453788 | 0 | 10-9 | 29561 | 1048578 | 14892 | 2089991 | 126 | (94868 ; 109760] | zstd_1.3.8 | 0 |
| 1100611139453788 : 0 | 1100611139453788 | 0 | 11-9 | 29561 | 1048578 | 14892 | 2089991 | 126 | (94868 ; 109760] | zstd_1.3.8 | 0 |
| 1100611139453788 : 0 | 1100611139453788 | 0 | 10-9 | 29564 | 1048579 | 14893 | 2089402 | 126 | (109760 ; 144653] | zstd_1.3.8 | 0 |
| 1100611139453788 : 0 | 1100611139453788 | 0 | 10-8 | 28326 | 1048579 | 14889 | 2089651 | 126 | (184665 ; 199554] | zstd_1.3.8 | 0 |
| 1100611139453788 : 0 | 1100611139453788 | 0 | 11-9 | 29564 | 1048579 | 14893 | 2089402 | 126 | (109760 ; 144653] | zstd_1.3.8 | 0 |
| 1100611139453788 : 0 | 1100611139453788 | 0 | 11-8 | 28326 | 1048579 | 14889 | 2089651 | 126 | (184665 ; 199554] | zstd_1.3.8 | 0 |
| 1100611139453788 : 0 | 1100611139453788 | 0 | 10-9 | 29566 | 1048580 | 10012 | 1405052 | 85 | (144653 ; 184665] | zstd_1.3.8 | 0 |
| 1100611139453788 : 0 | 1100611139453788 | 0 | 11-9 | 29566 | 1048580 | 10012 | 1405052 | 85 | (144653 ; 184665] | zstd_1.3.8 | 0 |
| 1100611139453788 : 0 | 1100611139453788 | 0 | 10-9 | 29588 | 1048581 | 5446 | 759132 | 46 | (199554 ; MAX] | zstd_1.3.8 | 0 |
| 1100611139453788 : 0 | 1100611139453788 | 0 | 11-9 | 29588 | 1048581 | 5446 | 759132 | 46 | (199554 ; MAX] | zstd_1.3.8 | 0 |
+----------------------+------------------+--------------+--------------+-----------+----------+-----------+-------------+-------------------+-------------------+-----------------+-----------------+
14 rows in set (0.77 sec)


当把 max_kept_major_version_number 修改为1,修改部分数据,并再次发起合并(命令:alter system major freeze)后,查询结果变为:


+----------------------+------------------+--------------+--------------+-----------+----------+-----------+-------------+-------------------+-------------------+-----------------+-----------------+
| partition_id | major_table_id | base_version | data_version | macro_idx | data_seq | row_count | occupy_size | micro_block_count | macro_range | compressor_name | row_count_delta |
+----------------------+------------------+--------------+--------------+-----------+----------+-----------+-------------+-------------------+-------------------+-----------------+-----------------+
| 1100611139453788 : 0 | 1100611139453788 | 0 | 12-9 | 29531 | 1048576 | 14970 | 2089598 | 126 | (MIN ; 49970] | zstd_1.3.8 | 0 |
| 1100611139453788 : 0 | 1100611139453788 | 0 | 12-9 | 29550 | 1048577 | 14898 | 2089894 | 126 | (49970 ; 94868] | zstd_1.3.8 | 0 |
| 1100611139453788 : 0 | 1100611139453788 | 0 | 12-9 | 29561 | 1048578 | 14892 | 2089991 | 126 | (94868 ; 109760] | zstd_1.3.8 | 0 |
| 1100611139453788 : 0 | 1100611139453788 | 0 | 12-9 | 29564 | 1048579 | 14893 | 2089402 | 126 | (109760 ; 144653] | zstd_1.3.8 | 0 |
| 1100611139453788 : 0 | 1100611139453788 | 0 | 12-8 | 28326 | 1048579 | 14889 | 2089651 | 126 | (184665 ; 199554] | zstd_1.3.8 | 0 |
| 1100611139453788 : 0 | 1100611139453788 | 0 | 12-9 | 29566 | 1048580 | 10012 | 1405052 | 85 | (144653 ; 184665] | zstd_1.3.8 | 0 |
| 1100611139453788 : 0 | 1100611139453788 | 0 | 12-9 | 29588 | 1048581 | 5446 | 759132 | 46 | (199554 ; MAX] | zstd_1.3.8 | 0 |
+----------------------+------------------+--------------+--------------+-----------+----------+-----------+-------------+-------------------+-------------------+-----------------+-----------------+
7 rows in set (0.39 sec)



OB数据文件空间特点二


分析完C1、C2、C3数据特点,再来总结一下OB数据文件的特点。OB节点初始化时预分配一定大小数据文件,并格式化为一定数目的宏块。随着业务持续写,不断有内存中增量数据转储到数据文件中为C1,然后从C1推进到C2,从C2推进到C3。每一次推进都是写新的宏块,删除老的宏块。OB的数据文件写IO大小为2M,该IO在发送到SSD之前会被拆分为多个小IO(IO size最大128K或256K),所以通常观察数据文件所在磁盘的 iostat 的请求大小都不超过256K。此外,SSD的页大小是4K,这里还会有个“原子写”挑战。MySQL使用双写来确保数据库块在磁盘上的原子写,OB数据文件只有超级块(super block)使用双写。其他宏块都是顺序写出,宏块里有块的 checksum 信息。如果有坏块,根据上一层数据重写一份新的即可。“原子写”也是一个跟存储有关的话题,以后再讨论。OB在删除老的宏块时只是修改内部宏块的引用计数(降到0表示无效宏块,可以复用),并不会通知文件系统释放存储空间。在MySQL里,有一种透明压缩技术,释放的空间会通过打洞(punch hole)方式释放存储空间。在OB里有这个代码,但是却没有调用。OB在这里选择的不释放空间,改由自己管理空间。OB视图__all_virtual_disk_stat可以观察数据文件空间使用情况。

select now(), svr_ip, round(total_size/1024/1024) total_size_mb, round(free_size/1024/1024) free_size_mb, round(used_size/1024/1024) used_size_mb
from __all_virtual_disk_stat;
+---------------------+-----------------+---------------+--------------+--------------+
| now() | svr_ip | total_size_mb | free_size_mb | used_size_mb |
+---------------------+-----------------+---------------+--------------+--------------+
| 2022-02-12 21:34:48 | 192.168.110.152 | 1638400 | 798160 | 840240 |
+---------------------+-----------------+---------------+--------------+--------------+
1 row in set (0.01 sec)

LSMTree的合并都是需要额外空间来容纳新增的数据,合并后再删除删除老的数据。所以数据文件剩余空间的变化会存在起伏。下图就是一个OB数据文件内部剩余空间监控曲线。

OB 空间分配分析_数据_03


OB数据文件最后使用效果就像下图,由不同类型的宏块组成。当OB的表都开启压缩时,这些宏块使用了LZ4或者ZSTD压缩。


OB 空间分配分析_数据文件_04

OB的宏块写在前期是近似顺序写,优先写空的宏块,直到写到文件末尾然后再从头查找此前被删除的宏块(was used),就这样循环利用。OB的数据文件写这个特点也跟SSD的写特别像。删除宏块时不通知文件系统释放存储空间,其影响跟前面删除文件不释放空间对SSD的影响是一样的。在SSD里,不管是当前有效的宏块,还是已经删除的宏块,都是有效的数据,无效的宏块不会被SSD GC回收。初步分析这个设计问题也不大。一是宏块大小是定长的2M,跟SSD页大小(4K)或者块大小(256K)是对齐的,即使做GC也是多个SSD块整体GC,不会有写放大。实际上,OB复用宏块(即改写宏块)的时候,由于宏块对应文件系统的LBA地址不变,SSD写是写新的页,LBA对应的PBA地址会变化。SSD里老的PBA地址自动失效,成为后期GC的目标,且不会有写放大。从这个角度看,OB的LSMTree的这种宏块管理策略对SSD也是很友好的。



数据空间压缩


当一个业务持续的写时,业务占用的总数据空间实际是 C1+C2+C3 ,这个总量会随着转储和合并而上下浮动。C1+C2和C3的比例会是影响整体数据压缩率的一个重要因素(另外一个因素就是数据自身压缩率)。

selectdata_version,compressor_name ,count(*), round(sum(occupy_size)/1024/1024)occupy_size_mb, avg(row_count) row_count_avg, sum(row_count) row_count_sum
from__all_virtual_partition_sstable_macro_info
where1=1 and tenant_id=1001
group bydata_version,compressor_name ;


+--------------+-----------------+----------+----------------+---------------+---------------+
|data_version | compressor_name | count(*) | occupy_size_mb | row_count_avg |row_count_sum |
+--------------+-----------------+----------+----------------+---------------+---------------+
| 4 | zstd_1.3.8 | 1168 | 2315 | 39409.2277 | 46029978 |
| 0 | lz4_1.0 | 4226 | 8377 | 20058.3474 | 84766576 |
| 4 | none | 27 | 1 | 117.3704 | 3169 |
| 0 | none | 20 | 0 | 118.7000 | 2374 |
+--------------+-----------------+----------+----------------+---------------+---------------+
4 rowsin set (0.02 sec)

就压缩算法的压缩效果,比较如下。下面是两种不同压缩率的数据(sysbench 测试数据)在不同压缩工具后的效果。LZ4 默认设置压缩最快,压缩效果最差。ZSTD和GZIP 压缩效果最好,但会比较耗CPU。

OB 空间分配分析_数据_05


不管是哪种合并(mini merge/mini minor merge/major merge),都可以设置一定并发。并发参数的设置需要在性能和稳定性之间取平衡。

Alter system set _mini_merge_concurrency =4;
Alter system set minor_merge_concurrency =8;
alter system set _mini_merge_concurrency=8;
alter system set merge_thread_count=20;

 不同SSTABLE间的并发好做,单个SSTABLE内部的并发是根据KEY值范围切分,在C3的SSTABLE还没有生成的时候,没有KEY值可以切分,所以依然是单线程做合并。所以初始化 1 张10亿的大表和初始化100张1000万的表,合并时的性能也是不一样的。


当数据文件没有可用的宏块可以复用时,就会报空间不足,这个会体现在OB的运行日志里。最后并发运行的线程发现空间不足后就会放弃任务,释放出一些宏块。然后又再次尝试申请宏块。所以观察视图__all_virtual_disk_stat 不一定能捕捉到 free_size 为 0 。

[2022-01-29
20:32:10.082573] ERROR [STORAGE] alloc_block (ob_store_file.cpp:968)
[77171][2997][YB42C0A86FBC-0005D6AE380C490A] [lt=19] [dc=0] Fail to alloc
block, (ret=-4184, free_block_cnt_=0, total_count=972800) BACKTRACE:0x97b78ce
0x970a211 0x2228026 0x2227c6b 0x22279e3 0x481ebc9 0x867cfa9 0x867c62c 0x80b0f11
0x80bb4b9 0x80d79a0 0x80d9ea3 0x80d5444 0x80cfd68 0x80cdfaf 0x80cdb96 0x85ddb46
0x85e00d9 0x85eb55c 0x7a50ab6 0x330450a 0x3307970 0x32dd03f 0x2be0a32 0x95a1975
0x95a06a2 0x959d1cf


基于LSMTree设计的数据库都有压缩的能力,而LSMTree的空间放大比又是对压缩效果有负面作用的因素。所以不同LSMTree产品的实际空间压缩效果也会有点区别。这里有几个有趣的问题留给读者思考,如果压缩比是1/4,1T的数据文件到底能写入多少业务数据?空间监控阈值应该设置为多少才安全?


从效果上说,基于LSMTree的数据库产品由于默认开启压缩,其实际存储空间和读写吞吐量确实比基于BTree的数据库产品节省很多。这个也间接降低SSD的写放大,提升SSD的寿命。基于BTree的数据库产品虽然也有表压缩(或者页压缩)能力,但是往往是以消耗CPU为代价,性能下降比较明显,所以生产环境用的不多。随着硬件技术的发展,部分SSD产品将数据压缩能力集成到SSD内部,做到压缩和解压缩对应用完全透明,降低SSD写放大,降低GC频率,所以性能有提升,且不占用主机的CPU。这类SSD又称为计算型存储(CSD,computational storage devices),可以弥补 BTree模型数据库产品的劣势。这个以后再分享。


注:本文纯个人技术观点,可能有不对之处,欢迎指出。


参考