MySQL 中的压缩技术

Author: 张林康

为什么要有这篇文章?

MySQL 中数据压缩技术主要有三种:表压缩,页压缩,列压缩。

在互联网上,关于页压缩的源码解析文章比较多,但是关于表压缩,列压缩的源码解析的文章处于空白状态,没有相关资料,这就为一些对压缩技术比较感兴趣的同学提出了一些挑战。

本文旨在通过对表压缩,页压缩,列压缩的源码进行解析,同时做出使用上的说明,以填补这部分的空白。

1. 综述

数据压缩可以减少存储空间,降低存储成本,增加 IO 效率,是降低数据库整体使用成本的重要手段,MySQL 目前具备的压缩能力,包括 InnoDB 存储引擎层提供的 表数据的压缩,以及在 Server 层实现的 binlog 日志压缩两种。

MySQL 中有一个分支 MyRocks 对压缩的支持比较多 IO 也比较优秀,被 Facebook 大规模使用,本文不做重点分析,对 MyRocks 感兴趣可以参考网易 MyRocks 使用和优化。

本文主要介绍 MySQL 中 InnoDB 存储引擎层的压缩,本文所有代码基于 MySQL 8.0.28。

1.1 MySQL 压缩要解决的问题

笔者认为,MySQL 的数据压缩要解决两个问题,第一个问题是通过压缩把数据需要的存储空间减少;第二个问题是对压缩后剩余空间的利用。

MySQL 中的表压缩解决第一个问题使用的方法是通过 zlib 压缩算法提供的接口,通过 Zlib 算法的压缩以及解压使得数据占有的存储空间减少;通过 KEY_BLOCK_SIZE 值的设置,如果成功压缩,就可以把一个页面中 KEY_BLOCK_SIZE 之外的空间通过 MySQL 的调度得以使用。

MySQL 中的页压缩解决第一个问题的方法是通过 zlib 以及 lz4 压缩算法提供的接口来实现,一个 MySQL 页面可以占用更少的操作系统页面;通过操作系统的 punching hole 功能把剩余空间得以调度以及使用。

2 MySQL 中的表压缩

本节主要包括两部分:表压缩的使用与表压缩的代码实现。

2.1 表压缩的使用

2.1.1 如何创建一个压缩表

在 file_per_table 的表空间或者 general 表空间里,可以使用表压缩,系统表不支持对表级别的压缩;用户在设置好 innodb_file_format 之后(仅支持 Barracuda ),再把 ROW_FORMAT (COMPRESSED)与 KEY_BLOCK_SIZE 都设置为对应的值,可以启用表压缩。

file_per_table space 创建压缩表:

  1. mysql> SET GLOBAL innodb_file_per_table=1;
  2. mysql> ## 5.7 设置, 8.0 取消此参数,5.7 默认值 Barracuda
  3. mysql> SET GLOBAL innodb_file_format=Barracuda;
  4. mysql> CREATE TABLE t1 (c1 INT PRIMARY KEY)
  5. mysql> ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE=8;

general space 创建压缩表:

  1. mysql> CREATE TABLESPACE `ts2` ADD DATAFILE 'ts2.ibd'
  2. mysql> FILE_BLOCK_SIZE = 8192 Engine=InnoDB;
  3. mysql> CREATE TABLE t4 (c1 INT PRIMARY KEY)
  4. mysql> TABLESPACE ts2 ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE=8;

官方文档中对表压缩参数设置限制了限制,最常用的是 KEY_BLOCK_SIZE = 8K.

2.1.2 压缩表使用的限制

1.KEY_BLOCK_SIZE 原则上不超过 innodb_page_size;但也不能过小,如果 KEY_BLOCK_SIZE 如果指定的值太小,则当数据值无法压缩到足以容纳每页中的多行时,重新组织页面会产生额外的开销;因此会有硬性的规定,KEY_BLOCK_SIZE 的值如果太小,会导致 CREATE 或者 ALTER SQL 执行失败。

2.在使用压缩表的时候,可以适当调大 buffer_pool_size ,以增强性能。

3.FILE_BLOCK_SIZE 没有设置的时候,默认值是 innodb_page_size ,这时候不允许使用 COMPREESSION 功能。

2.1.3 表压缩参数

参数 参数说明
innodb_compression_failure_threshold_pctGLOBAL当压缩失败的次数 / 总压缩次数达到该值时,MySQL 会动态增加每个页面的额外可用空间,以此来提高压缩成功率
innodb_compression_pad_pct_maxGLOBAL每个页面中预留空间占总空间的比例
innodb_compression_levelGLOBAL压缩时使用的 zlib 算法的压缩级别(0-9 , default 6),更高的压缩级别意味着更高的压缩率与更高的 CPU 消耗
innodb_log_compressed_pagesGLOBAL是否在 redolog 中记载页面的 re-compression 信息,default ON
   

2.1.4 表压缩监测

全部压缩表的性能监测可以在 INNODB_CMP 以及 INNODB_CMP_RESET 表中看到;单独压缩表的性能监测数据可以在 INNODB_CMP_PER_INDEX 以及 INNODB_CMP_PER_INDEX_RESET 表中看到。

