写在前面
最近在 OceanBase 社区官网里看到有比较多的同学反馈了一些磁盘上的疑问,包括磁盘异常占用,磁盘分配原理,以及如何查看租户/表的磁盘具体使用等。相比于 MySQL 等传统数据库的数据文件存储形式,OceanBase 的数据文件管理稍有不同。因此笔者想把磁盘数据文件分配机制做简单的描述,目的是希望大家在阅读文章后对 OceanBase 的磁盘数据文件分配有初步了解。特此说明,以下内容仅针对 OceanBase 4.x 发行版本。
磁盘系统层级关系
Block_file
我们还是先整体看一下磁盘数据文件系统吧。熟悉 OceanBase 数据库的同学应该知道,集群启动后,会创建几个文件目录,例如 clog、sstable、slog 以及 tslog,其中用户的数据存储在一个名叫 sstable/block_file 的文件里,该文件存储的是二进制数据,并且经过了编码压缩等,是无法直接打开查看的。那么 block_file 这个文件是怎么来的,里面又是一些什么内容?在集群启动阶段,我们会创建这个名为 block_file 的文件,并通过 Linux 系统调用 fallocate 函数来预分配这个磁盘数据文件的大小。这样做的好处是可以快速 hold 住一块磁盘区域,得到了一个相对连续的磁盘空间。然后我们在这个磁盘空间上定制高效的文件系统,例如 block 的分配、复用、回收等一些列磁盘操作。
现在来看下 block_file 的文件路径:
Macro/Micro Block
了解了 block file 的由来之后,我们再展开看下 block file 里究竟由哪些结构组成。OceanBase 把 block_file 按照一定大小划分成了多个宏块,宏块按照变长的大小划分成了多个微块,来保存数据。宏块默认长度为 2M,变长微块的默认长度是 16K,一般无法保证一定是 16K,但大小一定在 16K 左右。变长的最大好处是可以保证数据的压缩率,这点很好理解,因为压缩后的数据长度可能非固定。另外,我们用一个 Bitmap 来维护所有宏块的使用状态 (Free/Used等),以及用一个循环数组 Free Block Array 记录每个空闲宏块 Block idx。初始时 Bitmap 内所有的 Block 被标记为 Free,当用户需要写数据时,从 Free Block Array 中申请一个宏块,并将该宏块在 Bitmap 中标记为 In Used,最后把循环数组的 free_block_pos 游标向前推进。
SSTable
了解了宏块、微块后,不得不介绍下 SSTable (sort string table),这是一个逻辑上的概念,实际上由多个宏块所组成。我们已经知道了宏块对应的是实际物理磁盘数据,在数据读写时,宏块是写 IO 操作的最小单位,微块是读 IO 的最小单位。我们来看下SSTable/宏块/微块的关系图:
经过了上述简单介绍后,相信大家应该对 OceanBase 的磁盘数据文件系统有一些感觉,我来稍微总结一下:block_file 是大文件,在集群启动后根据预设大小生成,大文件里划分出很多 Macro Block 宏块/Micro Block 微块的物理结构,上层通过 SSTable 逻辑结构来组织和管理这些宏块微块,并使用 Free Block Array 循环数组维护这些空闲宏块的申请和释放。
代码层级关系
然后,我们再从代码层面看看这套系统是怎么分配和使用的,首先上个图:
其中 User 是调用 ObBlockManager 的模块,Block File 是 ObLocalDevice 直接操作的磁盘文件对象,主要是文件操作,例如 fallocate、fsync、read/write 等操作,我们重点介绍一下 ObBlockManager 和 ObLocalDevice。
ObBlockManager
这个类主要是对底层 LocalDevice 封装,是整个 LocalDevice 的管理类,包含启动定时任务巡检 bad block(坏块检测)、mark block(刷新 block)操作,以及 block 的分配(alloc)和释放(free)、block_file(resize file)文件大小的调整等。
ObLocalDevice
这个类提供了 block 相关的操作,例如 alloc_blocks、free_blocks、mark_blocks 等与 block 相关的处理,并且屏蔽了底层不同文件系统的差异,对上层提供统一的入口。
磁盘分配方式
总体来看,OceanBase 的磁盘数据文件分配方式有两种:
- 手动分配
- 自动分配
手动分配是指通过启动配置项,或配置文件,显式地指定 Observer 需要的磁盘空间。例如datafile_size/datafile_disk_percentage 配置项,其中 datafile_size 表示使用的固定大小,datafile_disk_percentage 表示使用磁盘的比例。
OceanBase 在 4.2 之后的版本支持磁盘数据文件的自动分配,通过配置集群级的可用磁盘的初始大小以及最大上限,如 datafile_maxsize,在集群运行过程中自动判断当前所使用的磁盘空间是否需要扩展,并可配置每次自动扩展的大小,如 datafile_next,自动扩充数据文件空间。
上述两种方式有各自的优势和缺点。
磁盘文件分配方式 | 优点 | 缺点 |
自动分配 | 渐进分配,磁盘空间线性增长 | 在机器不能独占的情况下,不能保证一定能扩展到 datafile_maxsize 空间 |
手动分配 | 一次分配,保证占用预置的磁盘空间 | 在磁盘预设较大的情况下,可能存在一定的空间浪费 |
来,一起来看代码流程
我们讲讲磁盘的预分配和调整流程。
在讲代码之前,我先给大家普及几个关键配置/变量/内部表以及含义
配置项/内部变量 | 含义 |
datafile_size | block_file 文件大小 |
dataflie_disk_percentage | block_file 文件占磁盘比例 |
datafile_maxsize | block_file 自动增长上限 |
datafile_next | block_file 自动增长步长 |
block_size_ | block大小,默认 2M(宏块大小) |
free_block_cnt_ | 空闲 block 数量 |
total_block_cnt_ | 总的 block 数量 |
其中,datafile_size 是一个集群级别的配置项,如果不配置,默认为 0,当然,此时我们会按照磁盘是否共享模式(clog 和 block_file 同一个盘)分别分配 90%(独占)和 60%(共享)的 datafile_size 磁盘空间。datafile_maxsize 是磁盘数据文件自动扩展的配置项,当该值大于 datafile_size 值,并且 datafile_next 不为 0 时,表示启动自动扩展。block_size 是每个宏块的大小,默认为 2M,在集群启动的时候初始化这个变量。total_block_cnt 是根据预分配磁盘空间计算得出的总的 block 数量,并在后续修改 datafile_size 做相应的动态调整。free_block_cnt 是当前 Observer 中空闲的 block 数量,每次上层调用 alloc_block 时减一,调用 free_block 时加一,默认等于 total_block_cnt。上述几个变量都严格管理在 LocalDevice 层,对外只读,这样可以保证 block 的分配和释放和上层模块完全解耦合。
如果我想看下当前磁盘空间的使用情况,有内部表吗?当然有:
内部表 | 字段 | 含义 |
__all_virtual_server | data_disk_capacity | block_file 总大小 |
data_disk_in_use | block_file 使用大小 | |
__all_virtual_disk_stat | total_size | block_file 总大小 |
used_size | block_file 使用大小 |
我们再来粗略的看下磁盘预分配的函数调用栈
ObServer::start // 启动server
-> ObBlockManager::start
-> ObLocalDevice::start
-> ObLocalDevice::open_block_file
-> ::fallocate // 预分配
block_file文件的调整函数调用栈
ObServerReloadConfig::operator() // 当配置参数修改后,触发config reload, 例如执行alter system set datafile_size=xxx,由此刷新datafile_size
-> ObBlockManager::resize_file
-> ObLocalDevice::reconfig
-> ::fallocate // 重新预分配
当然,上面的代码介绍忽略了很多计算和校验过程,有兴趣的同学可以找时间研究下 ob_block_manager.cpp 和ob_local_device.cpp 这两个文件,这里不做详细展开。
如何查看磁盘占用
了解磁盘数据文件的构成以及分配方式后,我们再看下如何查看实际生产过程中的磁盘占用情况。OceanBase 提供了丰富的内部表用以记录集群运行过程中产生的数据信息,方便用户了解集群的状态以及问题排查。但对于外部用户而言,过多的内部表可能会造成用户的选择困难,这里有个技巧,我们可以通过查 __all_table 内部表,根据通配符模糊匹配关键字的方法,来获取相关的内部表信息,例如这里我想查看磁盘相关,如下:
obclient [oceanbase]> select table_name from __all_table where table_name like '%disk%';
+-----------------------------------------+
| table_name |
+-----------------------------------------+
| __all_disk_io_calibration |
| __all_disk_io_calibration_aux_lob_meta |
| __all_disk_io_calibration_aux_lob_piece |
| __all_virtual_disk_stat |
+-----------------------------------------+
4 rows in set (0.029 sec)
然后再选择比较接近的表或视图查看,如下面数据为笔者的磁盘占用,其中 total_size 为总的大小,为 30G,used_sized 为当前已经使用大小,free_size 为当前剩余空闲的大小。因为笔者拉起的是单节点集群,看到的只有一个 server 的统计数据。
obclient [oceanbase]> select * from __all_virtual_disk_stat;
+---------------+----------+-------------+-----------+-------------+---------------+---------------------+----------------+
| svr_ip | svr_port | total_size | used_size | free_size | is_disk_valid | disk_error_begin_ts | allocated_size |
+---------------+----------+-------------+-----------+-------------+---------------+---------------------+----------------+
| 127.0.0.1 | 2882 | 32212254720 | 150994944 | 32057065472 | 1 | 0 | 32212254720 |
+---------------+----------+-------------+-----------+-------------+---------------+---------------------+----------------+
1 row in set (0.059 sec)
如果想查看更小粒度的磁盘使用数据,例如查看 tablet 级别的数据占用。那就需要查看另外一张内部表,方法如上述介绍的使用通配符匹配内部表:
obclient [oceanbase]> select table_name from __all_table where table_name like '%tablet%meta';
+--------------------------------------------------+
| table_name |
+--------------------------------------------------+
| __all_backup_skipped_tablet_aux_lob_meta |
| __all_backup_skipped_tablet_history_aux_lob_meta |
| __all_tablet_checksum_aux_lob_meta |
| __all_tablet_meta_table_aux_lob_meta |
| __all_tablet_replica_checksum_aux_lob_meta |
| __all_tablet_to_ls_aux_lob_meta |
| __all_tablet_to_table_history_aux_lob_meta |
+--------------------------------------------------+
7 rows in set (0.015 sec)
由于当前我们没有直接统计租户或单表的磁盘实际占用的视图或内部表,不能很直观的观测。但因为我们已经知道了单个 tablet 的实际占用,鉴于此可以利于 tablet 和表/租户的关系,来统计表/租户的实际占用。
租户级
obclient [oceanbase]> select tenant_id, sum(data_size), sum(required_size) from __all_virtual_tablet_meta_table group by 1 order by 3 desc;
+-----------+----------------+--------------------+
| tenant_id | sum(data_size) | sum(required_size) |
+-----------+----------------+--------------------+
| 1 | 5969149 | 241172480 |
| 1002 | 1451540 | 132120576 |
| 1001 | 0 | 0 |
+-----------+----------------+--------------------+
3 rows in set (0.304 sec)
表级
需要说明的是,表级的统计需要在 SQL 中指定表名,例如笔者统计的表名为“t3”:
obclient [oceanbase]> select sum(data_size), sum(required_size) from __all_virtual_tablet_meta_table where tablet_id in (select tablet_id from __all_virtual_tablet_to_table_history where table_id in (select table_id from __all_table where table_name='t3'));
+----------------+--------------------+
| sum(data_size) | sum(required_size) |
+----------------+--------------------+
| 1758139 | 4194304 |
+----------------+--------------------+
1 row in set (0.425 sec)
写在最后
OceanBase 的磁盘数据文件分配逻辑并不复杂,但由于我们的磁盘数据文件管理相比于其他传统的数据库有特别之处,使用者往往对我们的磁盘分配现象有不少疑惑。例如集群启动后,在没有写入数据时,通过 df 等工具查看磁盘占用比较高(原因是预分配了磁盘文件)。当然,还有其他使用上的疑惑。这篇文章普及了磁盘数据文件系统的大概过程,让同学们对 OceanBase 的磁盘管理和分配有个粗略的了解。如果需要深入 OceanBase 的磁盘数据文件分配及管理细节,欢迎在评论区留言讨论。