本节描述了 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 树节点(包括聚簇索引和辅助索引)的压缩处理方式不同于 溢出页面 的压缩处理方式,用于存储长 VARCHAR
、BLOB
或 TEXT
列,如下节所述。
B 树页面的压缩
由于 B 树页面频繁更新,因此需要特殊处理。尽量减少 B 树节点的拆分次数,并尽量减少对其内容的解压缩和重新压缩操作。
MySQL 使用的一种技术是在 B 树节点中维护一些未压缩的系统信息,以便于某些就地更新。例如,这允许行被删除标记和删除,而不需要任何压缩操作。
此外,MySQL 尝试避免在 B 树页面更改时不必要的解压缩和重新压缩操作。在每个 B 树页面中,系统保持一个未压缩的 “修改日志”,以记录对页面的更改。小记录的更新和插入可能会写入该修改日志,而不需要重新构建整个页面。
当修改日志空间用完时,InnoDB将页面解压缩,应用更改并重新压缩页面。如果重新压缩失败(一种情况称为压缩失败),B树节点将被拆分,过程将重复直到更新或插入成功。
为了避免频繁的压缩失败在写入密集型工作负载中,例如OLTP应用程序,MySQL有时会在页面中保留一些空白空间(填充),以便修改日志尽快填满,并在还有足够的空间时重新压缩页面,而不是拆分它。每个页面中留下的填充空间量会根据系统跟踪页面拆分的频率而变化。在繁忙的服务器上频繁地写入压缩表时,可以调整innodb_compression_failure_threshold_pct
和innodb_compression_pad_pct_max
配置选项来微调该机制。
一般来说,MySQL要求每个InnoDB表的B树页面至少可以容纳两个记录。对于压缩表,这个要求已经放松。B树节点的叶页面(无论是主键还是次要索引)只需要容纳一个记录,但该记录必须以未压缩形式容纳在每页修改日志中。如果innodb_strict_mode
为ON
,MySQL将在CREATE TABLE
或CREATE INDEX
期间检查最大行大小。如果行不适合,以下错误消息将被发出:ERROR HY000: Too big row
。
如果您在innodb_strict_mode
关闭时创建表,并且后续的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
。有关更多信息,请参阅第15.1.21节,“CREATE TABLESPACE语句”。
压缩BLOB、VARCHAR和TEXT列
在InnoDB表中,BLOB
、VARCHAR
和TEXT
列如果不是主键的一部分,可以存储在单独分配的溢出页面上。我们将这些列称为off-page列。它们的值存储在单链表溢出页面上。
对于在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 压缩页大小 (KEY_BLOCK_SIZE
)。有关更多信息,请参阅 第 17.6.3.3 节,“通用表空间”。
压缩和 InnoDB 缓冲池
在压缩的 InnoDB
表中,每个压缩页(无论是 1K、2K、4K 或 8K)对应于 16K 字节的未压缩页(或小于 innodb_page_size
设置的大小)。要访问页中的数据,MySQL 从磁盘读取压缩页,如果它不在 缓冲池 中,然后将页解压缩到其原始形式。本节描述了 InnoDB
如何管理缓冲池,以便访问压缩表中的页。
为了最小化 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
设置。有关更多信息,请参阅 第 17.6.3.3 节,“通用表空间”。