近在调试MySQL新功能发现MySQL 8.0相比5.7版本在mysqld crash recovery上有较大不同点,有必要记录下。主要包括事务gtid持久化到mysql.gtid_executed方式和InnoDB在其中发挥的作用。并延伸分析未来MySQL版本对InnoDB的定位。
MySQL 5.7故障恢复逻辑
关于这块实现,网上有很多文章,这里不再展开,结合下图直接说主逻辑:
1. MySQL扫描后一个Binlog文件file3,搜集其中的每个事务xid集合,集合中的事务均已提交; 2. InnoDB存储引擎通过redo和undo进行recovery,基于上一步传入的xid集合来决定处于prepare状态的事务应该commit还是rollback,r1中的事务commit,r2中的事务rollback; 3. 若开启gtid模式,file1和file2中的事务gtid均已写入gtid_executed表,MySQL层会扫描file3事务gtid信息,将gtid 201~280插入表中;
这里补充说明下两点: - 在步骤1中,除了收集xid集合,还会检查binlog event的完整性,如果有损坏的event,那么 会将Binlog文件大小截取/truncate到个有损坏的binlog event前; - 针对步骤3,有同学可能会问,为什么不在步骤1就收集gtid加入mysql.gtid_executed?因为那是InnoDB还没有完成数据恢复,对应的InnoDB表是不可用的;而且相比Binlog和InnoDB故障恢复,gtid系统的初始化并不是非常紧迫,因此放后执行。
MySQL 8.0开发时遇到的问题
在实现某个新功能时,需要将后一个Binlog文件truncate掉指定位置,该位置相比其有效位置会小一些。 调试时发现,虽然成功做了truncate,但终初始化出来的gtid_executed还是包含了已被truncate的那部分事务gtid。
按照MySQL 5.7的逻辑,gtid_executed的初始化是先获取mysql.gtid_executed表中的gtid集合,再逐个将后一个Binlog文件中的gtid加入到集合中。为什么还会出现已被truncate的gtid呢?
MySQL 8.0的gtid_executed表更新逻辑
背景介绍
在之前文章中,已经陆续介绍了一些MySQL 8.0的新特性,其中之一就是加入了InnoDB Clone/克隆功能,可通过点击链接详细了解。该功能需要记录克隆时数据对应的Binlog点位信息,xtrabackup等工具是通过加备份锁或FTWRL锁来获取。而克隆功能则是另起炉灶,直接改变了事务gtid的存储方式。
具体是怎么样的方式呢?WL#9211: InnoDB: Clone Replication Coordinates这个worklog进行了具体说明,简单来说,就是事务gtid会写入InnoDB的undo日志中。 由于mysql.gtid_executed表也是使用InnoDB存储引擎,这样就可以在克隆时仅拷贝InnoDB存储引擎层数据,再通过InnoDB recovery来恢复出对应的gtid_executed,进而再建立跟复制源的Binlog复制。
mysql.gtid_executed更新策略
undo日志对应事务在事务提交后,且不再有其他事务可能读取这些undo,其会被后台线程purge掉,其记录的事务gtid也就一并丢失。由于undo purge线程在事务提交后随时可能被触发,那么如果mysql.gtid_executed还是在Binlog文件rotate时才更新,就可能导致文件前一部分的gtid丢失。
为了解决这个问题,InnoDB层建立了gtid_executed表gtid更新与undo purge的协调关系,只有对应事务gtid已经写入gtid_executed,undo才能被purge。具体实现时维护一个InnoDB后台线程clone_gtid_thread,周期性持久化事务gtid到gtid_executed表,purge线程仅回收gtid已经被写入gtid_executed表的事务undo日志。
// clone0repl.cc
/** Persist GTID to on disk table from time to time.
@param[in,out] persist_gtid GTID persister */
static void clone_gtid_thread(Clone_persist_gtid *persist_gtid) {
persist_gtid->periodic_write();
}
上述即为由后台线程发起gtid_executed周期性持久化的代码逻辑。触发更新的条件如下:
if (!flush_immediate()) {
os_event_wait_time(m_event, s_time_threshold_ms * 1000);
}
os_event_reset(m_event);
/* Write accumulated GTIDs to disk table */
flush_gtids(thd);
/* Wake up background if GTIDs crossed threshold. */
if (current_value == s_gtid_threshold) {
os_event_set(m_event);
}
}
从上面2个代码片段可知,超过s_time_threshold_ms 毫秒,或者m_event被置位/set(被置位的场景有多个,其中一个为超过s_gtid_threshold)均会触发。再来看看这两个参数的说明:
/** Persist GTID along with transaction commit */
class Clone_persist_gtid {
...
private:
/** Time threshold to trigger persisting GTID. Insert GTID once per 1k
transactions or every 100 millisecond. */
const static uint32_t s_time_threshold_ms = 100;
...
/** Number of transaction/GTID threshold for writing to disk table. */
const static int s_gtid_threshold = 1024;
也就是说,每隔100毫秒,或者gtid累计1024个就会触发写gtid_executed表操作。显然,这个频度可以理解为准实时了。目前这两个变量处于硬编码状态,未暴露出来,用户无法设置。
基于undo恢复gtid_executed表
未被clone_gtid_thread后台线程周期性持久化的那部分事务gtid,由于其对应的undo日志不会被purged掉,所以在InnoDB recovery时,会扫描undo日志抽取出gtid并写入gtid_executed表。
如上图,file3中前一部分gtid 201~250在mysqld crash前已经写入gtid_executed表。剩余部分251~280从undo日志中写入,处理函数为trx_undo_gtid_read_and_persist,对应调用逻辑为:
显然,了解上面这些信息,就明白为什么我们truncate了后一个Binlog文件部分event后,这些event中的事务的gtid还会在gtid_executed表出现。
InnoDB和Binlog角色变化
MySQL 8.0.17版本改动
前面所述,MySQL 8.0.17将事务gtid写入InnoDB的undo日志中。是不是意味着,即使sync_binlog不为1,只要innodb_flush_log_at_trx_commit为1,除了可以保证服务器crash后事务数据不丢失,而且数据对应的gtid_executed也可以通过恢复后的mysql.gtid_executed表获得。因此,能够顺利地在crash之后连上其他MySQL节点建立复制关系。这样的话,在MySQL高可用部署下,从库可以无需将sync_binlog设置为1,甚至直接关闭log_slave_update不记录Binlog日志。
对于前一种场景,可能导致服务器crash后部分Binlog event丢失,若其他节点从该节点拉取Binlog,会导致数据丢失和复制错误,为了解决这个问题,可以先将Binlog都purge掉来预防。
MySQL 8.0.19版本改动
对于后一种情况,由于MySQL层的组提交是基于Binlog的,若要保证从库回放时事务提交顺序,需要依赖Binlog组提交,不开启Binlog的话只能进行单线程回放,会出现很大性能损失。显然MySQL官方团队也意识到该问题,并在MySQL 8.0.19版本进行了优化。详见worklog WL#7846: MTS: slave-preserve-commit-order when log-slave-updates/binlog is disabled。官方测试表明性能相比开启Binlog有大幅提升。
其中write-only性能提升20%,insert性能提升50%以上。
无责任YY
其实到了MySQL 8.0肉眼可见的变化是InnoDB存储引擎的重要性在增强,比如数据字典模块和原子DDL。进一步根据17和19版本这些信息,是否可以推断出MySQL中Binlog功能在弱化?新MySQL版本只有主从复制强依赖于Binlog,其他场景下无需保证在事务提交时的Binlog持久化。年初曾听到一些说法,MySQL官方会做基于Redo的物理复制,似乎跟目前的变化趋势有点吻合。如果是这样,那么未来Binlog文件可能仅用于进行第三方的异构复制了,也就是通过Binlog文件来维持MySQL与其他数据产品的生态。
总结
本文通过在MySQL 8.0内核开发过程中遇到的故障恢复相关问题,展开说明了MySQL 5.7和8.0版本在故障恢复时的不同处理方法,介绍了Binlog和InnoDB在其中所起的作用变化。并推测了后续MySQL官方可能的改动方向。