在之前的公众号文章《GTID实践和分析》中介绍了GTID的基本原理,MySQL主要通过Server引擎的binlog文件和Innodb的mysql.gtid_executed表来持久化GTID集合信息。在提交时会将分配给事务的GTID刷到binlog文件中,在事务成功提交后会将GTID加入内存的executed_gtids集合中,并周期性持久化到mysql.gtid_executed表中。在实例恢复时可以从mysql.gtid_executed表+最后一个binlog文件中的GTID信息来恢复executed_gtids集合,从而保证GTID的完整性。
在MySQL 8.0.17开始引入基于Innodb引擎的物理备份插件clone plugin,和xtrabackup不同,它在克隆时不会复制Server层的binlog文件,而mysql.gtid_executed表的数据并不是实时的,因此需要在Innodb层维护完整的GTID信息。引入clone plugin后一个明显的变化是事务提交时会将GTID信息记录在undo log header,并新增一个 Clone_persist_gtid 对象来维护Innodb层GTID的持久化。本文将从"分配Undo log segment"、"在Undo log header记录GTID信息"、"GTID的持久化"和"Purge Undo log"这几个关键环节代码对GTID的维护进行介绍。
2.1 分配Undo log segment
在事务提交前会先为事务分配相应的Undo log segment以便于事务回滚,update类型的事务会分配Update Undo log,在事务提交后等待后台purge线程回收释放,而insert类型事务会分配Insert Undo log,由于在事务提交后不会有其他事务会依赖这些刚插入的数据所以可以被直接回收释放。在引入clone plugin后,事务在提交时也会将GTID信息记录到Undo log header中。
我们可以看到事务在提交前调用了 trx_undo_gtid_add_update_undo 函数检查是否已为事务分配Undo log用于后续记录GTID信息,而如果是insert_only类型的事务,则会专门分配Update Undo segment。
dberr_t trx_undo_gtid_add_update_undo(trx_t *trx, bool prepare, bool rollback) { // ... if (undo_ptr->is_insert_only() || gtid_explicit) { ut_ad(!rollback); mutex_enter(&trx->undo_mutex); db_err = trx_undo_assign_undo(trx, undo_ptr, TRX_UNDO_UPDATE); mutex_exit(&trx->undo_mutex); } // ... }
为什么需要专门给insert_only类型的事务分配Update Undo log? 我们知道Insert Undo segment在事务提交后可能随时被释放,而GTID刷表并不是实时的,虽然事务提交时也会将GTID信息记录到gtid_persistor的Active list中(后续会介绍),但它是在内存维护的对象在没有刷表前并不能保证安全,因此仍需确保GTID尚未持久化的insert_only类型事务的GTID信息在存储层持久化。目前的做法是将提交事务的GTID信息记录到Update Undo segment中,并在purge时确保相应事务的GTID已刷表才进行purge来保证GTID完整性。
2.2 在Undo log header记录GTID信息
在给事务分配Update Undo Segment后,会调用 gtid_persistor.get_gtid_info 函数从trx中提取GTID信息,并在后续记录到Undo log header。
void trx_undo_gtid_write(trx_t *trx, trx_ulogf_t *undo_header, trx_undo_t *undo, mtr_t *mtr, bool is_xa_prepare) { // ...... std::tie(gtid_flag, gtid_offset) = undo->gtid_get_details(is_xa_prepare); // ......
gtid_persistor.get_gtid_info(trx, gtid_desc); // 从trx->mysql_thd->owned_gtid 和 owned_sid提取GTID信息
if (gtid_desc.m_is_set) { /* Persist GTID version */ mlog_write_ulint(undo_header + TRX_UNDO_LOG_GTID_VERSION, gtid_desc.m_version, MLOG_1BYTE, mtr); /* Persist fixed length GTID */ ut_ad(TRX_UNDO_LOG_GTID_LEN == GTID_INFO_SIZE); mlog_write_string(undo_header + gtid_offset, >id_desc.m_info[0], TRX_UNDO_LOG_GTID_LEN, mtr); undo->flag |= gtid_flag; } mlog_write_ulint(undo_header + TRX_UNDO_FLAGS, undo->flag, MLOG_1BYTE, mtr);}
至于记录到Undo log header的哪里,是通过 gtid_get_details(is_xa_prepare) 来决定的,具体可以看 Undo Segment header 的字段定义:
|----gtid version-(1) ---|---------gtid--(64)---------|--------gtid xa--(64)---------|
在将GTID信息记录到Undo log后,为了保证purge线程可以在事务成功提交后及时回收相应Undo log,还需要及时将这些GTID信息持久化到表。
2.3 GTID的持久化
在事务成功提交后,会调用 gtid_persistor.get_gtid_info 从trx中获取GTID信息并写入gtid_persistor的Active list,gtid_persistor会负责周期性将list上积累的GTID信息刷到mysql.gtid_executed表。
gtid_persistor是 Clone_persist_gtid 对象,它负责在事务提交后持久化GTID信息。它维护了两个Gitd_info_list,当事务提交后会将GTID信息记录到其中一个list上,此时它称为Active list;当积累了一定数量的GTID后需要持久化到表,这时会将其转为flush list进行刷盘,此时新事务的GTID信息会写到另一个list(新Active list)上。两种list是通过序号 m_active_number 和 m_flush_number 来维护和动态切换的。
Clone_persist_gtid 还定义了一些变量用来控制GTID刷盘和压缩GTID的条件,默认情况下gtid_persistor每100ms或者累计达到1024个GTID会进行一次GTID刷表。
s_time_threshold_ms // 100ms,触发持久化GTID的时间间隔 s_compression_threshold // 50,触发压缩GTID的阈值 s_gtid_threshold // 1024,写入磁盘表的事务数/GTID数阈值 s_max_gtid_threshold // 1024 * 1024,持有最大的事务/GTID数
持久化时从gtid_persistor相应的list中取出所有GTID信息,再调用 gtid_table_persistor->save() 将GTID信息刷到mysql.gtid_executed表。
int Clone_persist_gtid::write_to_table(uint64_t flush_list_number, Gtid_set &table_gtid_set,Sid_map &sid_map) { Gtid_set write_gtid_set(&sid_map, nullptr); auto &flush_list = get_list(flush_list_number); /* Extract GTIDs from flush list. */ for (auto >id_info : flush_list) { auto gtid_str = reinterpret_cast<const char *>(>id_info[0]); // 获取flush list并提取所有GTID信息 auto status = write_gtid_set.add_gtid_text(gtid_str); }
/* Write GTIDs to table. */ if (!write_gtid_set.is_empty()) { ++m_compression_counter; err = gtid_table_persistor->save(&write_gtid_set, false); // 将所有GTID信息写到表 }
/* Clear flush list and return */ flush_list.clear(); ut_ad((m_flush_number + 1) == flush_list_number); m_flush_number.store(flush_list_number); return (err);}
gtid_persistor->m_gtid_trx_no字段用来维护还未提交事务的最老的事务trx_no。在gtid_persistor完成刷表后,会从全局事务管理器中获取最老未提交事务的trx_no并更新该字段,从而purge线程就知道哪些事务的Undo log可以purge,哪些还需要等待gtid_persistor刷盘了。
void Clone_persist_gtid::flush_gtids(THD *thd) { // .... /* Update trx number upto which GTID is written to table. */ update_gtid_trx_no(oldest_trx_no); // ...}
2.4 Purge Undo log
purge线程在开始处理之前会调用 clone_oldest_view 函数来判断哪些Undo log需要处理,在该函数中会从gtid_persistor中获取最老未持久化事务的trx_no,并更新readview中的m_low_limit_no为min(m_low_limit_no, oldest_trx_no),从而保证GTID还未刷表的事务Undo log不会被purge。
void MVCC::clone_oldest_view(ReadView *view) { // ... /* Update view to block purging transaction till GTID is persisted. */ auto >id_persistor = clone_sys->get_gtid_persistor(); auto gtid_oldest_trxno = gtid_persistor.get_oldest_trx_no(); view->reduce_low_limit(gtid_oldest_trxno);}
本文简单介绍了自MySQL8.0引入clone plugin之后GTID在Innodb层的维护流程,从"分配Undo log segment"、"在Undo log header记录GTID信息"、"GTID的持久化"和"Purge Undo log"这4个方面选取了关键的流程和代码进行简单介绍,在对GTID的处理上仍有许多细节需要注意,感兴趣的读者可以自行选取相应代码进行更深入学习。
腾讯数据库研发部数据库技术团队对内支持微信支付、微信红包、腾讯广告、腾讯音乐等公司自研业务,对外在腾讯云上支持 TencentDB 相关产品,如 CynosDB、CDB、TDSQL等。本公众号旨在推广和分享数据库领域专业知识,与广大数据库技术爱好者共同成长。