第一种是传统的数据压缩,通过指定row_format及key_block_size,能够将用户表压缩到指定的page size并进行存储,默认使用zlib。这种压缩方式使用比较简单,但也是诟病较多的, 代码陈旧,相关代码基本上几个大版本都没发生过变化,一些优化点还是从facebook移植过来的(集中在5.6版本中, 不过现在fb已经放弃优化InnoDB压缩了,转而聚集在自家压缩更好的myrock上)。InnoDB压缩表的性能瓶颈明显,尤其是在压缩page到指定size失败时触发索引分裂。

第二种是MySQL5.7引入的所谓transparent compression,通过文件系统punch hole和sparse file特性来实现的。具体的就是在将数据页进行压缩后,将留白的地方进行打洞,从而实现数据压缩的目的。这个实现的好处就是代码逻辑简单,整个feature的实现基本上没加多少代码,无需指定key_block_size(但依然需要根据文件系统block size对齐),并且也能更方便的支持多种压缩算法。但缺点也明显,例如可能会产生大量的文件碎片,底层的文件管理可能更复杂;也无法降低buffer pool的占用(传统的压缩方式可以只在buffer pool保存压缩页)

另外还有一种方式是通过MySQL函数compress/decompress,由应用端来决定存入的数据是否压缩,并控制解压操作。但这种方式不够灵活,需要应用来修改代码。

在AliSQL中我们提供了一种新的列压缩方式,用户在建表时可以将列属性column_format指定为compressed,那么服务器就会在存入/取出这个列的数据时,自动对其进行压缩和解压动作。这个方案不仅降低了磁盘数据大小,而且也能最大程度的保证性能,例如在查询不涉及到压缩列时无需执行解压动作。该特性尤其适用于诸如blob或者text这样的大列。

Percona Server也基于该补丁进行了功能扩展和优化。社区用户现在可以同时从AliSQL及Percona Server中获得该特性。

本文主要简单介绍下AliSQL如何实现的该特性,以及Percona的实现方案。

使用该特性非常简单,可以在建表时指定列属性,或者在ALTER TABLE来修改列属性。

目前仅支持对blob/text/varchar/varbinary这几种类型进行压缩,如果在其他类型列上定义compressed属性,会抛出一个warning,并忽略列属性:

  1. Query OK, 0 rows affected, 1 warning (0.00 sec)
  2. mysql> SHOW WARNINGS;
  3. +---------+------+------------------------------------------------------------------------------------------+
  4. | Level | Code | Message |
  5. +---------+------+------------------------------------------------------------------------------------------+
  6. | Warning | 3002 | Can not define column 'b' in compressed format, silently change column_format to default |
  7. +---------+------+------------------------------------------------------------------------------------------+
  8. 1 row in set (0.00 sec)

也不支持在压缩列上创建二级索引,因为压缩后的数据可能已经不具备顺序性,在其上创建索引没有意义,一个错误码会被抛出:

  1. mysql> CREATE TABLE t1 (a INT AUTO_INCREMENT PRIMARY KEY, b BLOB COLUMN_FORMAT COMPRESSED, KEY (b(20)));
  2. ERROR 3001 (HY000): Compressed BLOB/TEXT/VARCHAR/VARBINARY column 'b' used in key list is not allowed

由于大部分用户的引擎还是InnoDB,因此目前该特性仅支持InnoDB表(其实真实原因是笔者在写这个补丁时只对InnoDB比较了解…..),未来不排除这个特性实现到server层,这样就可以做到和引擎无关了。

代码的实现也比较简单,分为两部分

在InnoDB接受到行数据并进行任何处理之前,先将对应的列数据进行压缩.

入口函数:row_compress_column

压缩后的数据包含如下部分:

如果发现压缩后的数据比原始数据还大,则放弃压缩,但会额外浪费1个字节来进行标识

我们提供了一些参数来对压缩进行控制,包括

  1. innodb_rds_column_compression_level: zlib的压缩级别
  2. innodb_rds_column_zip_mem_use_heap: 压缩过程中的内存分配/释放的回调函数,是使用InnoDB自带的还是系统自带的
  3. innodb_rds_column_zip_threshold: 当数据长度超过这么大时,才去进行压缩; 这个参数需要根据数据特点来进行调整,否则如果对很小的字段进行压缩,没什么效果不说,反而还浪费cpu.
  4. innodb_rds_column_zlib_strategy:使用的zlib压缩策略:
  5. innodb_rds_column_zlib_wrap: 是否在压缩/解压时进行adler32校验