INNODB_CMP 表跟 INNODB_CMP_RESET 表存储的信息都是全部压缩表的数据,这两个表里面的所有字段均一致,不同的是 INNODB_CMP_RESET 表在每次查询之后都会进行一次清零操作,所以 INNODB_CMP_RESET 表查询到的是一段时间的数据,INNODB_CMP 表查询到的是 MySQL 服务启动至今的数据。

INNODB_CMP_PER_INDEX 表提供更细粒度的表级别的压缩数据,本文会给出查询对应表的示例。

2.2 表压缩的实现

2.2.1 buffer_pool 中对表压缩的支持

从buffer_pool中获取一个压缩页的过程是:从磁盘上把压缩页取出 (buf_buddy_alloc),取出之后,把page的状态置为 BUF_BLOCK_ZIP_PAGE。page被解压之后,状态被置为 BUF_BLOCK_FILE_PAGE,同时加入unzip_LRU中。如果内存资源紧张,解压页将会被回收,如果这个时候page无更新,状态是BUF_BLOCK_ZIP_PAGE,否则状态是BUF_BLOCK_ZIP_DIRTY。

  1. struct buf_pool_t {
  2. /** zip_hash mutex */
  3. BufListMutex zip_hash_mutex;
  4. /** 压缩页链表 */
  5. UT_LIST_BASE_NODE_T(buf_page_t, zip_list) zip_list;
  6. /** Hash table of buf_block_t blocks whose frames are allocated to the zip
  7. buddy system, indexed by block->frame */
  8. hash_table_t *zip_hash;
  9. /** 解压后的压缩页链表 */
  10. UT_LIST_BASE_NODE_T(buf_block_t, unzip_LRU) unzip_LRU;
  11. /** 记录没被修改过的压缩页,debug 模式下才有 */
  12. UT_LIST_BASE_NODE_T(buf_page_t, list) zip_clean;
  13. /** 支持压缩表的 free lists, 不同页面大小对应数组的不同值 */
  14. UT_LIST_BASE_NODE_T(buf_buddy_free_t, list) zip_free[BUF_BUDDY_SIZES_MAX];
  15. }
  1. struct buf_block_t {
  2. /* page 页的元信息 */
  3. buf_page_t page;
  4. /* 真正存储数据的 page */
  5. byte *frame;
  6. };
  1. /* page 的一些元信息 */
  2. class buf_page_t {
  3. /* 压缩页 */
  4. page_zip_des_t zip;
  5. /* 是否在 buf_pool->zip_hash 链表中 */
  6. bool in_zip_hash;
  7. };
  1. /** 描述压缩页结构体 */
  2. struct page_zip_des_t {
  3. /** 压缩页数据 */
  4. page_zip_t *data;
  5. /** modification log 起始偏移量 */
  6. uint16_t m_start;
  7. /** modification log 终结偏移量 */
  8. uint16_t m_end;
  9. /** modification log 是否为空 */
  10. bool m_nonempty;
  11. /** 表示压缩页大小 */
  12. uint8_t ssize;
  13. };

2.2.2 B-tree 对压缩表的支持

B-tree 压缩表的数据插入的函数逻辑跟非压缩表的数据插入逻辑在 page_cur_tuple_insert函数之前都是一样的,不同的是,在 page_cur_tuple_insert函数的执行过程中,压缩表的情况会进入 page_cur_insert_rec_zip函数,这个函数会同时更新压缩页与非压缩页的数据。更新压缩页数据的时候,不会直接更新记录,而是会将更新信息记载到压缩页的modifition log(压缩页中负责记录修改信息的log,存储在压缩页的末尾)中。

