原文:
[MySQL 5.7 Manual 15.9.1.5 How Compression Works for InnoDB Tables]
https://dev.mysql.com/doc/refman/5.7/en/innodb-compression-internals.html
本节介绍关于InnoDB表的压缩的一些内部实现细节。 这里介绍的信息可能有助于性能调优,但对压缩功能的基本使用并不必要。
压缩算法
某些操作系统在文件系统级别实现压缩。文件通常被分为固定大小的块,这些块被压缩后大小不固定,很容易导致碎片化。当块内的内容被修改时,在被写入磁盘之前,整个块都要被重新压缩。这些特性使得这种压缩技术不适合在update密集型当数据库系统中使用。
MySQL利用了广为人知的zlib库(基于LZ77压缩算法)来实现压缩功能。这种压缩算法在CPU利用率和减小数据大小方面都是成熟,稳健和高效的。该算法是“无损”的,从而可以始终从压缩形式重构原始的未压缩数据。 LZ77压缩通过查找要压缩的数据中重复的数据序列来实现压缩的目的。数据的具体形式决定了它的压缩程度,典型的用户数据通常会压缩50%或更多。
与一个应用程序执行的压缩或某些其他数据库管理系统的压缩功能不同,InnoDB压缩既适用于用户数据又适用于索引数据。在许多情况下,索引可以构成总数据库大小的40-50%以上,所以这带来的差异是显著的。当压缩对于某个数据集工作良好时,InnoDB数据文件(file-per-table的表空间或一般的表空间.idb文件)的大小是未压缩大小的25%到50%或更小。根据工作负载,压缩后的较小的数据库反过来很可能导致I/O数的减少,并以适度增加CPU占用率为成本增大了吞吐量。你可以通过修改innodb_compression_level
配置选项来调整压缩级别和CPU开销之间的平衡。
InnoDB数据存储和压缩
InnoDB表中的所有用户数据都存储在包含B树索引(聚簇索引, clustered index)的页面中。 在其他一些数据库系统中,这种类型的索引称为“索引组织表”。 索引节点中的每一行都包含(用户指定的或系统生成的)主键和表的所有其他列的值。
InnoDB表中的辅助索引(secondary indexes)也是B树,包含键值对:索引键和指向聚簇索引中的某个行的指针。 指针实际上是表的主键的值,在需要除索引键和主键之外的列时,则需要查找到该值用来访问聚簇索引,进而访问到需要的列。 辅助索引记录必须始终可以放在单个B树页面。
B类树节点(聚簇索引和辅助索引)的压缩处理方式与用于存储长VARCHAR,BLOB或TEXT列的溢出页面的压缩有所不同,如以下部分所述。
B树中页的压缩
因为它们经常被更新,B树中的页面需要特殊处理。重要的是最小化B树节点被拆分的次数,以及最小化解压缩和重新压缩内容的次数。
MySQL使用的一种技术是以未压缩形式在B树节点中维护一些系统信息,从而有助于某些就地更新。例如,这允许删除标记,来使得删除行时不进行任何压缩操作。
此外,MySQL尝试在更改索引页时避免不必要的解压缩和重新压缩。在每个B树页面中,系统保留未压缩的“modification log(mlog)”来记录对页面所做的更改。可以将对较小记录的update和insert直接写入这个mlog,而不去重构整个页面。
当mlog的空间用尽时,InnoDB解压缩页面,将更改应用于将被重新压缩的页面。如果重新压缩失败(称为压缩失败的情况),则B树节点被拆分,该过程将重复直到update或insert成功。
为了避免在写入密集型工作负载(如OLTP应用程序)中频繁的压缩失败,MySQL有时会在页面中保留一些空白空间(填充(padding)),这样mlog会更快被填满,页面被重新压缩时就会有足够的空间避免分裂。随着系统跟踪页面分割的频率,每页中留下的填充空间量会有所不同。在进行频繁写入压缩表的服务器上,您可以通过innodb_compression_failure_threshold_pct
和innodb_compression_pad_pct_max
配置选项来调优这个机制。
通常,MySQL要求InnoDB表中的每个B-tree页面至少可容纳两个记录。对于压缩表,这个要求被放宽了。 B树节点的叶子页面(无论是主键还是辅助索引)只需要容纳一个记录,但该记录的未压缩的形式必须能够存储在每页修改日志。如果innodb_strict_mode
为ON,则在CREATE TABLE或CREATE INDEX时检查最大行的大小。如果该行放不下,将发出以下错误消息: ERROR HY000: Too big row.
如果在innodb_strict_mode
为OFF时创建表,并且随后的INSERT或UPDATE语句尝试创建不符合压缩页面大小的索引条目,则操作将失败,并提示:ERROR 42000: Row size too large.(此错误消息不记下这个记录太大的索引,不提及索引记录的长度,也不说明特定索引页上的最大记录的大小)要解决此问题,请使用ALTER TABLE
重新生成表,然后选择 较大的压缩页大小(KEY_BLOCK_SIZE
),缩短任何列前缀索引,或全部使用ROW_FORMAT = DYNAMIC
或ROW_FORMAT = COMPACT
禁用压缩。
innodb_strict_mode不适用于同时支持压缩的通用表空间。 一般表空间的表空间管理规则是独立于innodb_strict_mode进行严格执行的。 有关更多信息,请参见第14.1.19节“CREATE TABLESPACE语法”。
压缩BLOB, VARCHAR 和 TEXT列
在InnoDB表中,不属于主键的BLOB,VARCHAR和TEXT列可以存储在单独分配的溢出页面(overflow page)上。我们将这些列称为页外列。它们的值存储在溢出页面里的单链表中。
对于用ROW_FORMAT = DYNAMIC
或ROW_FORMAT = COMPRESSED
参数创建的表,BLOB,TEXT或VARCHAR列的值可能会完全存储在页外,具体会取决于列长度和整行长度。对于非页外存储的列,聚簇索引记录仅包含20字节指向溢出页的指针。一列是否被存在页外取决于页大小和整行的大小。如果行太长而不能放入聚簇索引的页时,MySQL会选出最长的列吗,将其放在页外,知道页的大小合适放入聚簇索引中。如上面提到的,如果一行在压缩页上不合适,就会发生错误。
注意:
对于在
ROW_FORMAT = DYNAMIC
或ROW_FORMAT = COMPRESSED
中创建的表,小于或等于40字节的TEXT和BLOB列会始终保存在页内。
在旧版本的MySQL中创建的表使用Antelope文件格式,它只支持ROW_FORMAT = REDUNDANT
和ROW_FORMAT = COMPACT
。在这些格式中,MySQL会将BLOB,VARCHAR或TEXT类型的列的前768个字节与主键一起存储在聚簇索引记录中,而768字节后的20字节存储指向列值的其余部分的溢出页指针。
当表格采用COMPRESSED格式时,写入溢出页的所有数据也照样压缩;也就是说,MySQL会将zlib压缩算法应用于整个数据项。除了数据之外,压缩的溢出页还包含一个未压缩的header部分和trailer部分,其中包括页校验和和到下一个溢出页的指针等信息。因此,如果数据是高度可压缩的,那么对于较长的BLOB,TEXT或VARCHAR列会和文本数据一样,也可以非常显著地节省存储空间。图像数据(例如JPEG)通常已被压缩,因此不会因为存储在压缩表中而获得得太多收益,双重的压缩可以只会浪费CPU周期或节省很小的空间。
溢出页与其他页大小相同。有10列需要存储在页外的行就要占用10个溢出页,所以即使要放入溢出页的列总大小只有8K字节。因此,在未压缩的表中,10个未压缩的溢出页占用160K字节;而在8K页面大小的压缩表中,它们只占用80K字节。因此,对于具有列值较长的表,使用压缩表格式往往更高效。
对于有file-per-table属性的表空间,使用16K的压缩页大小可以减少BLOB,VARCHAR或TEXT列的存储和I/O成本,这样虽然B树的节点本身的数量和不压缩相比并未减少,但由于这种情况下BLOB,VARCHAR或TEXT类型的数据经常被压缩得很好,所以这样可以减少溢出页的数量。通用表空间不支持16K压缩页大小(KEY_BLOCK_SIZE)。有关更多信息,请参见第15.7.9节“InnoDB常规表空间”。
压缩与InnoDB缓冲池(Buffer Pool)
在压缩的InnoDB表中,每个压缩页(无论是1K,2K,4K还是8K)都对应于一个16K字节的未压缩页面(如果设置了innodb_page_size,可能是其他大小)。要访问页面中的数据,如果相应的页没有在缓冲池中,那么MySQL会先从磁盘读取压缩页面,然后将页面解压缩到其原始格式。本节将介绍对于InnoDB缓冲池(buffer pool)的管理方法中与压缩表页面相关的部分。
为了最小化I/O并减少对页面进行解压缩的频次,缓冲池中有时会包含一个页面的压缩和未压缩格式。为了为其他必需的数据库页面腾出空间,MySQL可能从缓冲池中丢弃一些未压缩的页面,同时将这个页对应的压缩页留在内存中。或者,如果一段时间没有访问页面,页面的压缩形式可能也会写入磁盘,以为其他数据的释放内存空间。因此,在任何时间点,缓冲池可能包含一个页面的压缩和未压缩的形式,或者只包含这个页面的压缩形式,或者两者都不包含。
MySQL跟踪哪些页面保留在内存中,哪些页是使用最近最少使用的(LRU)列表去丢弃的,所以热(经常被访问的)数据更倾向于被保留在内存中。当访问压缩表时,MySQL使用自适应LRU算法来实现内存中压缩和未压缩页面的适当平衡。这种自适应算法对于I/O密集型或CPU密集型的系统都是敏感的。目标是避免在CPU繁忙时花费太多CPU时间来解压缩页面,或者在CPU具有空闲周期可以用于解压缩页(可能已经在内存中)时避免进行多余的I/O。当系统是I/O密集型时,该算法优先于逐出未压缩形式的副本而不是两个副本,从而为其他压缩页面驻留内存留下更多的空间。当系统受CPU限制时,MySQL更倾向于同时丢弃压缩和未压缩的页面,以便更多的内存用于存储“热”页面,避免浪费CPU解压内存中的仅存压缩形式的页面。