亚马逊AWS官方博客
在 Amazon RDS for MySQL 和 Amazon Aurora MySQL 上实现高速 InnoDB 清除
![]() |
了解清除操作的工作方式
与许多其它主流关系数据库管理系统(RDBMS,Relational DataBase Management System)一样,MySQL 已经实施了 MVCC,支持以并行读写方式来访问数据。MVCC 的核心理念是,当某个事务更新表记录时,让数据库引擎在表中创建数据的新版本。旧版本的数据并未实际移除,但会标记为删除。执行查询时,可以选择相应的数据版本,在所需的隔离级别上构造自己的数据库视图。这样做的一大好处是可以避免读取和写入之间发生阻塞情况。读取操作始终可以访问旧数据版本,而写入操作处理的是新版本。利用保留多版本表记录的功能,数据库引擎还可以方便地在事务内或崩溃恢复期间执行回滚操作。
作为可扩展的解决方案,MVCC 有一个内在的约束:需要对标记为删除的表记录进行垃圾回收。各种数据库引擎都配有自己的版本跟踪和垃圾回收机制。在数据库中常遇到的一个挑战是,大量的事务会造成旧版本数据的生成速度太快,而垃圾回收无法及时完成清除,进而导致表结构中出现大量的旧数据版本积压。从表面上看,这会直接导致空间占用量意外增长。而在底层,由于对旧版本进行检查,因此需要额外的 I/O 操作来执行读取操作。I/O 使用量的增加会造成系统资源争用,因而可能导致数据库的整体性能降级。
在 MySQL 数据库中,InnoDB 使用撤消日志作为键数据结构来支持 MVCC 和回滚操作。当表记录发生更改时,其旧数据版本存储在撤消日志中。与同一个表记录相关的所有撤消日志链接在一起,形成版本链。另一方面,清除操作是垃圾回收过程。此操作不仅负责清理撤消日志,还负责清理这些日志所引用的标记为删除的表记录。实质上,清除操作被视为 InnoDB 事务系统中不可或缺的一部分。
下图从整体上介绍了 InnoDB 清除操作的设计理念及其三步工作流。
![]() |
- 事务在启动时会分配一个回滚段。回滚段由撤消表空间中的多个撤消日志页组成。与存储表记录的数据页一样,撤消日志页需要加载到 InnoDB 缓冲池中才能读取或写入。
- 当事务更改表数据时,就会创建撤消日志记录。撤消日志记录包含着回滚表记录更改所需的相关信息,例如表 ID、聚集索引和更改前的旧数据版本。INSERT、DELETE 或 UPDATE 语句分别有不同类型的撤消日志记录。根据以后是否需要进行清除,这些日志会分组为单独的撤消日志。
- 在提交期间,事务获得事务序列化编号(trx_no)并将该编号写入其撤消日志。如果有撤消日志需要清除,这些日志将添加到回滚段历史记录列表中,按照 trx_no 排序。InnoDB 事务系统使用 trx_no 来跟踪所有已提交事务的顺序。在这个意义上,历史记录列表是数据库范围的全局列表。
- 清除是多线程操作。清除线程数使用
innodb_purge_threads
来设置。通常,系统中有一个清除协调器和多个工作线程。这些线程不断重复包含三个步骤的清除操作。- 清除协调器线程检查是否有可供清除的撤消日志。如果找到任何可清除的日志,该线程会提取批次,解析撤消记录,并按表 ID 排序。然后,线程将每个组分配给一个工作线程进行处理。
- 清除工作线程按照表 ID,并行处理撤消日志记录。这些线程使用撤消日志记录中的信息,识别和移除标记为删除的记录,包括聚集索引、二级索引和 BLOB 列中的记录。在完成清除后,如果数据页的数据太少,则会将其与其它页面合并来优化存储。
- 在处理了几个批次的撤消日志后,清除协调器线程会从回滚段历史记录列表中移除这些日志,以释放回滚段。MySQL 提供了一个名为
innodb_undo_log_truncate
的选项,用于自动截断撤消。此选项用于在撤消表空间超出了innodb_max_undo_log_size
指定的大小限制,并且其中包含的所有回滚段已释放时,缩减占用的物理存储空间。完成此步骤后,清除协调器线程将启动另一个清除周期。
撤消表空间自动截断选项在 Aurora MySQL 兼容版本 3.06.0 及更高版本中可用。
平衡读取和写入工作负载
当事务使用 INSERT、DELETE 或 UPDATE 等数据操作语言(DML,Data Manipulation Language)语句修改记录时,InnoDB 会创建不同类型的撤消日志记录。但是,并非所有类型的撤消日志记录都需要清除。来自 INSERT 语句的撤消日志不包含旧数据版本,因此这些日志在事务提交后将立即删除,不会添加到回滚段历史记录列表中。
DELETE 和 UPDATE 语句生成的撤消日志记录分别存储删除或更新表记录之前的旧数据版本,因此是清除操作的目标。其它并发 SELECT 查询或未完成事务可能需要访问这些日志,以便为 MVCC 构造一致性读取。InnoDB 使用读取视图作为一种机制,用来跟踪活动查询和事务的撤消日志可见性。清除协调器线程也会使用该视图来确定能否安全地删除撤消日志。
创建和使用撤消日志的数据库工作负载会直接影响清除线程的工作方式。系统从回滚段历史记录列表中清除撤消日志时,将按照 trx_no 的升序执行操作。如果某个撤消日志无法清除,就会阻止清除其它 trx_no 较高的日志,即使这些日志属于不同的表。也就是说,清除是数据库范围的全局操作,而长时间运行的查询或事务会阻止清理在其后启动的所有事务的撤消记录。在需要密切关注清除操作的 MySQL 数据库中,建议您查看数据库工作负载的时间、并发和事务特征。
您可以采用这样一种策略,即选择 DROP PARTITION 或 DROP TABLE 语句而不是 DELETE 语句,因为前两种语句不会生成撤消日志。如果您对表进行分区,就可以通过 DROP PARTITION 删除数据子集,从而避免执行清除操作,如下图所示。当需要从表中删除大量数据时,您始终应该考虑采用如下方法:创建新表,将数据复制过去,然后删除旧表。
![]() |
另一种策略是在执行大量 DELETE 或 UPDATE 语句时,避免长时间运行的 SELECT 查询,因为这些查询的读取视图会占用撤消日志并阻止清除。您可以使用 max_execution_time
选项,通过设置最长时间限制来自动停止运行时间过长的 SELECT 查询。您还可以将事务隔离级别从 REPEATABLE READ
切换为 READ COMMITTED
。这可以缩小 SQL 语句创建的读取视图范围,从而减少阻止清除撤消日志的可能性。
Aurora MySQL 是一个集群式数据库,由一个或多个数据库实例组成,使用同一个共享集群存储卷。InnoDB 清除的实施受其集群拓扑的影响。使用 Aurora MySQL 数据库集群时,与 Amazon RDS for MySQL 数据库实例相比,有以下区别:
- 从概念上说,在整个 Aurora MySQL 的数据库架构中,均认为清除是数据库范围的全局操作。清除操作在主(写入器)数据库实例上运行。但是,Aurora 副本数据库实例上的 SELECT 查询可能会阻止清除操作,因为这些查询会创建读取视图。在 Aurora Global Database 中,辅助数据库集群上的 SELECT 查询也有可能阻止主数据库集群上的清除操作。
- 在 Aurora 副本数据库实例上,
READ COMMITTED
隔离级别经过优化,可减少长时间运行的 SELECT 查询对清除线程的影响,并具有 Aurora MySQL 特定的行为。尽管与使用 MySQL 原生READ COMMITTED
级别的主数据库实例相比,查询结果可能会略有不同,但仍然符合 ANSI SQL 标准。您可以在数据库集群参数组中或者在会话级别上,将aurora_read_replica_read_committed
设置为 ON 来启用此功能。
优化表和索引结构
除了撤消日志,在 InnoDB 表中标记为删除的表记录也是清除操作的目标。一般而言,表记录是指存储或指向表行数据的各种数据结构,例如聚集索引、二级索引以及按照 InnoDB 行格式存储在外部的可变长度列。由于撤消日志记录仅包含表 ID 和聚集索引,因此清除线程需要识别标记为删除的其它相关表记录,并在发现这些记录时予以删除。这可能是清除操作中最繁重的环节。
请考虑以下示例,来了解二级索引如何影响清除操作。下图比较了两个 Aurora MySQL 数据库集群的 Amazon CloudWatch 指标:RollbackSegmentHistoryListLength
。两个集群都有一个 r7g.2xlarge 主(写入器)数据库实例,并使用 Sysbench oltp_write_only.lua prepare
工作负载来加载包含 80 GB 数据的表。一个集群(集群 A)运行 oltp_update_non_index.lua
工作负载,用于更新二级索引未涵盖的列。另一个集群(集群 B)运行 oltp_update_index.lua
工作负载,来更新在其上构建了二级索引的列。两个 Sysbench 工作负载都使用 --rate
,以相同的速率生成事务。
![]() |
您可以观察到以下情况:
DMLThroughput
显示两个集群上具有相同的规律,表明这些集群运行了相似数量的 UPDATE 语句。- 在运行
oltp_update_index.lua
工作负载的集群上,峰值时段RollbackSegmentHistoryListLength
超过了 300 万。而在另一个集群上,此参数保持接近于零。这表明,在处理涉及二级索引的撤消日志时,清除线程的速度可能会极大地减慢。使用二级索引并不一定会造成问题。两个集群具有相同的表结构,并且两个表都有二级索引。只有当数据库工作负载修改二级索引时,才会对清除线程造成负面影响。 - 11:52 位置的垂直线显示的是我们在集群 B 上删除二级索引的时间。删除二级索引会立即加快清除操作的速度,因为清除操作不再需要在二级索引中查找和清理记录。您可以看到,删除二级索引几分钟后,
RollbackSegmentHistoryListLength
降至零。您可以在 SYS 架构中使用schema_unused_indexes
视图来标识未使用的二级索引,并评估是否还需要这些二级索引。
从 MySQL 8.0 开始,清除工作线程设计为并行处理不同的表,来清理标记为删除的表记录。并行操作的效率取决于几个因素。下图显示的示例说明了清除工作线程数与等待清除撤消日志的表数量之间的关联。
这些数据是在对前述两个 Aurora MySQL 数据库集群进行的另一次测试中收集的。一个集群(集群 B)继续使用包含 80 GB 数据的表,而另一个集群(集群 A)总共有 80 GB 数据,分布在 10 个表中,每个表包含 8 GB 的数据。两个集群都运行 Sysbench oltp_update_index.lua
工作负载,并使用 --rate
以相同的速率生成事务。由于两个集群中的数据库实例都是 r7g.2xlarge,默认情况下 innodb_purge_threads
设置为 3。也就是说,两个清除工作线程(和清除协调器)可以同时运行。
![]() |
您可以观察到以下情况:
DMLThroughput
显示两个集群上具有相同的规律,表明这些集群运行了相似数量的 UPDATE 语句。- 在包含 10 个表的集群 B 上,
RollbackSegmentHistoryListLength
峰值可达到大约 50 万,与之对比的是,在包含 1 个表的集群 A 上,该值为 300 万。更快的清除速度源自两个因素:两个工作线程并行处理,并且每个工作线程处理的表较小。一次只有一个工作线程来清除一个表。为了充分利用并行机制,理想的做法是按照清除工作线程的数量,将数据更改均匀地分散到相等或更多数量的表中。 innodb_purge_threads
因素会影响清除操作的速度。当有其它因素在起作用时,此因素可能有助于加快清除线程的速度。
选择合适的实例类
从设计上讲,清除操作是非侵入性的。清除线程在后台运行,与用户事务异步运行。对清除线程的期望是消耗尽可能少的系统资源,在合理的延迟内完成工作。MySQL 将 innodb_purge_threads
的最大值定义为 32。也就是说,您可以在 MySQL 数据库上配置最多 32 个清除线程。相比 max_connections
,这种设置并非用于为清除线程提供竞争优势,前者是为了让数千个并发用户能够接入繁忙的生产数据库。
当清除线程处理撤消日志或标记为删除的表记录时,线程需要从撤消日志页和 InnoDB 缓冲池中的数据页读取数据。当这些数据页不在缓冲池中时,线程将发出 I/O 调用来从存储中提取数据页。RDS 数据库实例的 CPU、内存和 IO 带宽等系统资源会显著影响清除操作的速度。
要想实现高速清除操作,不仅需要为清除线程规划容量,还需要为整个数据库工作负载规划容量。在资源不足或者用户事务大量占用系统资源的数据库实例上,由于资源争用,清除线程会变慢,清除延迟可能显示意外的结果。下图来自在 Aurora MySQL 数据库集群上开展的测试,集群有一个主(写入器)数据库实例位于 r7g.2xlarge 上,用于展示此类情况的例子。
在测试开始时,先按顺序加载两个不同的 Sysbench 数据集。首先,集群加载 10 个 Sysbench 表,每个表有 8 GB 数据,接下来,集群加载另一个包含 34 GB 数据的表。加载数据后,测试对 34 GB 的表运行 oltp_read_only.lua
工作负载。这个 34 GB 表的数据完全缓存在 InnoDB 缓冲池中,因为默认情况下 innodb_buffer_pool_size
设置为 42 GB。同时,在缓冲池中逐出了其他 10 个表的大部分数据。在只读工作负载完成之前,测试启动对另外 10 个表的 oltp_update_index.lua
工作负载。
![]() |
![]() |
您可以观察到以下情况:
SelectThroughput
和DMLThroughput
表明,两种不同类型的工作负载争用 InnoDB 缓冲池来加载各自的数据集。- 在
oltp_update_index.lua
工作负载结束时,RollbackSegmentHistoryListLength
超过了 500 万,相比之前的测试,这是意料之外的情况。在该测试中,我们对 10 个表以更高的速率(1.5 万与 1 万)运行了相同的oltp_update_index.lua
工作负载,而RollbackSegmentHistoryListLength
值达到了大约 50 万。 - 由于缓冲池争用,在
oltp_update_index.lua
工作负载启动时,BufferCacheHitRatio
出现急剧下降。即使在该工作负载完成后,此值仍然很低,这表明在将撤消日志或表记录提取到缓冲池时,清除线程遇到了 I/O 通道瓶颈。 - 向 InnoDB 缓冲池分配足够的内存会显著影响清除操作的速度。当缓冲池中没有所需的撤消日志或表数据时,清除线程的性能与 IO 延迟相关。
在 MySQL 数据库中,您可以通过更改 innodb_purge_threads
参数来配置清除线程的数量。如果您使用 RDS for MySQL,则默认值为 1,与 MySQL 社区版相同,您可以在数据库参数组中更改该值。如果您使用 Aurora MySQL,则默认值是一个公式,会随着数据库实例大小的增加而增加,您可以在集群参数组中进行更改。下表根据 r7g 实例类型的默认公式,列出了与清除相关的设置的有效值。
RDS 实例类型 | vCPU | 内存 (GiB) | innodb_buffer_pool_size(GiB) | innodb_purge_threads | innodb_purge_batch_size |
db.r7g.large | 2 | 16 | 7.76 | 1 | 600 |
db.r7g.xlarge | 4 | 32 | 19.36 | 1 | 600 |
db.r7g.2xlarge | 8 | 64 | 42.59 | 3 | 1800 |
db.r7g.4xlarge | 16 | 128 | 89.11 | 3 | 1800 |
db.r7g.8xlarge | 32 | 256 | 182.06 | 6 | 3600 |
db.r7g.12xlarge | 48 | 384 | 275.11 | 12 | 7200 |
db.r7g.16xlarge | 64 | 512 | 368.13 | 12 | 20000 |
对于 Aurora Serverless v2 实例类型,此设置会在实例纵向扩展或缩减时自动配置,不能由参数组修改。
监控和警报
对于监控 InnoDB 清除操作,一个为人熟知的指标是回滚段历史记录列表的长度,也称为清除延迟,该指标指示等待清除的撤消日志数量。在 MySQL 数据库中,您可以运行 SHOW ENGINE INNODB STATUS
来直接检查历史记录列表长度
。Aurora MySQL 在所有 3.0 版本中,提供 RollbackSegmentHistoryListLength
作为 CloudWatch 指标。Amazon RDS for MySQL 在性能详情中提供指标 trx_rseg_history_len
,您可将该指标发布到 CloudWatch。
此时比较好的做法是对该指标设置 CloudWatch 警报,以检测清除延迟远远落后并可能导致数据库性能问题的情况。您可以根据数据库曾经达到的历史正常值来设置警报阈值,以及设置触发警报时将执行的操作项。
如果您注意到数据库工作负载生成了过多的撤消日志,而清除线程的处理速度不够快,因而导致清除延迟很长,请增加数据库实例的大小,为清除线程分配更多系统资源。或者,您可以使用数据库分片架构,将工作负载分散到多个数据库分片上。MySQL 还提供 innodb_max_purge_lag
来设置回滚段历史记录列表长度的阈值。在超过了阈值时,系统通过引入最高值为 innodb_max_purge_lag_delay
的延迟,对 INSERT、DELETE 和 UPDATE 启动内部节流。您可以测试这些选项,验证哪一个最适合您的应用场景。
总结
提高 InnoDB 清除效率需要将工作负载优化、数据库容量规划和配置等方法结合使用。这篇博文提供了指南,用于指导您在 RDS for MySQL 数据库实例、Aurora MySQL 数据库集群或其它类型的 MySQL 数据库中如何实现这一目的。
本篇作者
Lei Zeng 亚马逊云科技数据库工程师。