具体的压缩表数据插入逻辑:

  1. 数据插入主要函数:
  2. /* 向 clust_index 中插入一条数据 */
  3. -->row_ins_clust_index_entry_low
  4. /* 乐观插入 */
  5. --> btr_cur_optimistic_insert
  6. /* 向页面中插入 record, 成功的话返回这条record, 失败的话返回 NULL */
  7. --> page_cur_tuple_insert
  8. /* 同时在压缩页与非压缩页插入一条记录 */
  9. --> page_cur_insert_rec_zip
  10. /* 向 record 中写入 offset */
  11. --> page_zip_dir_insert
  12. /* 写入压缩页面 */
  13. --> page_zip_write_rec
  1. rec_t *page_cur_insert_rec_zip(
  2. page_cur_t *cursor, /*!< in/out: page cursor */
  3. dict_index_t *index, /*!< in: record descriptor */
  4. const rec_t *rec, /*!< in: pointer to a physical record */
  5. ulint *offsets, /*!< in/out: rec_get_offsets(rec, index) */
  6. mtr_t *mtr) /*!< in: mini-transaction handle, or NULL */
  7. {
  8. {
  9. ...
  10. }
  11. /* 1. Get the size of the physical record in the page */
  12. {
  13. ...
  14. }
  15. /* 2. Try to find suitable space from page memory management */
  16. {
  17. ...
  18. }
  19. /* 3. Create the record */
  20. insert_rec = rec_copy(insert_buf, rec, offsets);
  21. rec_offs_make_valid(insert_rec, index, offsets);
  22. /* 4. Insert the record in the linked list of records */
  23. ut_ad(cursor->rec != insert_rec);
  24. {
  25. /* next record after current before the insertion */
  26. const rec_t *next_rec = page_rec_get_next_low(cursor->rec, TRUE);
  27. ...
  28. page_rec_set_next(insert_rec, next_rec);
  29. page_rec_set_next(cursor->rec, insert_rec);
  30. }
  31. page_header_set_field(page, page_zip, PAGE_N_RECS, 1 + page_get_n_recs(page));
  32. /* 5. Set the n_owned field in the inserted record to zero,
  33. and set the heap_no field */
  34. rec_set_n_owned_new(insert_rec, nullptr, 0);
  35. rec_set_heap_no_new(insert_rec, heap_no);
  36. UNIV_MEM_ASSERT_RW(rec_get_start(insert_rec, offsets),
  37. rec_offs_size(offsets));
  38. page_zip_dir_insert(page_zip, cursor->rec, free_rec, insert_rec);
  39. /* 6. Update the last insertion info in page header */
  40. last_insert = page_header_get_ptr(page, PAGE_LAST_INSERT);
  41. ut_ad(!last_insert || rec_get_node_ptr_flag(last_insert) ==
  42. rec_get_node_ptr_flag(insert_rec));
  43. if (!dict_index_is_spatial(index)) {
  44. if (UNIV_UNLIKELY(last_insert == nullptr)) {
  45. page_header_set_field(page, page_zip, PAGE_DIRECTION, PAGE_NO_DIRECTION);
  46. page_header_set_field(page, page_zip, PAGE_N_DIRECTION, 0);
  47. } else if ((last_insert == cursor->rec) &&
  48. (page_header_get_field(page, PAGE_DIRECTION) != PAGE_LEFT)) {
  49. page_header_set_field(page, page_zip, PAGE_DIRECTION, PAGE_RIGHT);
  50. page_header_set_field(page, page_zip, PAGE_N_DIRECTION,
  51. page_header_get_field(page, PAGE_N_DIRECTION) + 1);
  52. } else if ((page_rec_get_next(insert_rec) == last_insert) &&
  53. (page_header_get_field(page, PAGE_DIRECTION) != PAGE_RIGHT)) {
  54. page_header_set_field(page, page_zip, PAGE_DIRECTION, PAGE_LEFT);
  55. page_header_set_field(page, page_zip, PAGE_N_DIRECTION,
  56. page_header_get_field(page, PAGE_N_DIRECTION) + 1);
  57. } else {
  58. page_header_set_field(page, page_zip, PAGE_DIRECTION, PAGE_NO_DIRECTION);
  59. page_header_set_field(page, page_zip, PAGE_N_DIRECTION, 0);
  60. }
  61. }
  62. page_header_set_ptr(page, page_zip, PAGE_LAST_INSERT, insert_rec);
  63. /* 7. It remains to update the owner record. */
  64. {
  65. rec_t *owner_rec = page_rec_find_owner_rec(insert_rec);
  66. ulint n_owned;
  67. n_owned = rec_get_n_owned_new(owner_rec);
  68. rec_set_n_owned_new(owner_rec, page_zip, n_owned + 1);
  69. /* 8. Now we have incremented the n_owned field of the owner
  70. record. If the number exceeds PAGE_DIR_SLOT_MAX_N_OWNED,
  71. we have to split the corresponding directory slot in two. */
  72. if (UNIV_UNLIKELY(n_owned == PAGE_DIR_SLOT_MAX_N_OWNED)) {
  73. page_dir_split_slot(page, page_zip, page_dir_find_owner_slot(owner_rec));
  74. }
  75. }
  76. /* 9. write compressed page */
  77. page_zip_write_rec(page_zip, insert_rec, index, offsets, 1);
  78. /* 10. Write log record of the insert */
  79. if (UNIV_LIKELY(mtr != nullptr)) {
  80. page_cur_insert_rec_write_log(insert_rec, rec_size, cursor->rec, index,
  81. mtr);
  82. }
  83. return (insert_rec);
  84. }

