查看原文
其他

MySQL 8.0新的GTID持久化线程和GTID恢复方式

老叶茶馆 2024-07-08

The following article is from MySQL学习 Author 高鹏

虽然线程本身很简单,但是涉及到purge线程,事务/UNDO等核心概念。

水平有限,仅供参考。


一、总体变化

我们这里说的GTID持久化线程,就是我们看到的如下:

| thread/innodb/clone_gtid_thread             |         6703 |

其实整个GTID持久化线程,依赖了数据结构Clone_persist_gtid,而它本也是全局变量Clone_Sys中的一个元素而已。我们后面看其中的重要元素,它在用户线程做innodb成提交的时候起到了作用,同时协调了GTID持久化线程和purge线程,是核心的数据结构。

总的说来就是下面的变化:

  • 用户线程提交事务将gtid写入到undo header,并且写入到gtid刷新链表中。
  • GTID持久化线程每100毫秒进行gtid flush链表的批量刷新到gtid_executed表中,且GTID持久化线程代替了GTID压缩线程的功能,进行GTID的压缩。
  • purge线程不允许清理未写入到gtid_executed表中事务的undo,为恢复提供基础。
  • Crash recovery的时候gtid内存值的恢复也会读取undo header,因此不再是5.7的仅仅依赖binlog和gtid executed表。

这样带来的最直观的表现是:

  • 5.7中binlog开启的情况下gtid_executed表示binlog切换更新
  • 8.0中binlog开启的情况下gtid_executed表是实时(准实时)更新

此外这一块和clone有着紧密的关系,也会后面学习clone plugin提供一个基础。

二、核心数据结构Clone_persist_gtid中的重要元素

元素解释
静态常量s_time_threshold_ms硬编码100毫秒,gtid 持久化线程的执行周期
静态常量s_compression_threshold硬编码50,这个50的单位是gtid持久化线程批量刷gtid_executed表的次数,通常binlog开启就是每50次gtid批量持久化后进行一次gtid_executed表压缩
静态常量s_gtid_threshold硬编码1024,单位是事务个数,如果用户提交线程发现积压的gtid事务有多于1024个没有写到gtid_executed表,会主动唤醒gtid持久化线程
静态常量s_max_gtid_threshold硬编码1024*1024,单位是事务个数,如果用户提交线程发现积压的gtid事务有多于1024*1024个没有写到gtid_executed表,会主动等待gitd持久化线程写gtid_executed表
m_gtids[2]gtid刷新链表,有2个,轮询使用,每批量刷一次gtid到gtid_executed表会切换一次,并且前一个链表清空
原子变量m_active_number单位是gtid刷新链表切换的次数,主要用确认该刷哪个链表了,代表准备刷入
原子变量m_flush_number单位是gtid刷新链表切换的次数,主要用确认该刷哪个链表了,代表刷链表完成
m_event用户线程唤醒gtid持久化线程的条件通知
m_compression_counter单位是gtid刷新链表切换的次数,每次切换会写入一批gtid,主要binlog开启的情况下判定是否需要压缩gtid_executed表
m_compression_gtid_counter单位是事务个数,也就是gtid的个数,主要用于binlog关闭的情况下,多少个gtid可以进行压缩一次和参数m_compression_gtid_counter进行对比
原子变量m_gtid_trx_no主要是供purge线程使用的,用户调整purge线程获取的oldest read view的判定下限,每次gtid持久化线程刷入gtid_executed表中过后会更新,因此保证没有持久化到gtid_executed表的gtid事务的undo不能清理
原子变量m_num_gtid_mem单位为gtid事务的个数,主要由用户提交事务线程增加,gtid持久化线程持久化后清0,用于用户提交线程判定是否需要唤醒或者等待gtid持久化线程
原子变量m_flush_in_progress为一个布尔值,每次gtid持久化线程进gtid刷入gtid_executed表工作的时候会将其设置为true,完成后设置为false

这里我们发现很多都是原子变量,这减少了线程间(这里主要是用户线程和gtid持久化线程)同步的复杂性,但是原子变量本也有一定的开销。

三、事务提交和GTID持久化的关系

这里我们主要考虑正常的事务,而不考虑外部XA事务,它们的处理还有不同。

