MySQL大事务的Recovery优化

你有没有碰到过mysqld进程启动了很长时间也起不来的情况?这时候我们可以用perf top命令查看一下MySQL进程主要在干什么事情。如果你查看到的信息如下图所示,启动过程中MySQL的主线程(mysqld_main函数开始的线程)绝大多数的时间都花在了回滚事务上。那么很可能是遇到了大事务回滚。

这种情况最常见的一个场景是一个大事务在写Binlog时把磁盘空间占满了,导致了实例的宕机重启。我曾经遇到的最大的Binlog文件超过了114GB。由于Binlog Cache的临时文件在写完Binlog后才被清理,所以这个事务总共占用了228GB的空间。MySQL的参数binlog_error_action用来控制写Binlog文件失败的行为,默认的配置是ABORT_SERVER,就是关闭进程。用户也可以配置这个参数为IGNORE_ERROR,意思是当写Binlog失败时关闭Binlog文件,后续的事务不再产生Binlog。这种情况显然会导致主备的数据不一致,因此除非不得以不要这样设置。

根因分析

为什么在MySQL进程启动时,主线程要做事务回滚的操作呢?这源自于Binlog的Crashsafe机制,详细的原理可以参考《MySQL的CrashSafe和Binlog的关系》,这里只做一个概括的介绍。事务的DML执行时会产生Binlog Events,当事务提交时这些Binlog Events会被写入到Binlog文件并持久化。为了保证MySQL宕机重启后数据和Binlog的一致性,MySQL设计了一个Crashsafe的机制。该机制对普通事务采用了两阶段提交(2PC),也称为内部 XA(Internal XA)。

如上图所示,在内部 XA 机制下一个事务的提交过程分为三个步骤:

  1. 存储引擎Prepare事务。事务状态由 ACTIVE 变为 PREPARED,并将事务的状态和XID持久化到Redo中。
  2. 事务会产生一个Xid_event,同DML的Binlog Events一同写入到binlog文件中,并持久化。
  3. 提交事务。

当异常宕机时,事务可能处于以下几种状态之一:

  • Active:在两阶段提交里,此类事务从未被写入 Binlog。
  • Prepared 但未写入 Binlog(或仅部分写入):事务已处于 Prepared 状态,但其 XID 未出现在 Binlog 文件中。
  • Prepared 且已写入 Binlog:事务已处于 Prepared 状态,且其 XID 已出现在 Binlog 文件中。
  • Committed:事务已经写入Binlog并且提交。

对于Committed的事务,设计上已经保证了它的Binlog Events一定写入了Binlog文件。因此Binlog和数据是一致的,启动时无需任何操作。对于Active的事务,Binlog Events肯定没有写入Binlog文件,InnoDB有一个后台回滚线程会自动将其回滚Prepared 的事务则需根据最后一个Binlog 文件中的XID信息进行处理。如果该事务的 XID 出现在了Binlog文件中,则需要提交该事务来保证Binlog和数据的一致性;反之则回滚该事务。处理Prepared 事务的过程称为 Binlog Recovery必须在MySQL向用户提供服务之前完成。事务提交通常很快,但回滚往往耗时与其执行时间相当。如果一个事务执行用了1小时,回滚很可能也需要 1小时,MySQL在此期间将不可用。

为什么必须要在提供服务前回滚所有事务呢?这和XID的实现有关系。XID是用MySQL前缀加上query_id构成的, query_id是一个全局的计数器,系统重启后会重新从1开始计数。如果在启动后,不对之前的Prepared事务进行提交或者回滚,那么就可能出现两个Prepared的事务有相同XID的情况。在恢复时,就无法区分哪个事务该提交,哪个事务该回滚。

异步回滚Prepared事务

AliSQL中,我们设计了一套异步回滚的机制来解决这个问题。

如上图所示,这个设计中将Prepared的事务回滚分为两个部分:

  1. 主线程将事务状态设为 Active 并持久化该状态。
  2. 利用InnoDB的后台回滚线程异步回滚事务的所有操作。

Binlog Recovery在完成第一部分后即可立即对外提供服务。由于第一步的执行非常快,Binlog Recovery可以在很短的时间内完成。

在宕机重启时,Active的事务会被InnoDB通过后台线程直接回滚掉,不需要XID来辅助决策。所以恢复时,只要将要回滚的事务的状态从Prepared改成Active就能避免两个Prepared事务有相同XID的问题。这里关键是要对Active的状态做持久化,保证在宕机重启后事务的状态仍然是Active,这样InnoDB就会自动将这个事务回滚掉。

社区版的InnoDB原本对Prepared事务的回滚就是先设置成Active状态,然后再根据Undo记录进行回滚。Active的状态会记录到Redo中,只是没有对Redo做持久化。然而InnoDB默认每秒会做一次Redo的持久化,所以在改成Active后,很快就会被持久化。因此当碰到了大事务回滚造成实例无法启动的情况时,即使是在社区版本,我们只要强制重启MySQL进程,大事务就会转变成后台回滚,不再阻塞实例的启动

这个功能的源码贡献给了MariaDB,已经合并到MariaDB-11.7中,详情参考MDEV-33853[1]。

结论

通过异步回滚的设计,在Binlog Recovery阶段只需要将Prepared的事务的状态设置为Active,真正耗时的事务回滚则由InnoDB的后台回滚线程异步的执行。通过这个优化,原本需要几十分钟甚至几个小时的启动过程,被缩短到秒级完成。

相关推荐
魔术师卡颂2 小时前
提问量暴跌 80% ,Stack Overflow 却赚翻了?
前端·后端·ai编程
FAFU_kyp3 小时前
Rust 字符串与切片
开发语言·后端·rust
Java水解3 小时前
Nginx 配置文件完全指南
后端·nginx
好想来前端3 小时前
私有化部署 LLM 时,别再用 Nginx 硬扛流式请求了 —— 推荐一个专为 vLLM/TGI 设计的高性能网关
后端·架构·github
OpenTiny社区3 小时前
TinyPro v1.4.0 正式发布:支持 Spring Boot、移动端适配、新增卡片列表和高级表单页面
java·前端·spring boot·后端·开源·opentiny
Java编程爱好者3 小时前
如何使用SpringAI来实现一个RAG应用系统
后端
明天有专业课3 小时前
穿搭式的设计模式-装饰者
后端
venton3 小时前
前端也能轻松上手:Express + MongoDB 搭建你的第一个后端服务
后端
a努力。3 小时前
中国电网Java面试被问:Dubbo的服务目录和路由链实现
java·开发语言·jvm·后端·面试·职场和发展·dubbo