创建压缩页的代码逻辑:

  1. 压缩页创建主要函数:
  2. /* 创建一个压缩页 */
  3. -->page_create_zip
  4. /* 创建一个页的底层函数,填充文件头等 */
  5. -->page_create_low
  6. /* 写入一条 MLOG_ZIP_PAGE_COMPRESS 类型的 redolog */
  7. --> page_zip_compress
  1. /** Compress a page.
  2. @return true on success, false on failure; page_zip will be left
  3. intact on failure. */
  4. ibool page_zip_compress(page_zip_des_t *page_zip, /*!< in: size; out: data,
  5. n_blobs, m_start, m_end,
  6. m_nonempty */
  7. const page_t *page, /*!< in: uncompressed page */
  8. dict_index_t *index, /*!< in: index tree */
  9. ulint level, /*!< in: commpression level */
  10. mtr_t *mtr)
  11. {
  12. ...
  13. err = deflateInit2(&c_stream, static_cast<int>(level), Z_DEFLATED,
  14. UNIV_PAGE_SIZE_SHIFT, MAX_MEM_LEVEL, Z_DEFAULT_STRATEGY);
  15. ...
  16. if (mtr) {
  17. page_zip_compress_write_log(page_zip, page, index, mtr);
  18. }
  19. ...
  20. const auto time_diff = std::chrono::duration_cast<std::chrono::microseconds>(
  21. std::chrono::steady_clock::now() - start_time);
  22. page_zip_stat[page_zip->ssize - 1].compressed_ok++;
  23. page_zip_stat[page_zip->ssize - 1].compress_time += time_diff;
  24. if (cmp_per_index_enabled) {
  25. mutex_enter(&page_zip_stat_per_index_mutex);
  26. page_zip_stat_per_index[ind_id].compressed_ok++;
  27. page_zip_stat_per_index[ind_id].compress_time += time_diff;
  28. mutex_exit(&page_zip_stat_per_index_mutex);
  29. }
  30. return (TRUE);
  31. }

2.2.3 redolog 对 压缩表的支持

redolog 冲对压缩页面的处理跟普通的页面有所区别,压缩页面在 redo 中的处理是:把系统信息列存入固定的位置,比如 trx_id 等信息;然后把数据列的修改写入 modified log 中,部分元数据信息也会经过 mlog_open_and_write_index 存储在 redolog 中,列数,定长列信息,变长列信息等。

redolog 类型 
MLOG_ZIP_WRITE_NODE_PTR在非叶结点上写入别的 page 的指针
MLOG_ZIP_WRITE_BLOB_PTR写入 blob 页的指针
MLOG_ZIP_WRITE_HEADER写页头部
MLOG_ZIP_PAGE_COMPRESS压缩页
MLOG_ZIP_PAGE_COMPRESS_NO_DATA压缩空的页
MLOG_ZIP_PAGE_REORGANIZE重新组织页面
  
  1. static byte *recv_parse_or_apply_log_rec_body(
  2. mlog_id_t type, byte *ptr, byte *end_ptr, space_id_t space_id,
  3. page_no_t page_no, buf_block_t *block, mtr_t *mtr, ulint parsed_bytes,
  4. lsn_t start_lsn) {
  5. ...
  6. ...
  7. case MLOG_ZIP_WRITE_NODE_PTR:
  8. ut_ad(!page || fil_page_type_is_index(page_type));
  9. ptr = page_zip_parse_write_node_ptr(ptr, end_ptr, page, page_zip);
  10. break;
  11. case MLOG_ZIP_WRITE_BLOB_PTR:
  12. ut_ad(!page || fil_page_type_is_index(page_type));
  13. ptr = page_zip_parse_write_blob_ptr(ptr, end_ptr, page, page_zip);
  14. break;
  15. case MLOG_ZIP_WRITE_HEADER:
  16. ut_ad(!page || fil_page_type_is_index(page_type));
  17. ptr = page_zip_parse_write_header(ptr, end_ptr, page, page_zip);
  18. break;
  19. case MLOG_ZIP_PAGE_COMPRESS:
  20. /* Allow anything in page_type when creating a page. */
  21. ptr = page_zip_parse_compress(ptr, end_ptr, page, page_zip);
  22. break;
  23. case MLOG_ZIP_PAGE_COMPRESS_NO_DATA:
  24. if (nullptr != (ptr = mlog_parse_index(ptr, end_ptr, true, &index))) {
  25. ut_a(!page || ((ibool) !!page_is_comp(page) ==
  26. dict_table_is_comp(index->table)));
  27. ptr = page_zip_parse_compress_no_data(ptr, end_ptr, page, page_zip,
  28. index);
  29. }
  30. break;
  31. ...
  32. }

2.2.4 压缩算法

  1. /* zip 算法默认的压缩级别 */
  2. #define DEFAULT_COMPRESSION_LEVEL 6
  3. /* zip 算法的压缩级别,默认是上面的 6 ,可以取 0-9 */
  4. uint page_zip_level = DEFAULT_COMPRESSION_LEVEL;
  1. /* 值得关注的是,表压缩跟页压缩使用相同的参数控制 zip 算法的压缩级别 */
  2. static MYSQL_SYSVAR_UINT(
  3. compression_level, page_zip_level, PLUGIN_VAR_RQCMDARG,
  4. "Compression level used for compressed row format. 0 is no compression"
  5. ", 1 is fastest, 9 is best compression and default is 6.",
  6. nullptr, nullptr, DEFAULT_COMPRESSION_LEVEL, 0, 9, 0);

3. MySQL 中的页压缩

3.1 页压缩的使用

在 file_per_table 的表空间里,在 设置 innodb_file_per_table 参数设置为 ON 之后,InnoDB 支持表空间中的表的页级别的压缩,这个功能叫页压缩,也叫透明压缩。用户的表结构中有一个选项 COMPRESSION ,可以通过设置该值来控制该表对应的页面是否会进行压缩;目前 MySQL 对透明压缩的支持算法包括 zlib,lz4 两种压缩算法。

