借助 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 架构。

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 头部结构如下:

    page 图 2:FIL 头部中的新页面类型。

    1. 图 2 中各项的更详细描述
    2. FIL_SPAGE_SPACE_OR_CHKSUM:目前不对压缩页面计算校验和,因为原始计算的校验和是压缩数据的一部分。因此,此字段初始化为值 BUF_NO_CHECKSUM_MAGIC。
    3. FIL_PAGE_OFFSET:页面号。
    4. FIL_PAGE_PREV 和 FIL_PAGE_NEXT:指向此页面类型的逻辑上一页和下一页的指针。
    5. FIL_PAGE_LSN:页面最后修改的 64 位日志序列号(LSN)。
    6. FIL_PAGE_TYPE:存储 FIL_PAGE_PAGE_COMPRESSED = 34354。
    7. FIL_PAGE_FILE_FLUSH_LSN:此处存储此页面使用的压缩算法。目前支持的值有 FIL_PAGE_COMPRESSION_ZLIB = 1 或 FIL_PAGE_COMPRESSION_LZ4 = 2。
    8. FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID:此处存储文件的空间 ID。
    9. 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 中。

     

    linkbench_measure

    图 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

    tpcc

    图 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

    [2] http://www.tpc.org/tpcc/

    [3] MairaDB 引入原子写入功能,https://blog.mariadb.org/mariadb-introduces-atomic-writes/