MySQL:MGR(单主)备节点MTS并发原理

2024-01-30  本文已影响0人  重庆八怪

最近遇到一个案例MGR备节点出现了hang死的问题,考虑到和备节点的并发有关,则学习了一下这部分。现记录如下。这里约定,

一、SQL线程的并发原则

这部分实际上不管是在MGR还是在主从中都是基于last commit进行并发的,在主从中我们可以通过设置参数,

然后从库接收到这些基于WRITESET生成的event后,SQL线程会判断last commit来判断并发度。但是在MGR中确要复杂一些了,因为MGR中在传输event的时候主库的gtid event根本就没有生成,更别说生成last commit了,它传输的event也不是简单的读取binlog文件,下面我们来看看MGR中的备节点节点如何生成last commit的。

二、MGR中last commit/seq number和GTID序号(gno)的生成方式

这里直接跳过了各种gcs和xcom处理,当做了一个黑盒。

那么这里我们需要重点关注的就是Certification_handler::handle_event中如何生成last commit和gno的。

  1. 首先Certification_handler::handle_event如果发现是Transaction_context_log_event,则进行缓存
  2. 如果Certification_handler::handle_event如果发现是Gtid_log_event,则调用Certification_handler::handle_transaction_id进行处理。

这个步骤首先调用Certifier::certify,其主要功能完成认证并且完成Gtid last commit和gno的生成,其中认证步骤为多主需要的这里不考虑。首先处理last commit,将其设置为冲突认证数据库中维护的最低值如下,

int64 transaction_last_committed = parallel_applier_last_committed_global

然后通过函数get_group_next_available_gtid分配关于group name的gno,这里Gtid信息就生成了,并生成snapshot_version,然后更新没有冲突的gtid信息。
接着就需要将这些writeset写入到冲突验证数据库,写入的时候同时完成last commit是否存在冲突的可能,也就是和Writeset_history完成功能一样,但是也有区别,因为这里的计算有60秒的时效性,后面在说。
如果在冲突验证数据库中不存在一样的值那么last commit就可以设置为parallel_applier_last_committed_global,否则是不能在备节点并发的,需要更新last commit,如下,