3.1.1 页压缩的创建与禁用

页压缩的使用有两种方式,一种方式是通过 CREATE TABLE 的方式,另一种方式是通过 ALTER TABLE 的方式;只需要把 COMPRESSION 选项设置为 NONE,就可以禁用页压缩功能。

3.1.2 页压缩的监控

  1. /* 通过 CREATE TABLE 的方式来创建页压缩表 */
  2. CREATE TABLE t1 (c1 INT) COMPRESSION="zlib";
  3. /* 通过 ALTER TABLE 的方式来创建压缩表 */
  4. ALTER TABLE t1 COMPRESSION="zlib";
  5. OPTIMIZE TABLE t1;
  6. /* 通过设置 COMPRESSION 的方式禁用 压缩表*/
  7. ALTER TABLE t1 COMPRESSION="None";
  8. OPTIMIZE TABLE t1;
监控项含义
FS_BLOCK_SIZE打孔使用的单位大小
FILE_SIZE表示文件的最大大小,未压缩
ALLOCATED_SIZE磁盘上分配的空间量
  1. # Create the employees table with Zlib page compression
  2. CREATE TABLE employees (
  3. emp_no INT NOT NULL,
  4. birth_date DATE NOT NULL,
  5. first_name VARCHAR(14) NOT NULL,
  6. last_name VARCHAR(16) NOT NULL,
  7. gender ENUM ('M','F') NOT NULL,
  8. hire_date DATE NOT NULL,
  9. PRIMARY KEY (emp_no)
  10. ) COMPRESSION="zlib";
  11. # Insert data (not shown)
  12. # Query page compression metadata in INFORMATION_SCHEMA.INNODB_TABLESPACES
  13. mysql> SELECT SPACE, NAME, FS_BLOCK_SIZE, FILE_SIZE, ALLOCATED_SIZE FROM
  14. INFORMATION_SCHEMA.INNODB_TABLESPACES WHERE NAME='employees/employees'\G
  15. *************************** 1. row ***************************
  16. SPACE: 45
  17. NAME: employees/employees
  18. FS_BLOCK_SIZE: 4096
  19. FILE_SIZE: 23068672
  20. ALLOCATED_SIZE: 19415040

3.2 页压缩的实现

页压缩的主要代码逻辑是在 file read 以及 file wirte 的时候完成的,对原来的代码侵入性不大。 3.2.1 页面压缩的逻辑

页面压缩的整体逻辑是先计算页面压缩后的大小,如果压缩后的大小比未压缩的大小至少小 1 个 block,执行压缩。否则,不会执行压缩。如果需要执行压缩,需要把压缩后的页面大小,以及压缩信息记录到页面头部。同时页面压缩,只压缩数据本身跟数据尾部,不压缩数据头部。

  1. /** Compress a data page
  2. @param[in] compression Compression algorithm
  3. @param[in] block_size File system block size
  4. @param[in] src Source contents to compress
  5. @param[in] src_len Length in bytes of the source
  6. @param[out] dst Compressed page contents
  7. @param[out] dst_len Length in bytes of dst contents
  8. @return buffer data, dst_len will have the length of the data */
  9. byte *os_file_compress_page(Compression compression, ulint block_size,
  10. byte *src, ulint src_len, byte *dst,
  11. ulint *dst_len) {
  12. ulint len = 0;
  13. ulint compression_level = page_zip_level;
  14. ulint page_type = mach_read_from_2(src + FIL_PAGE_TYPE);
  15. /* Must compress to <= N-1 FS blocks. */
  16. ulint out_len = src_len - (FIL_PAGE_DATA + block_size);
  17. /* This is the original data page size - the page header. */
  18. ulint content_len = src_len - FIL_PAGE_DATA;
  19. /* Only compress the data + trailer, leave the header alone */
  20. switch (compression.m_type) {
  21. case Compression::NONE:
  22. ut_error;
  23. case Compression::ZLIB: {
  24. uLongf zlen = static_cast<uLongf>(out_len);
  25. if (compress2(dst + FIL_PAGE_DATA, &zlen, src + FIL_PAGE_DATA,
  26. static_cast<uLong>(content_len),
  27. static_cast<int>(compression_level)) != Z_OK) {
  28. *dst_len = src_len;
  29. return (src);
  30. }
  31. len = static_cast<ulint>(zlen);
  32. break;
  33. }
  34. case Compression::LZ4:
  35. len = LZ4_compress_default(reinterpret_cast<char *>(src) + FIL_PAGE_DATA,
  36. reinterpret_cast<char *>(dst) + FIL_PAGE_DATA,
  37. static_cast<int>(content_len),
  38. static_cast<int>(out_len));
  39. ut_a(len <= src_len - FIL_PAGE_DATA);
  40. if (len == 0 || len >= out_len) {
  41. *dst_len = src_len;
  42. return (src);
  43. }
  44. break;
  45. case Compression::ZSTD:
  46. len = ZSTD_compress(dst + FIL_PAGE_DATA, out_len, src + FIL_PAGE_DATA, content_len, page_compress_zstd_level);
  47. break;
  48. default:
  49. *dst_len = src_len;
  50. return (src);
  51. }
  52. ut_a(len <= out_len);
  53. ut_ad(memcmp(src + FIL_PAGE_LSN + 4,
  54. src + src_len - FIL_PAGE_END_LSN_OLD_CHKSUM + 4, 4) == 0);
  55. /* Copy the header as is. */
  56. memmove(dst, src, FIL_PAGE_DATA);
  57. /* Add compression control information. Required for decompressing. */
  58. mach_write_to_2(dst + FIL_PAGE_TYPE, FIL_PAGE_COMPRESSED);
  59. mach_write_to_1(dst + FIL_PAGE_VERSION, Compression::FIL_PAGE_VERSION_2);
  60. mach_write_to_1(dst + FIL_PAGE_ALGORITHM_V1, compression.m_type);
  61. mach_write_to_2(dst + FIL_PAGE_ORIGINAL_TYPE_V1, page_type);
  62. mach_write_to_2(dst + FIL_PAGE_ORIGINAL_SIZE_V1, content_len);
  63. mach_write_to_2(dst + FIL_PAGE_COMPRESS_SIZE_V1, len);
  64. /* Round to the next full block size */
  65. len += FIL_PAGE_DATA;
  66. *dst_len = ut_calc_align(len, block_size);
  67. ut_ad(*dst_len >= len && *dst_len <= out_len + FIL_PAGE_DATA);
  68. /* Clear out the unused portion of the page. */
  69. if (len % block_size) {
  70. memset(dst + len, 0x0, block_size - (len % block_size));
  71. }v c sa
  72. return (dst);
  73. }