对于正常的事务来讲, 首先要确认的是当前的undo segment是否为insert 类型的,因为insert类型的undo是在提交后就可以清理的,但是显然我们前面说过gtid的undo是不能随意清理的,需要gtid持久化线程的处理,因此需要一个update类型的undo segment,这样才能有不被保留undo segment的条件。

这个过程如下:

/* For gtid persistence we need update undo segment. */
      db_err = trx_undo_gtid_add_update_undo(trx, falsefalse); //分配UNDO 用于存储gtid
trx_undo_gtid_add_update_undo:
  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); //分配update类型undo
    mutex_exit(&trx->undo_mutex);
  }

当然分配了undo segment过后,也会将gtid写入到undo segment header的TRX_UNDO_FLAG_GTID中如下:

trx_undo_gtid_write:
  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); //写入GTID VERSION
    /* Persist fixed length gtid */
    ut_ad(TRX_UNDO_LOG_GTID_LEN == GTID_INFO_SIZE);
    mlog_write_string(undo_header + gtid_offset, &gtid_desc.m_info[0],
                      TRX_UNDO_LOG_GTID_LEN, mtr); //写入GTID 字符串到undo segment header中
    undo->flag |= gtid_flag;
  }

除此之外我们发现不光是gtid信息写入undo segment header中,binlog的position信息也会写入到如系统表空间ibdata的如下块中(write_binlog_position):

#define FSP_TRX_SYS_PAGE_NO \
  5 /*!< transaction        \
    system header, in       \
    tablespace 0 */

gtid写入到undo segment header中过后,接着就需要写入到Clone_persist_gtid的gtid 链表中了大概的函数调用如下:

trx_release_impl_and_expl_locks
 ->trx_erase_lists
   ->gtid_persistor.add

这里完成的任务包含了:

  • 没有写入到gtid_executed表的事务个数大于了s_gtid_threshold(1024)个,用户线程主动唤醒gtid持久化线程
  • 没有写入到gtid_executed表的事务个数大于了s_max_gtid_threshold(1024*1024)个,等待gtid持久化线程写入gtid到gtid_executed表
  • 将gtid写入到当前刷新gtid链表(m_gtids[2]中的一个),供gtid持久化线程使用

具体就不展开讨论了,当然事务的提交完成了很多任务,比如比较关键和这里有关系的,将事务的trx no写入到trx_sys的serialisation_list链表中,purge线程会通过oldest read view和TRX_RSEG_HISTORY(History List)的每个事务的trx no比较去决定哪些事务的undo 能够清理:

->trx_serialisation_number_get
             -> trx->no = trx_sys_get_new_trx_id();
                为事务分配trx no
             -> UT_LIST_ADD_LAST(trx_sys->serialisation_list, trx);
                加事务挂入trx_sys的序列化链表,待purge线程使用,和oldest read view比较
->trx_erase_lists
这里在完成了将事务undo移动到TRX_RSEG_HISTORY,并且将gtid挂载到flush gtid链表后从trx_sys->serialisation_list去掉。

当然为了获取oldest read view的下限,就需要和Clone_persist_gtid结构的m_gtid_trx_no再次比较,来更改下限。

而在后面我们会看到Clone_persist_gtid结构的m_gtid_trx_no就来自最老的trx_sys->serialisation_list中的值,因为在提交的最后会将事务gtid放到到gtid flush链表中,一旦gtid持久化线程完成gtid写入到gtid_executed表后,那么这批gtid就已经持久到gtid_executed表的,可以清理了,如果没有完成则不能这些事物的update undo segment。

还有比如insert类型的undo segment,事务提交就会清理掉也是在commit期间进行的。

四、gtid持久化线程的相关处理

整个线程的入口函数为:

->clone_gtid_thread
  ->Clone_persist_gtid::periodic_write

每100ms进行一次Clone_persist_gtid::flush_gtids的调用,我们主要来看这个函数的功能,因为这是核心函数。

首先第一步需要从trx_sys->serialisation_list中获取最老的trx no,也就是说拿到已经写入到gtid flush链表中的事务的trx no的最大值,那么小于这个值的都在我们的gtid flush链表中(这个问题困扰了我比较久,来回的翻了一阵逻辑),如下:

 ->oldest_trx_no = trx_sys_oldest_trx_no(); 
   获取最老的 trx no