for (std::list<const char *>::iterator it = write_set->begin();
         it != write_set->end(); ++it) { //循环整个writeset
      int64 item_previous_sequence_number = -1;

      add_item(*it, snapshot_version_value, &item_previous_sequence_number);//逐行加入,并且获取存在冲突的上一行的seq number到item_previous_sequence_number

      /*
        Exclude previous sequence number that are smaller than global
        last committed and that are the current sequence number.
        transaction_last_committed is initialized with
        parallel_applier_last_committed_global on the beginning of
        this method.
      */
      if (item_previous_sequence_number > transaction_last_committed && //注意这里是大于才修改,也可能item_previous_sequence_number <  transaction_last_committed ,什么时候小于后面再说
          item_previous_sequence_number != parallel_applier_sequence_number)
        transaction_last_committed = item_previous_sequence_number; //根据加入冲突验证数据的结果设置last commit  }
  1. 备库提升parallel_applier_sequence_number计数器
    如果是备库,就需要提升这个计数器(Certifier::certify的末尾),用于后面计算Gtid event的seq number 如下,并且保存计算好的last commit和seq number ,后面会用到。
if (!local_transaction) {
...
    gle->last_committed = transaction_last_committed; //设置last commit
    gle->sequence_number = parallel_applier_sequence_number;//设置seq number
    increment_parallel_applier_sequence_number(
        !has_write_set || update_parallel_applier_last_committed_global); //增加seq number计数器
  1. 接下来就要根据是否为本地事务还是备库事务进行分别处理了,这部分依旧在Certification_handler::handle_transaction_id函数中

这一步中如果是本地事务(主库),则唤醒本地提交,在这之前生成gno信息会保存在一个事务上下文中,在正式事务提交的时候的会用到这个gno信息,如下,

        transaction_termination_ctx.m_generated_gtid = true;
        transaction_termination_ctx.m_sidno = group_sidno;
        transaction_termination_ctx.m_gno = seq_number; //设置了gtid 跳过了gtid 生成gno的步骤
 if (set_transaction_ctx(transaction_termination_ctx))
...

当提交的时候这部分信息会重用如下,

MYSQL_BIN_LOG::assign_automatic_gtids_to_flush_group:
 if (gtid_state->generate_automatic_gtid(
              head,
              head->get_transaction()->get_rpl_transaction_ctx()->get_sidno(),
              head->get_transaction()->get_rpl_transaction_ctx()->get_gno(),//MGR这里的gno已经生成
              &locked_sidno) != RETURN_STATUS_OK)

这里带入了MGR中生成的gno信息,因此主库提交的时候这个gno就是前面主库完成认证步骤后生成的GTID信息。但是last commit并没有保存,依旧是按照原有流程进行生成

如果是远端事务(备库),那么就简单了因为seq number和last commit都生成了那么只要通过它们进行Gtid event的构建就好了如下,

    /*
      Remote transaction.
    */
    if (seq_number > 0) {
      const rpl_sid *sid = nullptr;
      rpl_sidno sidno = group_sidno;
      rpl_gno gno = seq_number; //认证中获取的Gtid信息

      if (!tcle->is_gtid_specified()) {
        // Create new GTID event.
        Gtid gtid = {sidno, gno};
        Gtid_specification gtid_specification = {ASSIGNED_GTID, gtid};//定义gtid已经分配
        Gtid_log_event *gle_generated = new Gtid_log_event(
            gle->server_id, gle->is_using_trans_cache(), gle->last_committed,//认证中获取的last commit信息
            gle->sequence_number, gle->may_have_sbr_stmts, 
            gle->original_commit_timestamp, gle->immediate_commit_timestamp,
            gtid_specification, gle->original_server_version,
            gle->immediate_server_version);
  1. 接下备库就是执行next pipeline 写入到relay log,写入到relay log后applier通道的SQL线程就可以根据last commit进行并发了。

三、关于parallel_applier_last_committed_global的变更

这个变量直接影响到last commit的计算,前文已经提到。实际上这值的变更来自各个节点定期60秒,通过broadcast线程通过类型为Plugin_gcs_message::CT_CERTIFICATION_MESSAGE的消息,将各自节点已经执行的gtid 发送给其他节点。

    if (broadcast_counter % broadcast_gtid_executed_period == 0)//60秒
      broadcast_gtid_executed(); //广播GTID EXECUTED   CT_CERTIFICATION_MESSAGE 

当各个节点收到来自远端的Plugin_gcs_message::CT_CERTIFICATION_MESSAGE消息后(Certifier::handle_certifier_data),如果收集到全部节点的则会计算一个stable_gtid_set,也就是各个节点都执行过的事务集合,用于冲突验证数据的清理,Certifier::garbage_collect函数有如下:

  while (it != certification_info.end()) { //清理冲突验证数据库,得到信息是 全部节点都已经提交的GTID,通过广播线程 广播
    if (it->second->is_subset_not_equals(stable_gtid_set)) { //循环整个冲突认证数据库
      if (it->second->unlink() == 0) delete it->second;
      certification_info.erase(it++); //进行清理
    } else
      ++it;
  }

清理完成后会进行parallel_applier_last_committed_global的抬高,这个时候我们发现抬高视乎比较粗野,并没有考虑到冲突验证库,而是直接抬高到parallel_applier_sequence_number,前面我们说这个值就是gtid event中的gno,官方其实也知道因为有注释如下:

  /*
    We need to update parallel applier indexes since we do not know
    what write sets were purged, which may cause transactions
    last committed to be incorrectly computed.
  */
  increment_parallel_applier_sequence_number(true);

从注释来看这是没有考虑到writeset的清理的,但是需要注意这是粗野大抬高,其影响的只能是降低MGR备库的并发,而不是提高,因为也没什么关系。

四、总结

从这里的流程不难看出,MGR中的last commit和seq number以及gno在主库和备库是不同的,其类似主从中基于writeset生成的last commit,但是又有区别,总结如下,

  1. 冲突验证数据库除了多主(当然没用过多主)下验证事务冲突以外,在单主下60秒内还充当了验证并发冲突的角色。
  2. 冲突验证数据库每60秒就会进行一次冲突验证数据库的清理,清理主要根据的是各个节点都执行过的事务,然后执行清理。
  3. MGR的GTID和普通的主从有本质的区别,为各个节点自己生成。
  4. 主库的last commit和seq number完全基于原有的原则生成,也就是如果binlog_transaction_dependency_tracking设置为writeset则是writeset的方式,如果是commit order则是commit order的方式。
  5. 主库的gno(GTID序号) 则和备节点一样,在MGR内部完成认证的时候生成。
  6. 备库的last commit和seq number则是在认证的时候生成,而last commit比较复杂,主要包含
  1. 备点的gno(GTID序号)和主库一样,在MGR内部完成认证的时候生成。

五、测试

这部分测试我们主要测试60秒抬升备库last commit的事实。我们制造一些没有冲突的事务,然后不断的在主库插入,同时堵塞备库执行事务,则这个时候有如下:

那么我们理论上可以看到relay log中的gtid event last commit成片成片的更新。测试结果如下,截取交界处的部分:


image.png

这里貌似gtid event header的timestamp在relay log并不准确(图中10:22:58),稍微看了一下,relay log gtid event的head timestamp没有初始化,导致根据线程的时间来初始化,

time_t Log_event::get_time() {
  /* Not previously initialized */
  if (!common_header->when.tv_sec && !common_header->when.tv_usec) {
    THD *tmp_thd = thd ? thd : current_thd;
    if (tmp_thd)
      common_header->when = tmp_thd->start_time; //线程的启动时间

那么这个线程是applier线程因此早就启动了,所以不准。

上一篇下一篇

猜你喜欢

热点阅读