3.2.2 页面压缩调用链

01.svg

3.2.3 hole punching

hole punching 的代码主要在 os 的函数 os_file_io 中进行调用。最终调用函数 os_file_punch_hole 来实现打洞,而 os_file_punch_hole 函数最底层调用了 OS 的 fallocate 接口进行实现。

Linux 官方文档中有 fallocate 的接口具体说明 点击链接

  1. /** Decompress after a read and punch a hole in the file if it was a write
  2. @param[in] type IO context
  3. @param[in] fh Open file handle
  4. @param[in,out] buf Buffer to transform
  5. @param[in,out] scratch Scratch area for read decompression
  6. @param[in] src_len Length of the buffer before compression
  7. @param[in] offset file offset from the start where to read
  8. @param[in] len Compressed buffer length for write and size
  9. of buf len for read
  10. @return DB_SUCCESS or error code */
  11. static dberr_t os_file_io_complete(const IORequest &type, os_file_t fh,
  12. byte *buf, byte *scratch, ulint src_len,
  13. os_offset_t offset, ulint len) {
  14. dberr_t ret = DB_SUCCESS;
  15. /* We never compress/decompress the first page */
  16. ut_a(offset > 0);
  17. ut_ad(type.validate());
  18. if (!type.is_compression_enabled()) {
  19. if (type.is_log() && offset >= LOG_FILE_HDR_SIZE) {
  20. Encryption encryption(type.encryption_algorithm());
  21. ret = encryption.decrypt_log(type, buf, src_len, scratch, len);
  22. }
  23. return (ret);
  24. } else if (type.is_read()) {
  25. ut_ad(!type.is_row_log());
  26. Encryption encryption(type.encryption_algorithm());
  27. ret = encryption.decrypt(type, buf, src_len, scratch, len);
  28. if (ret == DB_SUCCESS) {
  29. return (os_file_decompress_page(type.is_dblwr(), buf, scratch, len));
  30. } else {
  31. return (ret);
  32. }
  33. } else if (type.punch_hole()) {
  34. ut_ad(len <= src_len);
  35. ut_ad(!type.is_log());
  36. ut_ad(type.is_write());
  37. ut_ad(type.is_compressed());
  38. /* Nothing to do. */
  39. if (len == src_len) {
  40. return (DB_SUCCESS);
  41. }
  42. #ifdef UNIV_DEBUG
  43. const ulint block_size = type.block_size();
  44. #endif /* UNIV_DEBUG */
  45. /* We don't support multiple page sizes in the server
  46. at the moment. */
  47. ut_ad(src_len == srv_page_size);
  48. /* Must be a multiple of the compression unit size. */
  49. ut_ad((len % block_size) == 0);
  50. ut_ad((offset % block_size) == 0);
  51. ut_ad(len + block_size <= src_len);
  52. offset += len;
  53. return (os_file_punch_hole(fh, offset, src_len - len));
  54. }
  55. ut_ad(!type.is_log());
  56. return (DB_SUCCESS);
  57. }
  1. /** Free storage space associated with a section of the file.
  2. @param[in] fh Open file handle
  3. @param[in] off Starting offset (SEEK_SET)
  4. @param[in] len Size of the hole
  5. @return DB_SUCCESS or error code */
  6. dberr_t os_file_punch_hole(os_file_t fh, os_offset_t off, os_offset_t len) {
  7. /* In this debugging mode, we act as if punch hole is supported,
  8. and then skip any calls to actually punch a hole here.
  9. In this way, Transparent Page Compression is still being tested. */
  10. DBUG_EXECUTE_IF("ignore_punch_hole", return (DB_SUCCESS););
  11. #ifdef _WIN32
  12. return (os_file_punch_hole_win32(fh, off, len));
  13. #else
  14. return (os_file_punch_hole_posix(fh, off, len));
  15. #endif /* _WIN32 */
  16. }

