MYSQL存储引擎InnoDB(六十一):InnoDB表的压缩如何工作

压缩算法

一些操作系统在文件系统级别实现压缩。文件通常被分成固定大小的块,这些块被压缩成可变大小的块,这很容易导致碎片。每次修改块内的某些内容时,都会对其进行重新压缩。这些特性使得这种压缩技术不适合在更新密集型数据库系统中使用。

MySQL 借助著名的 zlib 库实现压缩,该库实现了 LZ77 压缩算法。这种压缩算法在 CPU 利用率和减少数据大小方面都是成熟、稳健和高效的。该算法是 “无损”的,因此始终可以从压缩形式中重建原始未压缩数据。LZ77 压缩通过查找在要压缩的数据中重复的数据序列来工作。数据中的值模式决定了它的压缩程度,但典型的用户数据通常会压缩 50% 或更多。

与应用程序执行的压缩或某些其他数据库管理系统的压缩功能不同,InnoDB压缩既适用于用户数据,也适用于索引。在许多情况下,索引可以构成总数据库大小的 40-50% 或更多,因此这种差异是显著的。当数据集的压缩效果很好时,InnoDB 数据文件( 独立表 空间或通用表空间 .ibd文件)的大小是未压缩大小的 25% 到 50% 或可能更小。根据工作量,这个较小的数据库可以反过来导致 I/O 的减少和吞吐量的增加,在增加 CPU 利用率方面的成本适中。您可以通过修改配置选项innodb_compression_level 来调整压缩级别和 CPU 开销之间的平衡 。


InnoDB 数据存储和压缩

InnoDB 表中的所有用户数据都存储在包含 B 树索引( 聚集索引)的页面中。在其他一些数据库系统中,这种类型的索引称为 “索引组织表”。索引节点中的每一行都包含(用户指定的或系统生成的) 主键的值以及表的所有其他列。

InnoDB 表中的二级索引也是 B 树,包含成对的值:索引键和指向聚集索引中一行的指针。二级索引记录必须始终适合单个 B 树页面。


B-Tree 页面的压缩

因为它们经常更新,所以 B 树页面需要特殊处理。重要的是尽量减少 B 树节点被拆分的次数,以及尽量减少解压缩和重新压缩其内容的需要。

MySQL 使用的一种技术是以未压缩的形式在 B-tree 节点中维护一些系统信息,从而促进某些信息就地更新。例如,这种技术允许在不进行任何压缩操作的情况下对行进行删除标记和删除。

此外,MySQL 试图避免索引页在更改时不必要的解压缩和重新压缩。在每个 B-tree 页面中,系统都会保留一个未压缩的 “修改日志”来记录对页面所做的更改。可以将小记录的更新和插入写入此修改日志,而无需完全重建整个页面。

当修改日志的空间用完时,InnoDB 解压缩页面,应用更改并重新压缩页面。如果重新压缩失败(这种情况称为压缩失败),B-tree 节点将被拆分并重复该过程,直到更新或插入成功。

为了避免写入密集型工作负载的频繁压缩失败,例如对于OLTP 应用程序,MySQL 有时会在页面中保留一些空白空间(填充),以便修改日志更快地填满,并在仍有足够空间的情况下重新压缩页面避免分裂它。每个页面中剩余的填充空间量随着系统跟踪页面拆分的频率而变化。在频繁写入压缩表的繁忙服务器上,您可以调整 innodb_compression_failure_threshold_pct、 和 innodb_compression_pad_pct_max 配置选项来微调此机制。

一般来说,MySQL 要求 InnoDB 表中的每个 B 树页面至少可以容纳两条记录。对于压缩表,此要求已放宽。B-tree 节点的叶子页面(无论是主键还是二级索引)只需要容纳一条记录,但该记录必须以未压缩的形式适合每页修改日志。如果 innodb_strict_mode是ON,MySQL在CREATE TABLE或CREATE INDEX期间检查最大行大小 。如果该行不适合,则会发出以下错误消息:ERROR HY000: Too big row。

如果您在innodb_strict_mode为 OFF 时创建表 ,并且后续INSERT或 UPDATE语句尝试创建不适合压缩页面大小的索引条目,则操作将失败并显示ERROR 42000: Row size too large。要解决此问题,请使用ALTER TABLE重建表并选择更大的压缩页面大小,缩短任何列前缀索引,或使用ROW_FORMAT=DYNAMIC或 ROW_FORMAT=COMPACT完全禁用压缩。

innodb_strict_mode不适用于通用表空间。通用表空间的表空间管理规则独立于 innodb_strict_mode。


压缩 BLOB、VARCHAR 和 TEXT 列

在 InnoDB 表中,不属于主键的BLOB、VARCHAR、TEXT列可以存储在单独分配的 溢出页上。我们将这些列称为 离页列。它们的值存储在溢出页面的单链表中。