解压

在从InnoDB取到一条数据并返回到server层之前,对列进行解压

入口函数: row_decompress_column

解压也比较简单,首先根据Header中的信息判断是否进行了压缩;然后再读出原始数据的长度;找到压缩数据的起始位置并进行解压后,跟原始长度进行校验。

全局Status变量来监控压缩和解压的次数:

  1. mysql> show status like '%column%compress%';
  2. +----------------------------+-------+
  3. | Variable_name | Value |
  4. +----------------------------+-------+
  5. | Innodb_column_compressed | 0 |
  6. | Innodb_column_decompressed | 0 |
  7. +----------------------------+-------+

完整的补丁见

为了实现更好的压缩比,Percona 实现了一个称为 “predefined dictionary”, 实际上这是引用了新版本的zlib的一个特性。在压缩初始化后(deflateInit2),可以去设置一个预定义的数据词典.

参阅函数row_compress_column

  1. err = deflateInit2(&c_stream, srv_compressed_columns_zip_level,
  2. Z_DEFLATED, window_bits, MAX_MEM_LEVEL,
  3. srv_compressed_columns_zlib_strategy);
  4. if (dict_data != 0 && dict_data_len != 0) {
  5. err = deflateSetDictionary(&c_stream, dict_data,
  6. dict_data_len);
  7. ut_a(err == Z_OK);
  8. }

Percona利用这个特性,并增加了一系列的接口来管理预定义词典。每个压缩列都可以通过显式的命名指向一个词典。

系统表

增加了一个新的系统表SYS_ZIP_DICT, 用于存储词典数据, 定义如下:

系统表SYS_ZIP_DICT_COLS,用于存储哪些使用预定义压缩词典的列信息,定义如下:

  1. CREATE TABLE SYS_ZIP_DICT_COLS(
  2. TABLE_ID INT UNSIGNED NOT NULL,
  3. COLUMN_POS INT UNSIGNED NOT NULL,
  4. DICT_ID INT UNSIGNED NOT NULL
  5. );
  6. CREATE UNIQUE CLUSTERED INDEX SYS_ZIP_DICT_COLS_COMPOSITE ON SYS_ZIP_DICT_COLS (TABLE_ID, COLUMN_POS);
  7. -- 建立在该表之上的视图:information_schema.xtradb_zip_dict_cols

创建词典

  1. 语法: CREATE COMPRESSION_DICTIONARY <dict>(...)
  2. 例如:
  3. mysql> CREATE COMPRESSION_DICTIONARY dt1('abcd');
  4. Query OK, 0 rows affected (0.00 sec)
  5. +----+------+----------+
  6. | id | name | zip_dict |
  7. | 1 | dt1 | abcd |
  8. +----+------+----------+
  9. 1 row in set (0.00 sec)
  10. 入口函数: innobase_create_zip_dict

使用词典

删除词典

  1. 语法: DROP COMPRESSION_DICTIONARY <dict>
  2. # 很显然,当有列引用到这个词典时,是不可以删除的
  3. mysql> DROP COMPRESSION_DICTIONARY dt1;
  4. ERROR 1894 (HY000): Compression dictionary 'dt1' is in use
  5. mysql> ALTER TABLE t1 MODIFY COLUMN b BLOB COLUMN_FORMAT COMPRESSED;
  6. Query OK, 0 rows affected (0.01 sec)
  7. Records: 0 Duplicates: 0 Warnings: 0
  8. mysql> DROP COMPRESSION_DICTIONARY dt1;
  9. Query OK, 0 rows affected (0.01 sec)
  10. mysql> SELECT * FROM INFORMATION_SCHEMA.XTRADB_ZIP_DICT_COLS;
  11. Empty set (0.00 sec)
  12. mysql> SELECT * FROM INFORMATION_SCHEMA.XTRADB_ZIP_DICT;
  13. Empty set (0.00 sec
  14. 入口函数:innobase_drop_zip_dict

参考文档: Percona Column compression 文档