3.2.4 解压缩逻辑

  1. /** Decompress the page data contents. Page type must be FIL_PAGE_COMPRESSED, if
  2. not then the source contents are left unchanged and DB_SUCCESS is returned.
  3. @param[in] dblwr_read true if double write recovery in progress
  4. @param[in,out] src Data read from disk, decompressed data will be
  5. copied to this page
  6. @param[in,out] dst Scratch area to use for decompression or
  7. nullptr.
  8. @param[in] dst_len If dst is valid, size of the scratch area in
  9. bytes.
  10. @return DB_SUCCESS or error code */
  11. dberr_t Compression::deserialize(bool dblwr_read, byte *src, byte *dst,
  12. ulint dst_len) {
  13. if (!is_compressed_page(src)) {
  14. /* There is nothing we can do. */
  15. return (DB_SUCCESS);
  16. }
  17. meta_t header;
  18. deserialize_header(src, &header);
  19. byte *ptr = src + FIL_PAGE_DATA;
  20. if (!is_valid_page_version(header.m_version) ||
  21. header.m_original_size < UNIV_PAGE_SIZE_MIN - (FIL_PAGE_DATA + 8) ||
  22. header.m_original_size > UNIV_PAGE_SIZE_MAX - FIL_PAGE_DATA) {
  23. return DB_CORRUPTION;
  24. }
  25. if (dst != nullptr && dst_len < header.m_original_size + FIL_PAGE_DATA) {
  26. /* The caller can retry with a larger buffer. */
  27. return DB_OVERFLOW;
  28. }
  29. ut_ad(dst == nullptr || dst_len == header.m_original_size + FIL_PAGE_DATA);
  30. // FIXME: We should use TLS for this and reduce the malloc/free
  31. bool allocated;
  32. /* The caller doesn't know what to expect */
  33. if (dst == nullptr) {
  34. /* Add a safety margin of an additional 50% */
  35. ulint n_bytes = header.m_original_size + (header.m_original_size / 2);
  36. dst = reinterpret_cast<byte *>(
  37. ut::malloc_withkey(UT_NEW_THIS_FILE_PSI_KEY, n_bytes));
  38. if (dst == nullptr) {
  39. return (DB_OUT_OF_MEMORY);
  40. }
  41. allocated = true;
  42. } else {
  43. allocated = false;
  44. }
  45. int ret;
  46. Compression compression;
  47. ulint len = header.m_original_size;
  48. compression.m_type = static_cast<Compression::Type>(header.m_algorithm);
  49. switch (compression.m_type) {
  50. case Compression::ZLIB: {
  51. uLongf zlen = header.m_original_size;
  52. if (uncompress(dst, &zlen, ptr, header.m_compressed_size) != Z_OK) {
  53. if (allocated) {
  54. ut::free(dst);
  55. }
  56. return (DB_IO_DECOMPRESS_FAIL);
  57. }
  58. ut_ad(zlen <= len);
  59. len = static_cast<ulint>(zlen);
  60. break;
  61. }
  62. case Compression::LZ4: {
  63. if (dblwr_read) {
  64. ret = LZ4_decompress_safe(
  65. reinterpret_cast<char *>(ptr), reinterpret_cast<char *>(dst),
  66. header.m_compressed_size, header.m_original_size);
  67. } else {
  68. /* This can potentially read beyond the input
  69. buffer if the data is malformed. According to
  70. the LZ4 documentation it is a little faster
  71. than the above function. When recovering from
  72. the double write buffer we can afford to us the
  73. slower function above. */
  74. ret = LZ4_decompress_fast(reinterpret_cast<char *>(ptr),
  75. reinterpret_cast<char *>(dst),
  76. header.m_original_size);
  77. }
  78. if (ret < 0) {
  79. if (allocated) {
  80. ut::free(dst);
  81. }
  82. return (DB_IO_DECOMPRESS_FAIL);
  83. }
  84. break;
  85. }
  86. case Compression::ZSTD: {
  87. size_t zstd_len =
  88. ZSTD_decompress(dst, header.m_original_size, ptr, header.m_compressed_size);
  89. if (ZSTD_isError(zstd_len)) {
  90. if (allocated) {
  91. ut::free(dst);
  92. }
  93. return (DB_IO_DECOMPRESS_FAIL);
  94. }
  95. ut_ad(zstd_len <= len);
  96. len = static_cast<ulint>(zstd_len);
  97. break;
  98. }
  99. default:
  100. #ifdef UNIV_NO_ERR_MSGS
  101. ib::error()
  102. #else
  103. ib::error(ER_IB_MSG_741)
  104. #endif /* UNIV_NO_ERR_MSGS */
  105. << "Compression algorithm support missing: "
  106. << Compression::to_string(compression.m_type);
  107. if (allocated) {
  108. ut::free(dst);
  109. }
  110. return (DB_UNSUPPORTED);
  111. }
  112. /* Leave the header alone */
  113. memmove(src + FIL_PAGE_DATA, dst, len);
  114. mach_write_to_2(src + FIL_PAGE_TYPE, header.m_original_type);
  115. ut_ad(dblwr_read || BlockReporter::is_lsn_valid(
  116. src, header.m_original_size + FIL_PAGE_DATA));
  117. if (allocated) {
  118. ut::free(dst);
  119. }
  120. return (DB_SUCCESS);
  121. }

