ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MySQL 5.7 Undo表空间膨胀排查与清理实战

MySQL 5.7 Undo表空间膨胀排查与清理实战 1. 磁盘告警响起来那一刻问题现场与最初的误判上个月我们一台核心业务MySQL实例5.7.29版本触发了磁盘容量告警。监控上显示数据盘使用率已经爬到93%而前一天还在80%附近。我的第一反应是binlog又没被及时清理因为这台实例的binlog曾经出过一次忘记配置expire_logs_days导致磁盘打满的事故所以我下意识先去查了binlog列表。binlog确实积压了不少我马上按保留周期做了purge磁盘使用率确实掉了几个点但离告警线还有很远。于是我又在数据目录里翻大文件结果发现真正的罪魁祸首是三个undo文件undo_001、undo_002、undo_003。这三个文件加起来超过35GB其中最夸张的一个接近18GB。我印象里5.7的undo文件一般就是1GB左右这明显不正常。一开始我和很多半路出道的运维一样第一个想法是直接把文件删了不就行了。这个想法差点把我带进沟里。undo文件并不是像binlog那样可以随意删除的日志它是InnoDB回滚段和多版本控制的核心载体脏删文件轻则导致数据库启动失败重则直接触发数据文件损坏警告。如果你也遇到同样的故障请先克制住删文件的冲动先把问题所在搞清楚。这个事故最终处理完花了大概半天时间期间我还踩了事务永远杀不完和purge线程不工作两个坑。这篇文章我会把完整的排查思路、清理动作和后续防膨胀措施都写出来希望对遇到同样问题的人有帮助。1.1 本次故障的实例背景先把背景交代清楚方便你对号入座数据库版本MySQL 5.7.29InnoDB存储引擎。部署方式单机物理机数据目录在独立数据盘上。典型业务电商类订单系统白天高峰时段写入TPS大概2000到3000同时存在一些报表类的长查询。存储配置没有单独规划undo目录undo表空间和主数据文件在同一块盘上。这个配置很常见恰恰因为常见undo膨胀的问题才容易被忽略。很多人会在意慢查询、在意buffer pool、在意binlog但对undo的维护没什么概念我也是吃了这次亏之后才专门补齐了这块知识盲区。1.2 它是怎么从1GB涨到18GB的在排查过程中我翻看了这台实例的监控记录发现undo_003文件在一个月内从1GB增长到18GB增长曲线几乎是垂直拉升。这说明不是偶发的瞬时大事务造成的而是有一个持续性因素在阻止undo log被回收。这个因素本质上就是有事务长期持有旧版本的undo链。InnoDB只有在确认这个旧版本已经没有任何快照会用到的情况下才会通过purge线程去清除undo记录。如果存在长时间运行的事务哪怕这个事务只是开启后一直发呆它也会让InnoDB不敢清理它之后产生的undo记录。这种现象的典型表现就是一个70GB的库数据没怎么变undo文件却涨到30GB以上而且无论你怎么等待它都不会自己降下来。2. 为什么undo log能拖垮磁盘InnoDB的undo机制与膨胀根因2.1 Undo log在InnoDB里到底是干什么的要解决undo膨胀先得理解undo是什么。InnoDB的最核心特性之一就是MVCC多版本并发控制允许多个事务同时读取同一个数据行的不同版本。举一个最简单的场景事务A修改了一行数据事务B紧接着去读这行数据。在默认的REPEATABLE READ隔离级别下事务B应该看到事务A修改之前的值那么这行数据的旧版本放在哪答案就是undo log。每次事务修改数据之前InnoDB会把修改前的行记录写入undo log形成一个版本链。当事务B需要读旧版本时InnoDB会顺着版本链找到对应的历史版本。等所有并发事务都结束、不再需要这些历史版本后后台的purge线程才会把这些undo记录标记为可复用。所以undo log的存在时间点非常关键它的生命周期不取决于写事务本身是否已经结束而取决于所有可能读取旧版本的读快照是否都已经关闭。这就解释了为什么一个只开启但一直不提交、不查询的事务也能让undo文件疯狂膨胀——它虽然不干活但它蹲在那一动不动InnoDB就得一直保留着它开启时刻之后所有的历史版本。2.2 为什么删了undo文件是绝对不可取的很多第一次遇到这个问题的人都会问既然undo文件只是存放旧版本数据那我把文件删掉让InnoDB重新创建一个不就行了吗这里有几个原因导致这个操作极其危险undo文件内部管理着回滚段rollback segment回滚段里有undo slot的分配映射删除文件会让InnoDB找不到这些回滚段。InnoDB在启动时会扫描所有的表空间文件如果发现配置了innodb_undo_tablespaces指定的数量与实际存在的文件数不一致可能会直接拒绝启动。即使侥幸启动成功事务回滚和崩溃恢复也会失去依赖轻则报错重则数据页损坏。你可以把undo表空间理解为检察院的案卷室案卷不会因为案件结案就立刻被销毁它要等过了追溯期、确认无人再调阅才能销毁。你直接把案卷室拆了后续所有需要调案卷的操作都会出错。所以正确处理方式是在InnoDB机制允许的前提下让旧undo表空间退役再建立新的可复用空间。2.3 判断undo膨胀严重程度的关键指标快速定位是不是undo膨胀问题要看两个关键指标history list length历史链表长度和undo表空间的实际大小。SHOW ENGINE INNODB STATUS\G执行这条命令后在TRANSACTIONS段落里可以看到类似下面这行History list length 864257正常健康实例的History list length一般在几百到几千这个量级。我这个实例在故障期间的数字长期维持在几十万甚至上百万级别这个值越大表示积压的undo记录越多purge清理的压力也越大。另一个指标是information_schema.innodb_trx表它能列出当前所有活跃事务及其启动时间这是找到元凶事务的关键入口。我会在下一章具体演示整个排查过程。3. 完整的排查链路从表空间到长事务再到purge线程3.1 第一步确认当前的undo表空间配置和状态排查时不要一上来就杀事务先确认实例的现有配置。在5.7中undo的布局主要由下面几个参数决定参数名默认值说明innodb_undo_tablespaces2undo独立的表空间文件数量innodb_undo_log_truncateOFF是否允许undo表空间自动截断innodb_max_undo_log_size10737418241GBundo表空间大小触发截断的阈值innodb_purge_threads4purge线程并发数我执行排查时的命令如下SHOW VARIABLES LIKE innodb_undo%; SHOW VARIABLES LIKE innodb_purge_threads;结果很意外innodb_undo_tablespaces已经是3说明这台实例创建时确实规划了独立undo表空间但innodb_undo_log_truncate还是OFF这意味着即使事务清空、purge线程把所有undo页都处理完文件的大小也不会自动缩回1GB。这是5.7默认配置里一个非常容易掉进去的坑你用了独立undo但没开自动截断。用操作系统命令再确认一下文件实际大小ls -lh /data/mysql/data/undo_*输出如下-rw-r----- 1 mysql mysql 12G Jul 21 10:15 /data/mysql/data/undo_001 -rw-r----- 1 mysql mysql 8.1G Jul 21 10:15 /data/mysql/data/undo_002 -rw-r----- 1 mysql mysql 18G Jul 21 10:15 /data/mysql/data/undo_003到这里基本可以确认undo文件确实异常膨胀且当前配置不允许自动收缩。接下来需要找到阻止purge正常工作的元凶事务。3.2 第二步揪出长期不结束的事务用下面的SQL可以直接查询当前活跃事务的清单SELECT trx_id, trx_state, trx_started, trx_mysql_thread_id, trx_query FROM information_schema.innodb_trx ORDER BY trx_started ASC;trx_started字段能直接告诉你每个事务是什么时候开启的。我执行后发现排在最前面的一个事务很夸张启动时间已经是3天前trx_id: 29205678 trx_state: RUNNING trx_started: 2024-07-18 09:23:11 trx_mysql_thread_id: 231 trx_query: NULL关键信息是trx_query为NULL的同时事务一直处于RUNNING状态这代表事务已经开启但没有正在执行的SQL。这种悬空事务是undo膨胀的最常见推手。它通常来源于以下几个场景应用代码里开启了事务但忘记在finally块里执行commit或rollback。数据库连接被线程池复用线程池没有正确重置事务状态。一个长事务调用了外部接口外部接口超时事务一直没有结束。长时间运行的SELECT查询需要快照这类在performance_schema.events_statements_history_long里可能能看到痕迹。我还顺手查了一下是否有大查询在跑SELECT id, user, host, db, command, time, state, info FROM information_schema.processlist WHERE command Sleep OR time 100;查询结果里确实有几个超过200秒的报表查询它们同样会持有旧版本快照但相比那个3天的悬空事务它们只是次要因素。3.3 第三步确认purge线程有没有在推进找到悬空事务之后我还需要判断purge线程是否有能力清理掉其他已完成事务的undo。继续看SHOW ENGINE INNODB STATUS重点看这个段落---TRANSACTION--- Trx id counter 25649201 Purge done for trxs n:o 25562000 undo n:o 0 state: running History list length 864257Purge done for trxs n:o 25562000这一行显示的是purge线程已处理到哪个事务编号。从整体来看purge一直在推进但推进速度跟不上新事务产生的速度。这里就形成一个恶性循环悬空事务让InnoDB必须保留从它开始时起的全部undo记录这部分记录体量巨大purge线程整天在处理它后面新产生的undo记录但处理速度永远追不上新增长。history list length越大实例的性能衰退得越厉害最终出现大量History list length过高导致的commit变慢、buffer pool被undo页占满等问题。3.4 第四步用perf和sys库辅助定位为了找到具体是哪个应用模块开启了这个悬空事务我用了performance_schema里的事务事件表SELECT THREAD_ID, EVENT_NAME, STATE, TRX_ID, TIMER_WAIT FROM performance_schema.events_transactions_current\G再关联线程表看这个事务对应的连接来源SELECT t.THREAD_ID, t.PROCESSLIST_ID, p.USER, p.HOST, p.DB, p.COMMAND, p.TIME FROM performance_schema.threads t JOIN information_schema.processlist p ON t.PROCESSLIST_ID p.ID;定位的结果是应用侧连接池里的一个僵死连接该连接在开启事务后因为业务调用的下游接口超时异常处理逻辑里漏掉了rollback于是事务就一直挂着。这种问题其实比SQL慢查询更隐蔽——你查慢日志、查processlist都很难一眼发现它因为它不占CPU、不占IO看起来只是睡着的连接但它对undo的拖累是实打实的。到这里我已经完成了问题定位。整个排查链路如果用一句话概括就是先看配置有没有全局兜底再看活跃事务谁在作祟最后确认purge线程状态和推进速度。三步走完动手处理的方向就非常明确了。4. 动手清理不同版本的实操方案对比4.1 明确版本差异5.7和8.0处理逻辑完全不同清理undo膨胀之前必须先确认当前MySQL版本。5.7和5.6类似undo自动截断需要显式开启且操作方式偏向手工8.0则把undo表空间自动truncate做成了默认能力还引入了独立的undo表空间回收机制。如果你在8.0上遇到undo文件膨胀绝大多数情况是长事务短时大量更新叠出来的处理重点是尽快结束长事务空间会自动回收。我这次处理的是5.7.29实例所以下面重点讲5.7的操作路径同时简单说说8.0的处理逻辑。4.2 MySQL 5.7手动调整开启自动截断并等待回收处理的第一步是终止那个已经挂了3天的悬空事务。在MySQL里可以通过KILL对应线程来强制结束事务KILL 231;这个操作在MySQL 5.7里是有可能直接让事务回滚的如果KILL之后事务还赖着不走可能需要重启相关应用节点让连接彻底断开。终止悬空事务后SHOW ENGINE INNODB STATUS里可以看到History list length开始回落说明purge线程正在消耗积压的undo记录。但这时候还有一个问题即使积压记录清空了undo文件本身的大小并不会自动下降因为innodb_undo_log_truncate还是OFF。这时候有两种路径路径一开启自动截断后继续观察。SET GLOBAL innodb_undo_log_truncate ON; SET GLOBAL innodb_max_undo_log_size 1073741824;设置完成后InnoDB会在purge线程把对应undo表空间里的记录清空后把该表空间临时标记为非活动状态然后将其truncate到初始大小。这个过程不是瞬时的可能需要几十分钟甚至更久取决于undo表空间里积压的数据量和purge速度。路径二直接重建undo表空间适用于自动截断持续不起效的情况。这个方案稍后会单独说。我这次是先用路径一配置了自动截断然后观察了大概1.5小时。history list length从86万降到了2万左右但文件大小变化不明显。这时候我判断可能是purge线程的清理速度还不够快又或者是undo文件内部碎片化严重截断一直没选中那个文件。于是我决定主动介入执行更彻底的手工重建流程。4.3 手工重建undo表空间更彻底的落地方案手工重建undo表空间的本质是让MySQL在干净的配置下重新创建一批undo文件然后把老的undo文件退役。具体流程如下第一步确认没有长事务和未提交事务。SELECT COUNT(*) FROM information_schema.innodb_trx;如果结果不是0先把它们全部处理掉。另外还需要关注主从复制的情况如果这台实例是从库要确保复制没有在跑大事务否则重建过程中可能出现复制中断。第二步修改配置文件增加undo自动截断相关的参数同时把undo表空间数量调整为一个新的初始值。我当时的做法是在my.cnf的[mysqld]段中加入innodb_undo_tablespaces3 innodb_undo_log_truncateON innodb_max_undo_log_size1G innodb_purge_threads4第三步干净关闭MySQL。mysqladmin -uroot -p shutdown确保mysqld进程完全退出ps -ef | grep mysqld第四步备份并移走旧的undo文件而不是直接删除。先用mv把三个旧文件挪到备份目录mv /data/mysql/data/undo_001 /backup/undo_001.bak.$(date %Y%m%d) mv /data/mysql/data/undo_002 /backup/undo_002.bak.$(date %Y%m%d) mv /data/mysql/data/undo_003 /backup/undo_003.bak.$(date %Y%m%d)第五步重启MySQL让InnoDB基于当前配置重新创建undo文件。systemctl start mysqld启动后确认新的undo文件是否生成了ls -lh /data/mysql/data/undo_*正常情况下会看到三个初始大小约10MB到100MB之间的undo文件并且随着业务写入逐渐增大但因为有自动截断兜底文件大小会在超过1GB后被截断回收。第六步确认旧文件确认没用了再删除。我建议至少观察一周确认实例稳定、无异常回滚需求后再清理备份目录里那个.bak文件。这套流程的本质就是换一批新的undo表空间让旧的、巨型化的undo文件彻底退役。有朋友问能不能不重启就完成至少在这次5.7里我没找到更优雅的在线方案除非你愿意冒in-place升级或加速purge的风险否则乖乖停库做才是稳妥的选择。4.4 MySQL 8.0的处理逻辑和注意点如果你的实例是8.0处理起来会轻松一些因为8.0里undo表空间的自动truncate默认是开启的。你不需要手工重建重点做三件事终止长期挂起的事务直接用information_schema.innodb_trx定位并KILL。确认undo表空间数量够用8.0默认innodb_undo_tablespaces2但官方其实建议至少3个以上这样truncate时才有空间可以轮换。观察一段时间等purge线程清理完所有可回收记录后undo文件会自动收缩到阈值以下。8.0如果遇到undo文件巨大且自动收缩迟迟不来还可以用ALTER UNDO TABLESPACE undo_001 SET INACTIVE将其置为非活动状态等purge结束后再设置为ACTIVE。但要注意这个命令需要innodb_undo_log_truncate开启并且不能作用在active的undo表空间上。这个操作相对激进慎用。4.5 清理完成后的验证方法我重建完成后专门蹲守了一段时间做验证。要判断undo是不是真的恢复正常了可以从三个维度看使用ls -lh检查undo文件大小应该稳定在1GB上下即使业务高峰短暂超过1GB之后也应该自动落回。执行SHOW ENGINE INNODB STATUS\G检查History list length健康实例这个值应该稳定在几百到几千。使用SHOW VARIABLES LIKE innodb_undo_log_truncate确认自动截断处于开启状态。另外我还关注了磁盘使用率的变化。清理前数据盘使用率93%清理后降到了70%左右效果立竿见影。这里补充一点如果清理完undo文件之后磁盘使用率依然很高那就要去排查binlog和binlog的过期时间设置或者ibtmp1临时表空间膨胀的问题这几个都是常规的消失的磁盘空间来源。5. 清理之后的防线配置优化、监控告警与业务规范5.1 从底层配置上给undo加一道保险经过这次事故我给这台实例以及后续所有新建实例都立了一条规矩undo的配置不能再按默认值走必须主动规划。核心配置项如下配置项本次推荐值理由innodb_undo_tablespaces3确保truncate时有足够的表空间轮换避免在线收缩卡死innodb_undo_log_truncateON开启undo表空间自动截断innodb_max_undo_log_size1G触发截断的阈值太小会导致频繁截断增加IO压力太大又浪费空间innodb_purge_threads4提升purge效率避免undo堆积max_binlog_size1G控制单个binlog大小便于排查时快速定位大日志另外undo文件和数据文件最好分盘存储。如果磁盘预算允许把innodb_undo_directory指向独立mount点或独立数据盘这样undo即使异常膨胀也不会拖垮主数据盘。这个优化我在部分业务量大的实例上已经落地了效果很明显至少故障影响面不会扩散到数据盘。5.2 把长事务和history list length加入监控比起事后清理更重要的手段是事前监控。我现在在监控系统里加了两个核心指标history list length实时值超过某个阈值比如5万就触发告警这个指标是undo堆积的第一信号。活跃事务中trx_started超过10分钟的事务数量一旦出现立即报警。实现方式很简单通过脚本定期执行下面这条SQL即可SELECT NOW() AS check_time, COUNT(*) AS long_running_trx, MIN(trx_started) AS min_started, MAX(TIMESTAMPDIFF(MINUTE, trx_started, NOW())) AS max_trx_minutes FROM information_schema.innodb_trx WHERE TIMESTAMPDIFF(MINUTE, trx_started, NOW()) 10;采集结果可以接prometheus或者简单的zabbix这个根据你的基础设施来定关键是把指标捞出来而不是等磁盘满了才去看。5.3 业务侧的预防规范这一条我觉得比技术配置更重要因为绝大多数undo膨胀的根源都在业务代码第一数据库连接池不要无限复用。很多连接池框架比如druid、HikariCP如果配置不当事务忘了关闭连接的隔离级别和事务状态没有正确复位就会产生悬空事务。我建议在连接池配置里增加transaction相关的检测至少保证每次获取连接时都检查trx_state。第二重事务拆分。如果一个事务里要更新的行数超过10万行或者事务内调用外部接口等待时间超过几秒这类事务都建议拆成小事务。不仅是undo膨胀的问题大事务还会引发锁等待、死锁、回滚慢等连锁反应。第三报表类的长查询尽量走从库。主库上的长SELECT会对undo快照的生命周期产生影响尤其和长事务叠加时会成倍延长undo记录存活时间。把这类查询分流是性价比很高的做法。这三个规范都不复杂难在落地。我建议你可以推动团队在代码评审阶段就加一条约束事务内不允许调用外部接口不允许出现select后长时间不commit的代码路径。真要这么执行之后undo相关的问题至少能少一半。6. 一些血泪教训与个人建议这次事故处理完之后我自己复盘了三个比较重要的点分享出来供你参考。第一MySQL的默认配置不是为长期运行的生产系统准备的而是为了能快速跑起来准备的。比如5.7默认的innodb_undo_log_truncateOFF如果在线业务有高并发写入又没有长事务你可能几个月都不觉得有异常但一旦出现一次故障级长事务undo文件立刻失控。所以在初始化数据库实例时最好一次性把这些参数规划到位不要等出了事再补救。第二KILL事务并不能立刻回收空间。我遇到很多人在KILL掉长事务之后看到磁盘没变化就以为修复无效其实是因为purge线程还在慢慢处理undo记录。这时候需要的不是再次暴力操作而是耐心观察一两个小时同时检查History list length是否在下降。如果连续观察两小时这个值完全不动才考虑purge线程是否被阻塞或者undo表空间数量配置是否不合理。第三动手之前先备份。这句老生常谈但在处理undo问题上特别重要。旧的undo文件虽然占空间但千万不要直接删先mv到备份目录确认新实例跑了几天没问题后再清理备份。万一新实例启动时数据和undo不完全匹配你还有后悔药可以吃。我们做运维的稳永远排在快前面。最后再分享一个小技巧如果你在执行SHOW ENGINE INNODB STATUS时看到History list length数值波动得非常快那说明实例的update/delete量很大、事务短小且频繁。这种情况下即使没有长事务undo文件也可能快速膨胀最优解是在应用层给这些高频写操作攒批降低单位时间内的版本链生成速度这比在数据库层调任何参数都更治本。这次的故障处理到这里就算彻底收尾了。你如果在实际环境里也遇到了类似的undo膨胀问题建议按照我这条链路走一遍先看配置再抓长事务然后耐心等待purge消耗最后确认自动截断机制已经接通。这套流程不敢说覆盖了所有情况但至少能帮你避免两个最常见的坑误删undo文件以及KILL完事务以为好了结果什么都不做。
返回列表