一、引言:为什么 Purge 是 MySQL 性能优化中绕不开的一环
在 MySQL InnoDB 的日常运维与性能调优中,很多 DBA 都遇到过这样的场景:业务删除或更新了大量数据,磁盘空间却迟迟没有下降;或者数据库整体吞吐正常,但偶尔出现毛刺、主从延迟缓慢上升;又或者监控面板上某个叫做history list length的指标持续飙升。要解释这些现象,就必须深入理解 InnoDB 存储引擎中一个非常关键的后台机制:Undo Log 的清理机制,也就是 Purge。
很多人对 Undo Log 的认知停留在「回滚」层面:事务失败时,InnoDB 通过 Undo Log 把数据恢复到修改前的状态。这个理解没有错,但只是 Undo Log 一半的职责。Undo Log 的另一半职责,也是更复杂、更容易影响生产环境稳定性的部分,是为MVCC(多版本并发控制)服务。当一个事务提交后,它产生的 Undo Log 并不能立刻被删除,因为系统中可能还存在更早开启、尚未结束的事务,它们需要通过这段 Undo Log 读到「旧版本」的数据。只有等到没有任何事务再需要这些旧版本时,后台的 Purge 线程才会真正清理这些 Undo 记录,并同步完成真正的物理删除操作。
因此可以说,Purge 机制是 InnoDB 多版本并发控制闭环中的「清道夫」。Purge 效率高不高,直接决定了 Undo 历史链的长度、Undo 表空间的大小、删除数据的物理清理速度,以及是否会产生连锁的锁等待和性能抖动。本文将从 Undo Log 的基础原理讲起,逐步深入到 Purge 线程的架构与工作流程、如何判断 Purge 是否滞后、如何监控和排查,最后给出可落地的优化实战方案。
二、Undo Log 与 MVCC:理解 Purge 之前必须打牢的基础
要讲清楚 Purge,必须先理解 InnoDB 中的 Undo Log 和 MVCC。只有搞清楚「Undo Log 里到底存了什么」「为什么它不能随便删」,才能真正理解 Purge 存在的意义。
2.1 Undo Log 是什么
Undo Log(撤销日志)是 InnoDB 存储引擎维护的一种逻辑日志,它记录的是某条记录被修改之前的状态。与 Redo Log 记录「如何重新做一遍修改」相反,Undo Log 记录的是「如何撤销一遍修改」。它的核心作用有两个:
- 事务回滚:当一条 SQL 执行失败或用户显式执行 ROLLBACK 时,InnoDB 需要把已经修改的数据恢复原样,这就依靠 Undo Log。
- MVCC 读取旧版本:当某个事务执行一致性读(Consistent Read,即普通的 SELECT)时,它可能需要在某个时间点看到数据的快照版本,而当前最新的记录已经不是它该看到的版本。此时 InnoDB 通过 Undo Log 里保存的旧版本信息,构造出符合该事务可见性要求的版本。
需要特别强调的是,Undo Log 本身也会产生 Redo Log。也就是说,Undo Log 的写入是受 Redo Log 保护的,这是为了保证崩溃恢复时 Undo 信息自身也不会丢失。
2.2 Undo Log 的两大类型
InnoDB 根据操作类型把 Undo Log 分为两大类:Insert Undo和Update Undo。
| 类型 | 对应操作 | 生命周期特点 | 清理方式 |
|---|---|---|---|
| Insert Undo | INSERT 语句 | 只在事务回滚时需要;事务提交后即可释放 | 提交后直接释放回 undo 页面,无需进入 Purge 队列 |
| Update Undo | UPDATE、DELETE 语句 | 回滚需要;更关键的是 MVCC 读旧版本需要,必须等待所有可能需要它的读事务结束后才能清理 | 提交后进入 History List,由 Purge 线程统一清理 |
这个分类非常重要,因为它解释了为什么同样是修改数据,INSERT 和 UPDATE/DELETE 对 Purge 的压力完全不同。Insert Undo 在事务提交后基本不参与 MVCC 版本链,因为新插入的记录在它提交之前对其它事务根本不可见,提交之后其它事务也不会通过 Undo 去构造「插入之前」的版本。所以 Insert Undo 提交后就可以释放。而Update Undo 则必须等待所有可能读取旧版本的事务结束,这部分就构成了 Purge 的清理对象。
2.3 版本链与 Read View
InnoDB 的每一条聚簇索引记录上都有两个隐藏字段:DB_TRX_ID(最后一次修改该记录的事务 ID)和DB_ROLL_PTR(回滚指针,指向该记录上一个版本的 Undo Log)。当一条记录被多次修改时,硬盘上的最新记录通过回滚指针连到 Undo Log 里的旧版本,旧版本再通过自己的回滚指针连到更旧的版本,形成一条版本链。
一个事务执行一致性读时,会生成一个 Read View(读视图),其中记录了:
- 当前系统中活跃事务 ID 的集合;
- 最小的活跃事务 ID(即低水位);
- 下一个待分配的事务 ID(即高水位)。
InnoDB 拿着 Read View 沿版本链逐版本判断可见性:如果某个版本的 DB_TRX_ID 小于低水位,说明该版本由已提交的事务产生,通常可见;如果版本的事务 ID 在活跃事务列表中,说明它由尚未提交的事务产生,不可见,需要继续沿着回滚指针找上一个版本。就这样,通过 Undo Log 组成的版本链,每个事务都能看到一份符合自身隔离级别语义的数据快照。
从这段机制中可以直接推导出一个结论:只要系统中还存在活跃的、可能读到某个旧版本的事务,那个旧版本对应的 Undo Log 就不能被删除。这就是 Purge 无法实时完成的最根本原因。
三、Purge 机制全景:到底在清理什么
有了上面的基础,我们就可以给 Purge 下一个比较完整的定义了。
3.1 Purge 的定义
Purge 是 InnoDB 后台线程对「已经不再被任何事务需要的 Undo Log 记录」进行回收清理的机制。这里的「清理」至少包含两层含义:
- 清理 Undo Log 记录本身:把那些不再被任何 Read View 需要的旧版本记录从 Undo 页面中移除,并释放 Undo 页面空间。
- 完成物理删除:对于被标记为删除(Delete Mark)的记录,Purge 会真正把它们从索引页面中删除,这是数据最终消失的环节。
换句话说,当用户执行 DELETE 或 UPDATE 时,前端 SQL 执行只完成了「逻辑上的变更」,真正把旧数据从物理结构中抹掉的活,是 Purge 干的。这也解释了为什么「DELETE 之后磁盘空间不减少」在 InnoDB 中并不是 Bug,而是机制使然。
3.2 为什么不能立即删除 Undo Log
很多初学者会问:事务已经提交了,为什么 Undo Log 还要保留?答案在 MVCC。设想下面这个时序:
- 事务 A 在 T1 时刻开启,执行一条普通 SELECT,它会建立自己的 Read View。
- 事务 B 在 T1 之后 T2 时刻修改了某条记录并提交。此时这条记录的最新版本由 B 产生,而 A 在 T1 建立 Read View 时要读到的是 B 修改之前的旧版本。
- 如果 B 提交后立刻把旧版本 Undo Log 删除,事务 A 再读这条记录时就找不到旧版本了,会读到错误的数据,破坏一致性读语义。
因此,只要事务 A 还没有结束,B 提交产生的 Undo Log 就仍然有可能被 A 用到,不能删除。只有当系统中最老的那个活跃事务也结束了,比它提交更早的 Undo 记录才彻底失去价值,Purge 才能安全地处理它们。
3.3 哪些数据需要被清理
梳理一下,Purge 需要处理的对象主要包括:
- 已提交事务产生的 Update Undo 记录:包含 UPDATE 操作留下的旧版本和 DELETE 操作的标记信息。这是 Purge 最主要、也最耗时的处理对象。
- 标记删除但尚未物理删除的索引记录:DELETE 操作在聚簇索引和二级索引上留下删除标记,Purge 时对它们做真正的删除。
- 相关的锁和内存结构:与这些 Undo 记录关联的锁信息、内存中的 undo 结构等,也会在 Purge 过程中一并清理。
值得注意的是,当一个 UPDATE 涉及二级索引时,情况会更复杂:InnoDB 在更新二级索引时,采用的是「先标记删除旧索引项,再插入新索引项」的策略。这些被标记删除的二级索引项,同样要等到 Purge 阶段才会被真正移除。因此,大量 UPDATE 二级索引列的语句,对 Purge 的压力往往比单纯的 DELETE 更大。
四、Purge 的底层执行原理
了解了「清什么」,接下来看「怎么清」。这一部分涉及 History List、Undo 记录的遍历和解析、以及真实删除的处理细节。
4.1 事务提交后 Undo 的去向
当一个事务提交时,InnoDB 会把这个事务产生的 Update Undo 记录挂到一个全局链表上,这个链表就是History List。可以把这个链表理解为「待清理的 Undo 记录队列」。链表中的 Undo 记录按照事务提交的先后顺序排列,通常越靠近链表尾部的记录越旧。
同时,系统会为每个事务记录的 Undo Log 保存一个事务编号(Trx No)。Purge 线程需要知道「清理到哪个事务为止」,这个边界由当前所有活跃事务中最老的那个决定,从而保证 Purge 不会删除任何活跃事务还需要的版本。
4.2 Read View 与 Purge 的推进边界
前面说过,Purge 不能清理「可能还要用」的 Undo。那 Purge 如何确定处理边界?在可重复读(REPEATABLE READ)隔离级别下,InnoDB 通过当前系统中所有活跃事务构造一个全局视图来判断可清理范围。简单地说,Purge 需要找到一个时间点,使得在此之前提交的事务产生的 Undo 记录,已经不会再被任何现有事务读取。
这里有一个非常重要的推论:长事务会严重阻塞 Purge。如果系统中有一个执行时间很长的只读事务一直停留在某个旧 Read View 上,那么从这个 Read View 之后再提交的事务,它们的 Undo 都不能清理,History List 会不断增长。这也是为什么在 InnoDB 中,一批长时间不结束的查询往往比高并发写入更可怕的原因之一。
4.3 Delete Mark 与真正的删除
InnoDB 对 DELETE 操作采用的是标记删除(Delete Mark)策略,而不是立即从索引页中物理删除记录。执行 DELETE 时,InnoDB 会在记录的删除标记位上打上标记,同时把该记录对应的 Undo 信息写入 Undo Log。这样做的好处是:
- 如果事务需要回滚,只需清除删除标记即可,代价很低。
- 未提交事务删除的记录,对其他事务仍然「看起来还在」,符合隔离性要求。
事务提交后,该条记录的删除标记仍然保留,直到 Purge 线程处理它时才真正从索引页面中移除。如果一条记录上还存在指向它的二级索引项,Purge 还需要同步清理这些二级索引项。
4.4 Purge 处理一条 Undo 记录的完整流程
Purge 线程处理 Update Undo 记录的过程大致可以拆解为以下几步:
- 定位 Undo 记录:Parsing 已提交事务的 Undo Log,找到需要清理的记录以及对应的表、索引和主键。
- 读取聚簇索引记录:根据主键回到聚簇索引中读取当前记录,判断该记录是否仍然有效。
- 执行物理删除或版本清理:
- 对于 DELETE 产生的 Undo,如果记录上还有删除标记,则真正删除该簇索引记录;同时清理对应的二级索引项。
- 对于 UPDATE 产生的 Undo,需要清理对应的上一个版本信息;如果更新涉及二级索引,还要处理二级索引中的旧版本项。
- 释放 Undo 空间:当一条 Undo 记录被处理完成后,其占用的 Undo 页空间被释放,供后续事务复用。
可以看到,Purge 并不是简单地在 Undo 链表上做删除操作,它还要回到索引页里做真正的数据删除、二级索引清理和空间释放,因此 Purge 对 CPU、内存和磁盘 I/O 都会产生开销。
五、Purge 线程架构与调度策略
理解了原理之后,再看 Purge 在 InnoDB 内部是如何被组织执行的,这直接关系到我们后面如何通过参数调优。
5.1 从单线程到多线程的演进
在非常早期的 MySQL 版本中,Purge 完全由 Master Thread 内联执行,或者只有一个 Purge 线程。当写入量较大时,单个线程既要负责清理 Undo 链、又要回到索引页删除数据,很容易成为瓶颈,导致 History List 越积越长。
从 MySQL 5.5.4 开始,InnoDB 引入了参数innodb_purge_threads,允许把 Purge 从 Master Thread 中分离出来。随后在 MySQL 5.6 及之后的版本中,Purge 的并行能力逐步增强,到了 MySQL 5.7 和 8.0,多线程 Purge 已经成为默认配置,Purge 能力有了明显提升。
5.2 协调线程与工作线程
当innodb_purge_threads大于 1 时,InnoDB 会把 Purge 线程分为两类角色:
- 协调线程(Coordinator Thread):只有一个,负责从 History List 中按批读取需要清理的 Undo 记录,然后把任务分发给工作线程。
- 工作线程(Worker Thread):数量由
innodb_purge_threads - 1决定,负责执行真正的清理动作,即回到索引页删除记录、清理二级索引、释放 Undo 空间。
协调线程本身也承担一部分清理工作,因此整体并行度接近innodb_purge_threads的值。
5.3 批处理与任务分配
Purge 并不是一条一条处理 Undo 记录,而是采用批量任务的方式。每次从 History List 中拉取一批 Undo 记录形成任务队列,再分发给工作线程并行处理。这里涉及两个关键参数:
innodb_purge_batch_size:每次 Purge 批量处理的 Undo 记录数,默认值为 300。它控制的是一个批次的任务规模。innodb_purge_threads:Purge 线程总数量,默认值在不同版本中有所变化,MySQL 5.7 和 8.0 默认通常为 4,允许设置的最大值在较新版本中为 32。
需要注意的是,并行度并不是越高越好。多个 Purge 线程同时对索引页做删除时,可能会争抢同一批索引页或产生额外的锁开销,需要根据实际负载测试确定合理的线程数。
5.4 Purge 与 DML 的相互制约
Purge 与前台 DML 是一种互相依赖又互相竞争的关系。一方面,前台写入持续产生新的 Undo 记录,让 History List 变长,给 Purge 制造工作;另一方面,如果 Purge 长时间落后,Undo 空间不断膨胀,反过来又会拖慢前台写入、查询,甚至触发参数innodb_max_purge_lag所定义的延迟机制(后文会详细介绍)。
此外,Purge 线程在物理删除二级索引记录时,需要获取相应的锁和页资源;当前台业务也在高频访问同一批索引时,二者会产生竞争。这也是为什么一些写入密集场景下,可以观察到 Purge 线程有较高的等待时间。
六、Undo 表空间与回滚段管理
Purge 清理的是 Undo Log,而 Undo Log 存放的位置是 Undo 表空间中的回滚段。要判断「空间为什么不回收」,必须先搞清楚 Undo 空间是如何组织、分配和回收的。
6.1 Undo 表空间
在 MySQL 5.6 之前,Undo Log 存放在共享的系统表空间ibdata1中,这带来一个很大的麻烦:Undo 空间一旦使用,即使 Purge 清理了里面的记录,ibdata1也很难缩小,导致系统表空间越用越大。MySQL 5.6 引入了独立的 Undo 表空间,MySQL 8.0 之后则默认由 InnoDB 自行管理 Undo 表空间,默认创建两个 Undo 表空间文件,例如undo_001和undo_002。
独立 Undo 表空间的最大好处是:Undo 空间的 truncate 成为可能。当 Undo 表空间膨胀到一定程度后,InnoDB 可以在满足条件时把它截断收缩,真正把空间还给操作系统。
6.2 回滚段
每个 Undo 表空间内部又包含若干回滚段(Rollback Segment)。MySQL 5.6 中默认一个 Undo 表空间包含 128 个回滚段,MySQL 8.0 中每个 Undo 表空间默认也包含数量可观的回滚段,具体数值与版本和配置有关。每个回滚段内部又进一步划分为多个Undo Slot,通常回滚段包含 1024 个 Slot,每个 Slot 对应一个事务或特定场景使用。
事务在写入 Undo Log 时,会根据规则分配到某个回滚段的某个 Slot 中。对于一般事务,MySQL 5.6 之后可以使用的大部分 Slot 属于「非临时回滚段」;对于修改临时表的事务,则使用临时回滚段。不同回滚段之间相对独立,这为多线程并发写入 Undo 提供了条件。
6.3 Undo 页的复用
Purge 完成以后,被清理出的 Undo 页面并不会立刻归还给操作系统,而是先进入一个「可复用」状态,供后续事务继续写入。只有当整个 Undo 表空间满足收缩条件时,InnoDB 才会真正通过重建表空间文件的方式把磁盘占用降下来。因此,常见的「删除数据后 .ibd 文件大小不变」「undo 文件大小不变」现象,本质上是因为物理空间进入了复用池,而不是还被有效数据占用。
6.4 Undo 表空间的自动收缩
MySQL 8.0 引入了Undo 表空间自动 Truncate机制,由以下参数控制:
innodb_undo_log_truncate:是否启用 Undo 表空间自动截断,MySQL 8.0 默认开启。innodb_max_undo_log_size:单个 Undo 表空间达到多大时触发截断,默认约 1GB。innodb_purge_rseg_truncate_frequency:控制 Purge 过程中检查回滚段是否可截断的频率,默认 128,即 Purge 每处理 128 个批次后检查一次。
截断的大致流程是:InnoDB 选择一个满足条件的 Undo 表空间,将其标记为非活跃状态,把新事务的 Undo 写入切换到其他表空间,等待该表空间内所有事务结束后,把文件重建为初始大小,再重新投入使用。这个机制让 Undo 空间在长期运行后仍有能力收缩,缓解了历史遗留版本的膨胀问题。
七、如何判断 Purge 是否滞后
在发生性能问题之前,Purge 通常会留下大量可观测的线索。学会解读这些线索,是判断「Purge 到底是不是瓶颈」的关键。
7.1 最核心的指标:History List Length
History List Length是判断 Purge 是否滞后的第一指标。它表示当前 Undo 页中尚未清理的历史记录数量。这个值越大,说明待清理的历史版本越多。
可以通过全局状态变量查看:
SHOW GLOBAL STATUS LIKE 'Innodb_history_list_length';结果类似Innodb_history_list_length | 0,也可能是一个很大的数字。判断标准不是绝对阈值,而是趋势:
- 如果值一直保持在较低水平,说明 Purge 能跟上写入节奏。
- 如果在业务流量变化不大的情况下持续增长,说明 Purge 处理不过来,或者存在长事务拖住了清理边界。
- 如果值突然暴涨后回落,通常对应一次大批量 DELETE/UPDATE 或长事务结束后的集中清理。
在 InnoDB 内部,这个指标也被称作innodb_history_list_length,是巡检脚本中必须采集的项。
7.2 Trx id counter 与 Purge done 的关系
执行SHOW ENGINE INNODB STATUS后,事务段落里会看到类似下面的输出:
------------ TRANSACTIONS ------------ Trx id counter 189068 Purge done for trx's n:o < 188963 undo n:o < 0其中:
Trx id counter:当前已经分配到的最大事务 ID,反映写入的推进速度。Purge done for trx's n:o:Purge 已经清理到的事务编号。
二者之间存在一个差值,表示「已经提交但尚未被 Purge 清理」的事务区间。如果Trx id counter持续增长,而Purge done长期停滞不前,说明 Purge 被阻塞或速度跟不上,需要进一步定位原因。
7.3 Undo 空间与磁盘占用的变化
Undo 表空间文件的增长速度也是重要的间接指标。在 Linux 下可以定期观察 Undo 文件大小:
ls -lh /var/lib/mysql/undo_001 /var/lib/mysql/undo_002如果 Undo 文件在短时间内快速增大,而业务写入量没有明显增加,就要高度怀疑 Purge 被长事务阻塞或清理能力不足。配合innodb_undo_log_truncate的状态,还可以判断表空间是否曾尝试收缩却无法收缩。
7.4 与慢查询和锁等待的关联
Purge 滞后本身不一定会直接出现慢查询,但它会间接影响:
- Undo 版本链变长,一致性读需要沿更长的链回溯,扫描成本上升。
- 旧版本大量堆积导致 B-Tree 页面中残留大量待删除记录,索引变大、缓存命中率下降。
- Undo 空间膨胀到触发
innodb_max_purge_lag时,前台 DML 会被动延迟,响应时间上升。
因此,当出现「无明显慢 SQL 但整体延迟上升、锁等待增多」时,应把 Purge 纳入排查范围。
八、Purge 监控实战:把指标落到可执行的脚本里
光知道指标还不够,生产环境需要有自动化的采集和预警方式。下面给出几种常用的监控手段。
8.1 使用 SHOW ENGINE INNODB STATUS 快速诊断
这是最直接、无需额外权限的诊断入口。执行:
SHOW ENGINE INNODB STATUS\G重点关注TRANSACTIONS段落中的以下内容:
Trx id counter与Purge done for trx's n:o的差值;History list length的具体数值;- 当前活跃事务列表,特别是那些运行时间很长的只读事务;
如果存在一个运行数小时甚至数天的 SELECT 事务,它很可能就是拖住 Purge 边界的元凶。
8.2 查询 information_schema.innodb_trx 定位长事务
通过information_schema.innodb_trx可以找出当前正在执行的事务以及它们的运行时间:
SELECT trx_id, trx_state, trx_started, NOW() - trx_started AS running_time, trx_mysql_thread_id, trx_query, trx_rows_modified, trx_rows_locked FROM information_schema.innodb_trx ORDER BY trx_started ASC LIMIT 20;其中trx_started最早的记录就是最老的事务。对运行时间异常长的查询,要结合performance_schema或慢查询日志找到对应的连接和 SQL,判断是等待、执行慢,还是应用侧忘记提交或忘记关闭事务。
8.3 通过 performance_schema 关联线程与 SQL
拿到trx_mysql_thread_id后,可以进一步关联到performance_schema.threads查当前线程正在执行的 SQL 或状态:
SELECT t.PROCESSLIST_ID, t.PROCESSLIST_USER, t.PROCESSLIST_HOST, t.PROCESSLIST_TIME, t.PROCESSLIST_INFO, t.PROCESSLIST_STATE FROM performance_schema.threads t WHERE t.PROCESSLIST_ID = <线程ID>;当发现PROCESSLIST_COMMAND为 Sleep 且连接长期空闲时,往往意味着应用开启了事务后忘记提交,需要检查连接池设置和业务代码的事务边界。
8.4 利用 innodb_metrics 表做精细观测
MySQL 5.7 及以上版本提供了information_schema.innodb_metrics,其中包含一组与 Purge 和 Undo 相关的计数器,例如:
trx_rseg_history_len:等价于 History List Length。trx_undo_slots_used:当前使用的 Undo Slot 数量。trx_undo_slots_cached:缓存的 Undo Slot 数量。
示例:
SELECT name, subsystem, type, comment, status FROM information_schema.innodb_metrics WHERE name IN ('trx_rseg_history_len','trx_undo_slots_used','trx_undo_slots_cached');在多线程写入压力较大的环境中,关注trx_undo_slots_cached的增长,可以帮助判断 Undo 分配压力。若缓存槽位持续高位运行,说明 Undo 页的分配和回收较为频繁,也可能是 Purge 释放空间的速度不足。
8.5 推荐的自定义巡检脚本
生产定时巡检不一定需要复杂工具,简单 shell 加上 mysql 客户端即可。下面给出一个轻量巡检片段:
#!/bin/bash HISTORY_LEN=$(mysql -N -e "SHOW GLOBAL STATUS LIKE 'Innodb_history_list_length'" | awk '{print $2}') UNDO_SIZE=$(du -sm /var/lib/mysql/undo_* | awk '{sum+=$1} END {print sum}') echo "history_list_length=${HISTORY_LEN}" echo "undo_tablespace_size_mb=${UNDO_SIZE}" if [ "${HISTORY_LEN}" -gt 100000 ]; then echo "WARN: history list length is too high" fi实际使用时应根据业务规模调整告警阈值,并结合长事务查询脚本一起运行。注意脚本中的路径应根据实际数据目录修改。
九、Purge 优化实战:从参数到业务设计
发现 Purge 滞后后,优化思路可以归纳为三个层面:提升 Purge 自身的处理能力、移除阻塞 Purge 的障碍、从源头减少 Undo 产生量。
9.1 适当增加 Purge 线程数
如果确认 History List 在工作负载下持续增长,且系统 CPU 和 I/O 资源还有余量,可以尝试提高innodb_purge_threads:
SET GLOBAL innodb_purge_threads = 8;并通过配置文件持久化:
[mysqld] innodb_purge_threads = 8需要注意:
- 线程数建议从默认值开始小步提升,例如 4 到 8,观察效果后再继续。
- 若 CPU 核数较少、或已经存在较多活跃连接,盲目调高会造成线程争抢,反而降低整体性能。
- MySQL 5.7 与 8.0 对多线程 Purge 的支持成熟度不同,8.0 的并行清理效率通常更好,必要时可评估版本升级。
9.2 调整每次批处理规模
innodb_purge_batch_size控制每次 Purge 批量读取的 Undo 记录数,默认为 300。这个参数在很多版本中属于动态可调参数,但在调优前应确认当前 MySQL 版本是否支持在线修改。
该参数的调整思路是:
- 如果 History List 增长快,且 Purge 线程执行速度受限于频繁的任务调度,可以适当增大 batch size,减少调度开销。
- 过大的 batch size 可能导致单批任务占用较多页锁,影响前台 DML,因此要结合业务延迟观察。
一般建议先保持默认,只有在观测到明显瓶颈时才做实验性调整。
9.3 使用 innodb_max_purge_lag 保护系统
innodb_max_purge_lag是一个保护性参数。当 Purge 滞后的历史记录数超过该值时,InnoDB 会让前台 DML 操作主动延迟,从而给 Purge 让路。相关参数还有innodb_max_purge_lag_delay,表示延迟的最大毫秒数。
[mysqld] innodb_max_purge_lag = 100000 innodb_max_purge_lag_delay = 50这样配置的含义是:当 History List 超过 10 万时,前台 DML 会开始被限速,每次操作最多延迟 50 毫秒。该机制能防止 Undo 链无限膨胀,但它本质上是「牺牲写入延迟换取空间可控」。在写入延迟非常敏感的业务中,应谨慎使用,并优先从根源上解决长事务和清理能力问题。
9.4 消除长事务:最直接有效的优化
前面反复提到,长事务会阻塞 Purge 的清理边界。只要有一个旧 Read View 卡在那里,虽然读取它的查询本身可能消耗很低,却会让后续大量已提交的 Undo 记录都无法清理。因此,定位并终止或优化长事务,是解决 Purge 滞后的首要手段。
具体建议:
- 对于可重复读隔离级别下的普通查询,尽量缩短事务执行时间,避免在事务内做大量计算、调用外部服务。
- 检查应用连接池是否存在「连接归还但事务未提交」的情况。
- 对于报表类长查询,考虑放到从库执行,必要时使用读已提交(READ COMMITTED)隔离级别,它的事务生命周期更短,对 Purge 的阻塞更小。
- 对确实异常的旧事务,在确认不影响业务的前提下果断 kill:
KILL <线程ID>。
9.5 优化大批量 DELETE 与 UPDATE 的执行方式
一次性删除数百万行数据,会在短时间内产生巨量 Undo,History List 瞬间飙升,还可能撑爆 Undo 表空间。更稳妥的做法是分批处理,每批删除或更新少量行,并在批次之间让 Purge 有机会推进。示例:
-- 不推荐:一次性删除 DELETE FROM large_table WHERE create_time < DATE_SUB(NOW(), INTERVAL 6 MONTH); -- 推荐:分批删除 DELIMITER // CREATE PROCEDURE batch_delete() BEGIN DECLARE affected INT DEFAULT 1; WHILE affected > 0 DO DELETE FROM large_table WHERE create_time < DATE_SUB(NOW(), INTERVAL 6 MONTH) LIMIT 5000; SET affected = ROW_COUNT(); DO SLEEP(0.1); END WHILE; END// DELIMITER ;每批删除的行数建议根据表结构和索引情况调整,通常在几千行到几万行之间。批次之间短暂休眠,既能让 Purge 跟进,也能减小对在线业务的锁压力和主从延迟压力。
对于 UPDATE 操作同样如此。特别要注意:更新二级索引列会产生更多的二级索引旧版本,如果大批量更新高基数二级索引字段,Purge 需要处理的二级索引删除会非常多,更应分批执行。
9.6 合理设计索引,降低二级索引 Purge 成本
Purge 的成本中,很大一部分来自二级索引的物理删除。如果一个表上有大量二级索引,或者某些索引的区分度很低却仍然被频繁 DELETE/UPDATE 扫到,Purge 需要逐个索引删除对应项,开销成倍增加。
优化建议:
- 删除无用的、冗余的二级索引,减少 Purge 需要维护的索引项数量。
- 对于频繁做范围删除的表,如果某些业务查询确实需要索引,应保留;但仍需评估索引数量与写入、Purge 成本的平衡。
- 在 MySQL 8.0 中,某些 DDL 支持瞬时完成,但仍需评估 DDL 期间对 Purge 和历史版本的额外影响。
9.7 关注 Undo 表空间收缩能力
如果 Undo 文件已经膨胀,且历史版本堆积问题已经解决,应检查自动收缩是否正常工作:
SHOW VARIABLES LIKE 'innodb_undo_log_truncate'; SHOW VARIABLES LIKE 'innodb_max_undo_log_size';确认innodb_undo_log_truncate为 ON,并将innodb_max_undo_log_size设置为适合当前磁盘容量的值(例如 1GB 或 2GB)。如果 Undo 表空间长期超过阈值却没有收缩,需要检查是否存在长时间占用该表空间的事务,或者截断频率参数过低。
若版本较老(例如 MySQL 5.6/5.7 的某些小版本),自动收缩能力有限,可能需要通过重启实例或重建 Undo 表空间的方式回收空间,操作前务必做好备份和测试。
9.8 评估版本升级带来的效益
MySQL 5.6、5.7、8.0 三代版本的 Purge 实现差异较大。8.0 对 Undo 表空间管理、多线程 Purge、临时表 Undo 处理做了大量改进,并且支持更成熟的自动 Truncate。如果你的实例还停留在 5.6,且长期受 Purge 滞后、Undo 膨胀困扰,升级到 8.0 可能是比反复调参更彻底的方案。当然,升级需要综合评估兼容性、测试成本和业务风险,不能仅凭单一指标仓促决定。
十、常见问题排查与案例解析
本节把散落在前面的知识点串起来,通过几个典型现象给出排查路径和处置建议。
10.1 现象:Undo 表空间持续膨胀,甚至撑满磁盘
排查路径:
- 查看
Innodb_history_list_length,确认历史版本是否堆积。 - 查看
information_schema.innodb_trx,找出最老事务及其运行时长。 - 确认是否有人执行大批量 DELETE/UPDATE 而 Purge 清理不过来。
- 检查
innodb_undo_log_truncate是否开启、innodb_max_undo_log_size是否合理。
常见原因与处置:
- 存在长事务:定位并结束长事务,必要时 kill 连接。
- Purge 线程不足:适当增加
innodb_purge_threads。 - 大批量删除未分批:暂停任务,改为分批执行。
- 老版本不支持自动截断:评估升级,或计划窗口内重建 Undo 空间。
10.2 现象:History List Length 一直增长,但写入量并不大
这种情况几乎可以确定是清理边界被卡住,而不是 Purge 处理能力不够。重点排查:
- 通过
innodb_trx找运行时间最长的只读事务,观察它的trx_started。 - 检查应用层是否在事务中执行了长时间的外部调用,例如在事务里发起 HTTP 请求、调用 RPC、等待消息队列。
- 检查是否有备份工具或其他读库任务开启了长时间一致性快照。
对于 Java 应用,这类问题常与@Transactional范围过大、未正确提交等原因有关;对于 Python/Go 等应用,则要检查是否忘记 Commit 或连接归还后事务未清理。
10.3 现象:删除大量数据后,表文件大小不下降
这是 InnoDB 的正常行为,不一定属于故障。DELETE 完成后,记录只是被标记删除,真正物理删除由 Purge 完成,而 Purge 释放出来的页面进入可用页池,供后续写入复用,并不会主动把文件尾部截掉。若确实需要回收磁盘空间,可以考虑:
- 对表执行
OPTIMIZE TABLE或在线重建表,把碎片整理并回收空间; - 使用
ALTER TABLE ... ENGINE=InnoDB重建; - 如果是 Undo 空间问题,则参考 10.1 的排查方法。
要注意,重建大表本身会产生大量 Redo 和 Undo,且有些工具在执行期间可能影响在线事务,应在低峰期、做好备份后进行。
10.4 现象:主从延迟缓慢扩大,且与 Purge 有关
主从延迟的原因很多,其中一类确实与 Purge 及 Undo 相关。MySQL 5.7 时代,从库 SQL 线程在执行大批量 DELETE 或 UPDATE 时,回放速度可能较慢,同时这些操作产生大量 Undo,进一步加剧从库 Purge 压力。主库已经完成逻辑删除的部分,从库还需要逐行回放、逐行写 Undo,最终表现为主从延迟不断扩大。
应对思路:
- 源头分批:让主库的大批量删除分批执行,避免从库单批回放过重。
- 使用并行回放:开启基于组提交或写集合的并行复制,提升从库回放能力。
- 升级 MySQL 8.0:利用更强的并行复制和更优化的 Purge 实现。
- 评估业务:对于过期数据的删除,是否可以使用
TRUNCATE或分区表DROP PARTITION等更快、几乎不产生大量 Undo 的方式。
10.5 现象:监控显示大量Purging状态的查询或线程
如果发现很多线程状态为Purging,并不一定都是问题,这可能只是 Purge 线程正在工作。真正需要警惕的是:Purging 进程长期占用大量 CPU 或 I/O,同时 History List 却降不下来。这说明 Purge 线程干活了但效率不高,常见原因有:
- 二级索引过多,Purge 在大量索引之间反复查找删除。
- Undo 版本链非常长,每次读取旧版本都要回退很多层。
- 系统 I/O 能力不足,索引页读入慢。
此时应从减少二级索引、优化大批量写操作、提升磁盘 I/O 性能等方面入手。
十一、总结与最佳实践清单
Purge 是 InnoDB 多版本并发控制机制中承上启下的关键环节:它承接已提交事务遗留的历史版本,驱动真正的物理删除和 Undo 空间回收。理解 Purge,不只是理解一个后台线程,而是理解 InnoDB 如何在「一致性读」与「空间与性能」之间做平衡。
归纳全文,可以把 Purge 相关的核心知识与优化动作浓缩为以下清单:
- 理解对象:Purge 主要清理 Update Undo 和标记删除的索引记录;Insert Undo 提交后即可释放,不进入 Purge 主流程。
- 紧盯指标:
Innodb_history_list_length是首要指标;SHOW ENGINE INNODB STATUS中的 Trx id counter 与 Purge done 差值反映清理进度。 - 先找元凶:长事务是 Purge 滞后的最常见原因,优先查
information_schema.innodb_trx中最老事务。 - 再调能力:确认系统资源有余量后,可适当增加
innodb_purge_threads,并谨慎试验innodb_purge_batch_size。 - 必要时限速:
innodb_max_purge_lag与innodb_max_purge_lag_delay可防止 Undo 无限膨胀,但本质是延迟换空间。 - 源头治理:大批量 DELETE/UPDATE 分批执行,减少 Update Undo 的瞬间冲击;合理精简二级索引,降低 Purge 物理删除成本。
- 空间回收:MySQL 8.0 通过
innodb_undo_log_truncate与innodb_max_undo_log_size支持自动收缩;老版本需评估升级或手动重建。 - 版本红利:5.6、5.7、8.0 的 Purge 与 Undo 管理差异明显,长期受此类问题困扰时,应把版本升级纳入整体方案。
最后要提醒的是,Purge 调优和所有 MySQL 调优一样,不能脱离业务负载空谈参数。每一个innodb_purge_threads的调整、每一批删除行数的设定,都需要结合具体的硬件规格、表结构、索引数量和线上监控数据来验证。把原理吃透、把指标看准、把动作做小,才能让 Purge 这台「清道夫」稳定高效地运转,为业务持续提供可预期的写入性能与空间可控性。