3.3 页压缩的局限性

操作系统需要对 sparse file 以及 hole punching 的支持;在共享表空间中不支持页压缩。

MySQL 页压缩刚出现的时候,由于此功能对 OS 的强依赖性,有一些大神以此来针砭,其中比较著名的一篇是 how innodb lost its advantage

4. 字段压缩

本节主要介绍几种引擎对字段压缩的不同实现方式:

Percona 引擎的实现代码:代码链接

RDS 5.6 实现的代码:代码链接

MariaDB 的代码是由腾讯贡献:代码链接

4.1 RDS 5.6 的实现逻辑

给 COLUMN_TYPE 添加一个字段,遇到这个标记的话,就在行存储的时候,使用 zlib 算法压缩之后再存储。代码里面有不能当主键的判断逻辑,不能作为主键来使用;在进行 blob 等数据结构是否相等的时候,这些函数也都经过了特殊处理;只支持特定类型的数据。

4.2 Percona 引擎的实现逻辑

使用了一个新的 DD 来实现,如果把列的信息设置为 COMPRESS,就会把这个表的 space_id ,第几个列,列名等信息都记录在这个 DD 里面,建表的时候会进行判断,如果上面几个都相等,就压缩之后再进行存储;否则就进行正常的存储;整体的实现逻辑跟 RDS56 有点像。

5. MySQL 压缩的局限与发展

AI 技术对数据量的需求远远大于普通业务,在 AI 技术越发流行的今天,如何处理好大数据量的存储,成为了一个越来越重要的课题。在数据量越来越大的背景下,现在 MySQL 的表压缩功能的使用率却依然比较低,原因是什么呢? 笔者使用阿里云自建 MySQL 进行了 TPCC 标准测试,结果如下表

测试场景tpsqpsmin(ms)agv(ms)max(ms)95%(ms)
read_only(未压缩)1221.5519544.7423.0126.19429.1028.16
read_only(压缩)1134.8118156.9623.3328.19845.7830.26
write_only(未压缩)2688.2616129.568.2311.90415.4619.29
write_only(压缩)1166.506998.9910.4627.43629.4044.98
read_write(未压缩)892.6817853.5328.7135.84439.4746.63
read_write(压缩)634.7812695.6532.8750.41876.7964.47

在写比较多的场景下,QPS 跟 TPS 等性能数据只有 40% 左右,之所以有这个现象,跟压缩表的页面设计有关。压缩表的页面一般场景下为经验值 8K,本身就是正常页面的一半;页面写数据的逻辑是:先将写的数据写到压缩页的 modifition log 中,如果 modifition log 满,就会进行解压操作,解压之后把 modifition log 中的数据全部写入(页分裂概率很大),写入后再重新压缩,这个过程对性能的影响较大。

能否解决或者优化压缩对性能的影响问题,成为了影响压缩技术应用的关键。现在一般是从参数调整的角度来做优化,后续从代码的角度做一些优化很重要。

6. RDS 对 MySQL 的页压缩优化

笔者在阿里云 RDS 工作期间,注意到 MySQL 的页压缩算法只支持 zlib 算法以及 lz4 算法,这两种算法都已经是比较古老的算法了,于是写了一版代码,进行了 MySQL 上的页压缩算法替代,支持 zstd 算法来替代原有压缩算法。

算法替代后,在阿里云服务器上测试的性能数据如下:

云服务器配置 
CPU32
内存128GB
云盘SSD盘 400GiB (3400 IOPS)
OSLinux version 5.10.134-15.al8.x86_64
数据量20 WH(2 G),5WH ,10WH
innodb_buffer_pool_size128M

压缩率测试结果

 W=5W=10W=20平均压缩率
NONE555 M1034 M1999 M0 %
LZ4500 M ( 9.91% )924 M (10.63%)1788 M (10.55%)10.36 %
ZLIB401 M ( 27.7%)725 M (29.88%)1433 M (30.18%)29.25 %
ZSTD389 M ( 29.91%)706 M (31.72%)1358 M (32.61%)31.41 %

性能测试结果

 W=20性能损耗
NONE420.0 TRX/S0 %
LZ4417.3 TRX/S-0.7 %
ZLIB342.5 TRX/S-18.57 %
ZSTD389.3 TRX/S-7.38 %

可以看到,在 ZSTD 压缩算法下,使用 7% 的性能损耗,获得了 31% 的成本降低,与原生的 ZLIB 与 LZ4 算法相比,性能更为均衡与强劲。

原文:http://mysql.taobao.org/monthly/2023/12/04/