借助 MariaDB 在 FusionIO 上新的页面压缩功能,性能显著提升
MariaDB 项目很高兴宣布推出 MariaDB 10.0.9 的特殊预览版,该版本在 FusionIO 设备上带来了显著的性能提升。这是一个 beta 质量的预览版。
下载 MariaDB 10.0.9-FusionIO 预览版
背景
MariaDB 与 FusionIO 之间的最新工作重点在于显著提升 MariaDB 在 Fusion-IO 生产的高端 SSD 驱动器上的性能,同时大幅延长驱动器本身的寿命。此外,FusionIO 闪存解决方案提高了事务数据库的性能。MariaDB 包含针对 FusionIO 设备的专门改进,利用这些流行的高性能固态硬盘上的 NVMFS 文件系统的一项特性。利用此特性,MariaDB 10 在与 FusionIO 设备一起使用时,可以消除 InnoDB 存储引擎内部的一些开销。
下图的图 1 展示了左侧的传统 SSD 架构和右侧的 FusionIO 架构。
图 1:左侧为传统架构,右侧为新的 FusionIO 架构。
双写缓冲区(Doublewrite buffer)
当 Innodb 写入文件系统时,通常无法保证在断电事件发生或操作系统在写入操作正在进行的确切时刻崩溃的情况下,给定写入操作是完整的(而非部分完成)。
如果无法检测或防止部分写入,数据库的完整性在恢复后可能会受到损害。因此,Innodb 已经有一个通过 InnoDB 双写缓冲区(InnoDB Doublewrite Buffer)检测和忽略部分写入的机制(也可以使用 innodb_checksum 来检测部分写入)。
双写操作由 innodb_doublewrite 系统变量控制,本身也带来了一系列问题。尤其是在 SSD 上,将每个页面写入两次会产生有害影响(磨损均衡)。此外,SSD 的寿命也受到威胁,因为它在需要更换之前能够处理的最大写入次数是有限的。通过写入两次,预期寿命会减半。
在将页面写入数据文件之前,InnoDB 首先将它们写入一个称为双写缓冲区(doublewrite buffer)的连续表空间区域。只有在写入并刷新到双写缓冲区完成后,InnoDB 才会将页面写入它们在数据文件中的正确位置。如果操作系统在页面写入过程中崩溃(导致页面撕裂条件),InnoDB 可以在恢复期间从双写缓冲区找到该页面的良好副本。
更好的解决方案是直接要求文件系统提供原子性(全有或全无)写入保证。目前,只有 FusionIO 设备上提供原子写入功能的 NVMFS 文件系统才支持此功能。MariaDB 的 XtraDB 和 InnoDB 存储引擎支持此功能。要使用原子写入而非双写缓冲区,请添加
innodb_use_atomic_writes = 1
innodb_use_atomic_writes = ON
到 my.cnf 配置文件中。有关此功能的更多信息,请参阅 https://mariadb.com/kb/en/fusionio-directfs-atomic-write-support/
InnoDB 压缩表
通过使用 InnoDB 表的压缩选项,您可以创建以压缩形式存储数据的表。压缩有助于提高原始性能和可伸缩性。压缩意味着在磁盘和内存之间传输的数据更少,并且在磁盘和内存中占用更少的空间。对于带有二级索引的表,其好处更加明显,因为索引数据也会被压缩。压缩对于 SSD 存储设备很重要,因为……
InnoDB 将未压缩数据存储在 16K 页面中,并将这些 16K 页面压缩成固定的压缩页面大小,如 1K、2K、4K、8K。这个压缩页面大小是在表创建时使用 KEY_BLOCK_SIZE 参数选择的。压缩是使用常规的软件压缩库(zlib)执行的。
由于页面经常更新,B-tree 页面需要特殊处理。最大程度地减少 B-tree 节点的拆分次数,以及最小化解压缩和重新压缩其内容的需求至关重要。因此,InnoDB 在 B-tree 节点中以未压缩的形式维护一些系统信息,从而方便进行某些原地更新。例如,这允许在不进行任何压缩操作的情况下标记行以供删除并实际删除。
此外,当索引页面发生更改时,InnoDB 会尝试避免不必要的解压缩和重新压缩。在每个 B-tree 页面内,系统会保留一个未压缩的“修改日志”(modification log),用于记录对页面所做的更改。小型记录的更新和插入可以写入此修改日志,而无需完全重建整个页面。
当修改日志的空间用尽时,InnoDB 会解压缩该页面,应用更改并重新压缩页面。如果重新压缩失败,B-tree 节点将被拆分,并重复该过程,直到更新或插入成功。
为了避免在写入密集型工作负载(例如 OLTP 应用程序)中频繁发生压缩失败,InnoDB 在页面中保留了一些空白空间(填充),以便修改日志更快地填满,并在页面重新压缩时仍有足够的空间避免拆分。每个页面中剩余的填充空间量因系统跟踪页面拆分的频率而异。
然而,所有这些都有明显的缺点
- 内存
- 空间:缓冲区池中同时存储未压缩和压缩页面
- 访问:更新应用于内存中的两个副本
- CPU 消耗
- 软件压缩库(从磁盘读取时解压缩,拆分时重新压缩)
- mlog 溢出时进行拆分、重新压缩和重新平衡
- 容量优势
- 固定的压缩页面大小 – 限制了压缩效益
- 修改日志和填充占用空间,降低了效益
- 采用率低
- 代码非常复杂,与未压缩表相比性能下降明显
解决方案:页面压缩
缓冲区池中不再同时存储压缩和未压缩页面,而只存储未压缩的 16KB 页面。这避免了何时需要重新压缩页面或何时向 mlog 添加更改等非常复杂的逻辑。同样,也不需要进行页面拆分等操作。在创建页面压缩表之前,请确保已启用 innodb_file_per_table
配置选项,并将 innodb_file_format
设置为 Barracuda
。
这项工作是与 FusionIO 合作完成的,特别感谢
-
Dhananjoy Das
-
Torben Mathiasen
当页面被修改时,它会在写入之前进行压缩(fil 层),并且只写入压缩后的大小(对齐到扇区边界)。如果由于压缩失败导致压缩失败,我们会将未压缩页面写入文件空间。然后,我们通过以下方式修剪压缩页面中未使用的 512B 扇区
fallocate(file, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, off, trim_len);
fallocate(file_handle, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, file_offset, remainder_len)
这导致 NVMFS 文件系统报告媒体上使用的空间更少。如果此 fallocate 调用失败,则会在错误日志中报告错误,并且不再使用 trim,服务器可以正常继续运行。
读取页面时,它在进入缓冲区池之前会进行解压缩。所有这一切都通过使用新的页面类型 FIL_PAGE_PAGE_COMPRESSED 来实现。每个页面都有一个 38 字节的 FIL 头部(FIL 是“file”的缩写形式)。头部包含一个用于指示页面类型的字段,该字段决定了页面其余部分的结构。新 FIL 头部结构如下:
- 图 2 中各项的更详细描述
- FIL_SPAGE_SPACE_OR_CHKSUM:目前不对压缩页面计算校验和,因为原始计算的校验和是压缩数据的一部分。因此,此字段初始化为值 BUF_NO_CHECKSUM_MAGIC。
- FIL_PAGE_OFFSET:页面号。
- FIL_PAGE_PREV 和 FIL_PAGE_NEXT:指向此页面类型的逻辑上一页和下一页的指针。
- FIL_PAGE_LSN:页面最后修改的 64 位日志序列号(LSN)。
- FIL_PAGE_TYPE:存储 FIL_PAGE_PAGE_COMPRESSED = 34354。
- FIL_PAGE_FILE_FLUSH_LSN:此处存储此页面使用的压缩算法。目前支持的值有 FIL_PAGE_COMPRESSION_ZLIB = 1 或 FIL_PAGE_COMPRESSION_LZ4 = 2。
- FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID:此处存储文件的空间 ID。
- FIL_PAGE_DATA:原始页面的实际压缩大小从此处开始存储,使用 2 字节。
最后,压缩格式的原始页面存储在位置 FIL_PAGE_DATA + FIL_PAGE_COMPRESSED_SIZE = 38+2=40。
注意,FIL 尾部不存储(它存储在原始压缩页面中)。这种新的页面类型允许表中的页面在同一文件空间中处于未压缩状态(如果压缩失败),使用 ZLIB 压缩或使用 LZ4 压缩。例如,页面 5 可以是未压缩的,页面 45 使用 ZLIB 压缩,页面 60 使用 LZ4 压缩。但是,当前实现只允许在单个文件空间内存在未压缩和压缩页面。LZ4 默认未编译,因为许多发行版默认没有此库。因此,如果想使用 LZ4,需要从 http://code.google.com/p/lz4/ 下载必要的源代码,编译并安装。之后需要从源码分发版编译 MariaDB。
在 MariaDB 中,我们实现了页面压缩,以便页面压缩、原子写入和使用的压缩级别可以按表进行配置。新的 create table 选项是使用引擎定义的表属性实现的(参见 https://mariadb.com/kb/en/engine-defined-new-tablefieldindex-attributes/)。这避免了向 SQL 语法添加不必要的扩展。
示例
CREATE TABLE T0 (A INTEGER) ENGINE=InnoDB PAGE_COMPRESSED=1;
-
CREATE TABLE A(B INTEGER) ENGINE=InnoDB PAGE_COMPRESSED=1 PAGE_COMPRESSION_LEVEL=6
-
CREATE TABLE B(C INTEGER) ENGINE=InnoDB ATOMIC_WRITES=ON;
-
CREATE TABLE t3 (a int KEY, b int) DATA DIRECTORY=’/dev/fioa’ PAGE_COMPRESSED=1 PAGE_COMPRESSION_LEVEL=4 ATOMIC_WRITES=’ON’;
现在我们有了一种新的页面类型,并且底层存储系统可以提供非常高的吞吐量,我们注意到 InnoDB 的页面刷新(page flushing)扩展性不好。这是因为 InnoDB 的页面刷新是单线程的。
解决方案:多线程刷新(Multi-threaded flush)
为了实现更好的吞吐量并降低操作延迟,我们在 InnoDB 内部开发了一种多线程刷新方法。可以通过配置参数 innodb_use_mtflush=1 启用此新功能,并使用 innodb_mtflush_threads= 配置使用的线程数。
这项新功能是按照传统的生产者-多个消费者概念实现的,例如:
- 工作任务插入到工作队列(wq)中
- 完成基于执行的操作类型,结果是 WRITE
压缩/刷新操作的完成结果被发送到返回队列 wr_cq。
实际生产者(单线程)工作方式如下:
loop: sleep so that one iteration takes roughly one second flush LRU: for each buffer pool instance send a work item to flush LRU scan depth pages in chunks send work items to multi-threaded flush work threads wait until we have received reply for all work items flush flush list: calculate target number of pages to flush for each instance set up a work item to flush (target number / # of instances) number pages send work items to multi-threaded flush work thread wait until we have received reply for all items
消费者工作方式如下:
loop (until not shutdown): wait until a work item is received from work queue if work_item->type is EXIT insert a reply message to return queue pthread_exit(); if work_item->type is WRITE call buf_mtflu_flush_pool_instance() for this work_item when we reach to os layer (os0file.cc) we compress the page if table uses page compression (fil0pagecompress.cc) set up reply message containin number of flushed pages insert a reply message to return queue
这意味着我们可以并行地压缩/解压缩页面。
基准测试(Benchmarks)
首先,我们使用了 LinkBench [1],它基于 Facebook(一个主要的社交网络)生产数据库中存储“社交图谱”数据的跟踪记录。LinkBench 为社交和 Web 服务数据的持久存储提供了现实且具有挑战性的测试。我们使用了以下配置进行页面压缩测试:
[mysqld] innodb_buffer_pool_size = 50G innodb_use_native_aio = 1 innodb_use_mtflush = 1 innodb_file_per_table = 1 innodb_doublewrite = 0 innodb_use_fallocate = 1 innodb_use_atomic_writes = 1 innodb_use_trim = 1 innodb_buffer_pool_instances = 16 innodb_mtflush_threads = 16 innodb_use_lz4=1 innodb_flush_method = O_DIRECT innodb_thread_concurrency = 32 innodb_write_io_threads = 32 innodb_read_io_threads = 32 innodb_file_format=barracuda innodb_lru_scan_depth=2000 innodb_io_capacity=30000 innodb_io_capacity_max=35000
[mysqld]
innodb_use_atomic_writes=1
innodb_use_mtflush=1
innodb_mtflush_threads=16
innodb_thread_concurrency=32
innodb_adaptive_flushing=1
innodb_log_buffer_size=16777216
innodb_log_file_size=1073741824
innodb_buffer_pool_size=10000000000
innodb_io_capacity=10000
innodb_flush_method=ALL_O_DIRECT
innodb_autoextend_increment=1024
innodb_flush_log_at_trx_commit=0
innodb_read_io_threads=16
innodb_write_io_threads=16
innodb_use_trim=1
innodb_checksum_algorithm=crc32
innodb_file_format=Barracuda
innodb_file_per_table=1
innodb_flush_neighbors=0
skip-name-resolve
default-storage-engine=InnoDB
innodb_large_prefix=1
log-output=FILE
general-log=0
max_connections=200
log-bin=/mnt/fusion/binlog/mariadb-bin
binlog-format=ROW
server-id=1
tmpdir=/tmp/
innodb_adaptive_hash_index=0
innodb_buffer_pool_size = 50G innodb_use_native_aio = 1 innodb_use_mtflush = 0 innodb_file_per_table = 1 innodb_doublewrite = 1 innodb_use_fallocate = 1 innodb_use_atomic_writes = 0 innodb_use_trim = 0 innodb_buffer_pool_instances = 16 innodb_mtflush_threads = 0 innodb_use_lz4=0 innodb_flush_method = O_DIRECT innodb_thread_concurrency = 32 innodb_write_io_threads = 32 innodb_read_io_threads = 32 innodb_file_format=barracuda innodb_lru_scan_depth=2000 innodb_io_capacity=30000 innodb_io_capacity_max=35000
对于行压缩和未压缩表,我们使用了以下设置:
图 3:存储使用情况。
Linkbench 数据库按如下方式填充:
./bin/linkbench -D dbid=linkdb -D ohst=127.0.0.1 -D user=root -D password= -D maxid1=100000001 -c config/MyConfig.properties -l
linkbench -c config/LinkBenchConfig.properties -D num_warmup_ops=10000000 -D num_request_ops=100000000 -D num_loader_threads=32 -D num_request_threads=32 -D range=10000000000 -D max_nodes=10000000000 -D requests_per_interval=1000000 -D display_freq=1000 -D max_id2=10000000 -D node_start_id=0 -D base_node_id=0 -D max_num_nodes=10000000000 -D link_start_id=0 -D base_link_id=0 -D max_num_links=100000000000 -D max_link_chain_len=10 -D max_link_data_size=200 -D max_link_table_size=100000000000 -D max_obj_data_size=200 -D max_obj_table_size=10000000000 -D bulk_insert_size=10000 -D percent_deletes=0 -D percent_add_link=10 -D percent_delete_link=1 -D percent_update_link=1 -D percent_add_node=0 -D percent_update_node=2 -D percent_delete_node=0 -D percent_find_node=3 -D percent_find_link=60 -D percent_count_link=20 -D percent_range_link=3
nohup ./bin/linkbench -D dbid=linkdb -D ohst=127.0.0.1 -D user=root -D password= -D maxid1=100000001 -c config/MyConfig.properties -cvstats stats.cvs -cvsvtream stream.cvs -D requests=50000000 -D maxtime=21600 -r &
这些结果显示在图 4 中。
图 4:LinkBench 基准测试结果。
其次,我们使用了类似 TPC-C [2] 的基准测试。TPC Benchmark C 于 1992 年 7 月获得批准,是一个联机事务处理(OLTP)基准测试。由于其多种事务类型、更复杂的数据库和整体执行结构,TPC-C 比之前的 OLTP 基准测试(如 TPC-A)更为复杂。TPC-C 涉及五种不同类型和复杂度的并发事务的混合,这些事务要么在线执行,要么排队等待延迟执行。数据库由九种类型的表组成,记录和填充大小范围广泛。TPC-C 以每分钟事务数(tpmC)衡量。在下图图 5 中,MySQL 压缩实际上指的是 InnoDB 压缩表(即 ROW_FORMAT=COMPRESSED)。
此运行使用以下 TPC-C 设置:
tpcc_start -h localhost -P3306 -d tpcc1000 -r root -p "" -w 1000 -c 32 -r 30 -l 3600
图 5:TPC-C 结果。
结论
MariaDB 包含针对 Fusion-IO 设备的高端 SSD 优化,尤其旨在提高性能、延长设备寿命并改善压缩率
- 使用原子写入,开箱即用的性能提升约 30%,通过为 XtraDB 启用快速校验和,性能提升 50% [3]
- 页面压缩的引入解决了 MySQL InnoDB 行压缩中常见的复杂逻辑、重新压缩和页面拆分问题。通过使用页面压缩,压缩率更高,从而带来更好的性能,并且对磁盘的写入操作更少。
- 多线程刷新提供了更好的吞吐量,并降低了操作延迟,从而提升了性能
通过在 Fusion-IO 设备上启用这些功能,与 MySQL InnoDB 自身的压缩相比,将获得更好的压缩率、更少的磁盘写入以及显著的性能提升。
参考文献
[1] Timothy G. Armstrong, Vamsi Ponnekanti, Dhruba Borthakur, 和 Mark Callaghan。2013 年。LinkBench:一个基于 Facebook 社交图谱的数据库基准测试。载于《2013 ACM SIGMOD 国际数据管理大会论文集》(SIGMOD ’13)。ACM,美国纽约州纽约市,1185-1196 页。DOI=10.1145/2463676.2465296 http://doi.acm.org/10.1145/2463676.2465296。
[3] MairaDB 引入原子写入功能,https://blog.mariadb.org/mariadb-introduces-atomic-writes/
有趣的基准测试。当您提到使用 InnoDB 5.6.15 引擎时,我是否可以假定您指的是 InnoDB 插件而不是 XtraDB 插件?如果是这样,那么对于 10.0.9,默认值已从 InnoDB 插件切换回 XtraDB 插件 https://mariadb.com/kb/en/mariadb-1009-release-notes/,所以如果您切换回 10.0.9+ 以及更高版本的 XtraDB 插件默认设置,上面的结果会如何变化,我很好奇?
在 10.0.9 中,InnoDB 是插件,结果也来自那里。我们也用 XtraDB 做了实验。然而,由于 XtraDB 内部的性能退步,我们决定不正式与 XtraDB 进行比较,因为 XtraDB 的性能结果不如 InnoDB 插件,即使是未压缩表,并且当前使用行格式压缩时,InnoDB 插件明显更优越。在我看来,与 InnoDB 插件进行比较更‘公平’。
还应该加上与 TokuDB 引擎的比较。考虑到它对 SSD 应该有很好的压缩和性能表现,看看它在这些测试中的表现会很有趣。
两个配置都有一个拼写错误
innodb_thread_concurrencty = 32
应该是
innodb_thread_concurrency = 32
我目前在 Windows 上使用 MariaDB 10.1.13,我想知道 xtradb 是否支持 Windows 上的压缩。我需要将 xtradb 换成 innodb 插件才能获得压缩功能吗?索引页面也会被压缩吗?
首先,您的配置和使用的压缩算法是什么。如果使用的压缩算法不是 zlib,您是从源代码编译的吗(zlib 是唯一捆绑的压缩算法方法)。是的,索引页面会被压缩。Windows 上支持压缩,您是如何发现没有发生压缩的?
Jan 您好,非常感谢您关注此事。我正在 Windows Server 2012 64 位系统上测试 MariaDB 10.1.13。使用 64k 页面重建 innodb 表,NTFS 是 4k 集群。不得不手动(通过 mysqldump)重新创建 performance 和 gtid 表,以消除缺失表的提示信息。想法是不打孔(或尽量避免)。我想我可以承受 64k 页面带来的开销,因为我当前的 myisam 表位于 Windows 的压缩文件夹中…… 难以承受的是,innodb 表+索引至少比我的 MyISAM 表大 3 倍……
压缩是 zlib(尝试了从 1 到 9 的压缩级别),安装程序是 MariaDB 网站上提供的默认 64 位安装程序
在 MySQL 上也检测到了这个问题。我设置了一个可重现的测试用例
https://bugs.mysql.com/bug.php?id=81145
在 MariaDB 上,我观察到 show status 显示 compression off,但在创建带压缩功能的表时没有警告消息。表是使用 page_compressed=1 PAGE_COMPRESSION_LEVEL=9(或 1..)创建的。
您是否有针对 Windows 的分步说明,我可以使用它来仔细检查我可能做错了什么?
谢谢!
您好,
我使用了 MariaDB 10.1.14 和以下命令:
set global innodb_compression_method=zlib;
set global innodb_file_format=Barracuda;
create table t1(a int not null primary key, b varchar(250)) engine=innodb page_compressed=yes;
insert into t1 values (1,’testingtestingcompression’);
insert into t1 values (2,’testingtestingcompression’);
之后我使用了 GnuWin32 工具中的 od 命令,我看到表确实被压缩了,也就是说,实际的 .ibd 文件内容不可
读。如果您仍然认为有问题,请在 JIRA 中提交 bug 报告,附带可重现的测试用例和完整的错误日志。
我会仔细检查并在 JIRA 中提交相应记录。我怀疑它写入的是一个包含压缩数据和末尾大量填充数据的 16k 页面,而不是与 NTFS 集群大小相同的 4K 页面。我确实需要仔细检查数据/索引页面结构。在我的情况下,我可以忍受处理严格的 4k 大小并处理页面数据溢出…… 总之,我处理 zlib level 1 时,它能给我节省 3 倍存储空间(压缩普通文件时)
记住,默认情况下 innodb_use_trim = 0,这意味着即使压缩载荷是例如 4K,也总是写入完整的 16K。这是因为 innodb_use_trim = 1 需要操作系统和文件系统支持 punch hole 功能,并且这是目前唯一可以在文件系统中留下孔洞的方式。
我明白了
我目前正在使用 innodb_use_fallocate=ON,但不是 innodb_use_trim=ON
所以,innodb_use_trim=ON 是用于 trim 和 punch_hole 的。
阅读
https://mariadb.com/kb/en/mariadb/compression/
让人觉得 innodb_use_fallocate=ON 足以启用 punch hole,配合
fallocate(file_handle, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, file_offset, remainder_len)
看不到 trim 的参与……
也许是 fallocate 的 bug?
在以下网址找到了一些信息
https://blogs.msdn.microsoft.com/oldnewthing/20101201-00/?p=12153
顺便说一下,lz4 是一个很棒的压缩算法。如果能默认包含在安装程序中就太好了……
MariaDB 适用于 Ubuntu Zesty 的仓库包现在默认包含 LZ4。
innodb_use_lz4 在 RDS 上不存在 🙁 🙁