接着如果Clone_persist_gtid结构的m_num_gtid_mem不为0,我们前面说过只要有事务的提交就会增加这个值,则需要进行gtid的持久化。

然后切换gtid flush链表因为我们前面说了这里有2个链表,并且将计数器m_num_gtid_mem清0,并且维护Clone_persist_gtid结构的m_compression_gtid_counter进行按事务的增加。

接下来就是写入gtid flush链表中的gtid值了,其中每次批量写入后还会进行Clone_persist_gtid结构的m_compression_counter的增加,而这个值是压缩判定的标准,实际上主要调用还是:

gtid_table_persistor->save(&write_gtid_set, false)
注意这里的false代表不进行,gtid 压缩线程的唤醒。

进行写入,完成后本gtid flush链表清空。

下来就是关键一步,前面从trx_sys->serialisation_list获取了最老的trx no,现在写入gtid_executed表完成了,就需要更新Clone_persist_gtid结构的m_gtid_trx_no,代表小于这个值的事务的gtid都持久化了,而前面你说过这个和purge线程定义oldest read view有关如下:

->MVCC::clone_oldest_view
  /* Update view to block purging transaction till gtid is persisted. */
  auto &gtid_persistor = clone_sys->get_gtid_persistor();
  auto gtid_oldest_trxno = gtid_persistor.get_oldest_trx_no();
  view->reduce_low_limit(gtid_oldest_trxno); //调整low_limit

调整了oldest read view,自然保证了undo的存在。

再然后就是判定是否进行压缩了,这里我们可以看到gtid持久化线程几乎代替了gtid压缩线程的功能,这也少了多线程并发控制的烦恼。压缩判定如下(Clone_persist_gtid::check_compress):

  • 如果没有开启binlog,和参数gtid_executed_compression_period有关,使用GTID个数进行判定,也就是m_compression_gtid_counter
  • 如果开启了binlog,和写入gtid_executed表的的刷新次数有关,也就是m_compression_counter

如果需要则进行gtid_executed表的压缩。

五、恢复读取undo中的gtid

这个部分就没有仔细的推敲,但是我们从如下栈中可以看到启动阶段获取了undo的gtid信息,如下:

#0  trx_undo_gtid_read_and_persist (undo_header=0x7fff425b0056 "") at /newdata/mysql-8.0.23/storage/innobase/trx/trx0undo.cc:666
#1  0x000000000524a7e0 in trx_rseg_persist_gtid (rseg=0x7fffe1e1e350, gtid_trx_no=3957552) at /newdata/mysql-8.0.23/storage/innobase/trx/trx0rseg.cc:212
#2  0x000000000524abed in trx_rseg_mem_create (id=16, space_id=4294967152, page_no=20, page_size=..., gtid_trx_no=3957552, purge_queue=0x7fffe1e160f0, mtr=0x7fffe7136590)
    at /newdata/mysql-8.0.23/storage/innobase/trx/trx0rseg.cc:264
#3  0x000000000524b289 in trx_rsegs_init (purge_queue=0x7fffe1e160f0) at /newdata/mysql-8.0.23/storage/innobase/trx/trx0rseg.cc:408
#4  0x000000000524ffbd in trx_sys_init_at_db_start () at /newdata/mysql-8.0.23/storage/innobase/trx/trx0sys.cc:429
#5  0x00000000051e1451 in srv_start (create_new_db=false) at /newdata/mysql-8.0.23/storage/innobase/srv/srv0start.cc:2682

也就是函数trx_undo_gtid_read_and_persist ,如果要深入学习可以以此为入口,实际上打开函数看看就能清晰的发现读取gtid这个事实。前面说过undo不清理为Crash recovery后获取到undo header中的gtid提供的了条件,这部分可以参考好友温正湖的文章:https://zhuanlan.zhihu.com/p/141403577

其他参考:

  • https://zhuanlan.zhihu.com/p/141403577

  • https://new.qq.com/rain/a/20211109a01amn00

  • https://www.modb.pro/db/447883


以上!


MGR

B

https://www.bilibili.com/medialist/play/1363850082?business=space_collection&business_id=343928&desc=0



文章推荐:



想看更多技术好文,点个“在看”吧!

继续滑动看下一个
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存