对于在ROW_FORMAT=DYNAMIC或ROW_FORMAT=COMPRESSED中创建的表, BLOB、TEXT或VARCHAR列的值可能会完全离页存储,具体取决于它们的长度和整行的长度。对于页外存储的列,聚集索引记录仅包含指向溢出页的 20 字节指针,每列一个。任何列是否存储在页外取决于页面大小和行的总大小。当行太长而无法完全容纳在聚集索引的页面中时,MySQL会选择最长的列进行页外存储,直到该行适合聚集索引页面为止。如上所述,如果行本身不能适合压缩页面,则会发生错误。

使用ROW_FORMAT=REDUNDANT和ROW_FORMAT=COMPACT的表会存储聚集索引记录中BLOB、 VARCHAR和 TEXT列的前 768 个字节以及主键。768 字节前缀后跟一个 20 字节指针,指向包含其余列值的溢出页。

当一个表被COMPRESSED格式化时,所有写入溢出页的数据都被“按原样”压缩;即 MySQL 对整个数据项应用 zlib 压缩算法。除了数据之外,压缩的溢出页面还包含未压缩的标题和尾部,其中包括页面校验以及到下一个溢出页面的链接等。因此,如果数据是高度可压缩的(文本数据通常就是这种情况),对于较长的BLOB、 TEXT或VARCHAR列,可以获得非常显着的存储节省 。图像数据,例如JPEG, 通常已经被压缩,因此存储在压缩表中并没有太多好处;双重压缩可能会浪费 CPU 周期而很少或根本没有节省空间。

溢出页面的大小与其他页面相同。即使列的总长度只有 8K 字节,包含离页存储的十列的行也会占用十个溢出页。在一个未压缩的表中,十个未压缩的溢出页占用 160K 字节。在一个 8K 页大小的压缩表中,它们只占用 80K 字节。因此,对具有长列值的表使用压缩表格式通常更有效。

对于独立表 空间,使用 16K 压缩页面大小可以减少BLOB、 VARCHAR或 TEXT列的存储和 I/O 成本, 因为这些数据通常压缩得很好,因此可能需要更少的溢出页面,即使 B 树节点它们自己占用与未压缩形式一样多的页面。通用表空间不支持 16K 压缩页面大小。


压缩和 InnoDB 缓冲池

在压缩InnoDB表中,每个压缩页面(无论是 1K、2K、4K 还是 8K)都对应一个 16K 字节的未压缩页面。为了访问页面中的数据,如果压缩页面不在 缓冲池中,MySQL 会从磁盘读取压缩页面,然后将页面解压缩为其原始形式。

为了最小化 I/O 并减少解压缩页面的需要,缓冲池有时会同时包含数据库页面的压缩和未压缩形式。为了为其他所需的数据库页面腾出空间,MySQL 可以从缓冲池中逐出未压缩的页面,同时将压缩页面留在内存中。或者,如果一个页面有一段时间没有被访问过,该页面的压缩形式可能会被写入磁盘,为其他数据腾出空间。因此,在任何给定时间,缓冲池都可能包含页面的压缩和未压缩形式,或者仅包含页面的压缩形式,或者两者都不包含。

MySQL 使用最近最少使用 ( LRU ) 列表 跟踪要保留在内存中的页面以及要驱逐的页面,以便热(经常访问)数据倾向于保留在内存中。当访问压缩表时,MySQL 使用自适应 LRU 算法来实现内存中压缩和未压缩页面的适当平衡。这种自适应算法对系统是在I/O-负载还是 CPU-负载中运行很敏感方式。目标是避免在 CPU 繁忙时花费过多的处理时间来解压缩页面,并避免在 CPU 有空闲周期可用于解压缩压缩页面(可能已经在内存中)时执行过多的 I/O。当系统是 I/O-负载时,该算法更喜欢驱逐一个页面的未压缩副本而不是两个副本,以便为其他磁盘页面腾出更多空间来驻留在内存中。当系统受 CPU 限制时,MySQL 更倾向于驱逐压缩和未压缩的页面,以便更多内存可用于“热”页面,并减少仅以压缩形式解压缩内存中数据的需要。


压缩和 InnoDB重做日志文件

在将压缩页面写入 数据文件之前,MySQL 将该页面的副本写入重做日志(如果自上次写入数据库以来已重新压缩)。这样做是为了确保重做日志可用于 崩溃恢复,即使在zlib库升级并且该更改引入压缩数据的兼容性问题的不太可能的情况下也是如此。因此,日志文件的大小会有所增加 ,或者需要更频繁的 检查点。日志文件大小或检查点频率的增加量取决于以需要重组和重新压缩的方式修改压缩页面的次数。

要在独立表空间中创建压缩表, 必须启用innodb_file_per_table。在通用表空间中创建压缩表时不依赖于innodb_file_per_table设置。

发表评论
留言与评论(共有 0 条评论) “”
   
验证码:

相关文章

推荐文章