MySQL semisync replication配置及源代码实现


之前一直没有对semi sync做太多的关注,原因是线上的生产环境使用的非常少,最近需要提升semisync的生产环境优先级,以适应数据保护非常严格的场景,借此机会了解一下semisync,顺便过了一下大部分代码,以帮助后续的性能优化工作,以下分析基于代码MySQL5.6.13

 

如何配置

 

semisync的配置非常简单,采用MySQL Plugin的方式,在主库和备库上分别安装不同的插件,当然你也可以主库备库全部标准安装上,使用参数来控制sesmisync;

 

 

INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so';
INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so';

 

正常情况下,这两个PLUGIN都应该被标准安装,谁知道哪天备库会不会被切换成主库呢。

安装完PLUGIN后,我们可以根据拓扑结构来定义主库和备库的配置,主要包括以下几个配置项;

1.rpl_semi_sync_master_enabled  
  —控制主库上是否开启semisync, 打开或关闭,立刻生效

2.rpl_semi_sync_slave_enabled     
  —控制备库是否开启semisync,

当主库打开semisync时,则必须至少要有一个链接的备库是打开semisync的,否则主库线程每次都会去等待,直至超时;因此如果想关闭semisync必须要先关闭主库配置,再关闭备库配置
3.rpl_semi_sync_master_timeout    
  —控制主库上客户端的等待时间,当超过这么长时间等待后,客户端返回,同步复制退化成原生的异步复制
  单位为毫秒,默认值为10000,即10秒  

默认打开,表示当备库起来后,并跟上主库时,自动切换到同步模式,如果关闭,即使备库起来并跟上了,也不会启用半同步;

— 输出监控信息的级别,详细点击见文档,不同的级别,可能输出更详细的信息,用于DEBUG

运行状态变量也比较丰富,不细说了,网上介绍的很多,官方文档也很详细
root@(none) 02:58:27>show status like '%semi%';
+--------------------------------------------+-------+
| Variable_name                              | Value |
+--------------------------------------------+-------+
| Rpl_semi_sync_master_clients               | 0     |
| Rpl_semi_sync_master_net_avg_wait_time     | 0     |
| Rpl_semi_sync_master_net_wait_time         | 0     |
| Rpl_semi_sync_master_net_waits             | 0     |
| Rpl_semi_sync_master_no_times              | 0     |
| Rpl_semi_sync_master_no_tx                 | 0     |
| Rpl_semi_sync_master_status                | OFF   |
| Rpl_semi_sync_master_timefunc_failures     | 0     |
| Rpl_semi_sync_master_tx_avg_wait_time      | 0     |
| Rpl_semi_sync_master_tx_wait_time          | 0     |
| Rpl_semi_sync_master_tx_waits              | 0     |
| Rpl_semi_sync_master_wait_pos_backtraverse | 0     |
| Rpl_semi_sync_master_wait_sessions         | 0     |
| Rpl_semi_sync_master_yes_tx                | 0     |
| Rpl_semi_sync_slave_status                 | OFF   |
+--------------------------------------------+-------+
15 rows in set (0.00 sec)

源代码实现

采用plugin +代码HOOK的方式实现,通过HOOK来回调在plugin中定义的函数
例如:
RUN_HOOK(transaction, after_commit, (head, all));

RUN_HOOK的定义在rpl_hander.h中:
#define RUN_HOOK(group, hook, args)             \
  (group ##_delegate->is_empty() ?              \
   0 : group ##_delegate->hook args)
因此上例被转化成
transaction_delegate->after_commit(head, all);

更具体的回调接口函数在sql/rpl_hander.cc文件中定义

有四类_delegate对象
binlog_storage_delegate、transaction_delegate、binlog_transmit_delegate、binlog_relay_io_delegate

主库

在主库semisync加载或初始化时,调用函数semi_sync_master_plugin_init,为transaction_delegate,binlog_transmit_delegate和binlog_transmit_delegate增加了observer,分别对应该plugin的变量为trans_observer,storage_observer,transmit_observer,这三个obeserver定义了各自的函数接口,如下:

Trans_observer trans_observer = {
  sizeof(Trans_observer),               // len

  repl_semi_report_commit,      // after_commit
  repl_semi_report_rollback,    // after_rollback
};

Binlog_storage_observer storage_observer = {
  sizeof(Binlog_storage_observer), // len

  repl_semi_report_binlog_update, // report_update
};

Binlog_transmit_observer transmit_observer = {
  sizeof(Binlog_transmit_observer), // len

  repl_semi_binlog_dump_start,  // start
  repl_semi_binlog_dump_end,    // stop
  repl_semi_reserve_header,     // reserve_header
  repl_semi_before_send_event,  // before_send_event
  repl_semi_after_send_event,   // after_send_event
  repl_semi_reset_master,       // reset
};

 

所有从server层向plugin的函数调用,都是通过函数指针来完成的,因此我们只需要搞清楚上述几个函数的调用逻辑,基本就可以清楚semisync plugin在MySQL里的处理逻辑

a.备库连接到主库时
调用函数repl_semi_binlog_dump_start
HOOK注入点:
mysql_binlog_send:

912   if (log_warnings > 1)
913     sql_print_information("Start binlog_dump to master_thread_id(%lu) slave_server(%d), pos(%s, %lu)",
914                         thd->thread_id, thd->server_id, log_ident, (ulong)pos);
915   if (RUN_HOOK(binlog_transmit, transmit_start, (thd, 0/*flags*/, log_ident, pos)))
916   {
917     errmsg= "Failed to run hook 'transmit_start'";
918     my_errno= ER_UNKNOWN_ERROR;
919     GOTO_ERR;
920   }

 

mysql_binlog_send用于每一个binlog dump请求,在开始dump之前,调用repl_semi_binlog_dump_start,该函数主要做以下几件事情:
1.首先判断连接过来的备库是否是开启semisync的,通过备库用户是否设置了变量rpl_semi_sync_slave来决定(ReplSemiSyncMaster::is_semi_sync_slave())

2.如果备库开启了semisync,增加连接的备库计数(repl_semisync.add_slave()),计数器用变量rpl_semi_sync_master_clients来维持,LOCK_binlog_锁来保护

3.
repl_semisync.reportReplyBinlog(param->server_id, log_file, log_pos); 后面介绍

b.当备库从主库上断开时,会调用repl_semi_binlog_dump_end
将计数器rpl_semi_sync_master_clients减1

c.我们以执行一条DML为例:

执行一条简单的SQL:insert into t1 values (1);

1.当将binlog写入到文件中后(尚未sync),调用repl_semisync.writeTranxInBinlog

HOOK位置(sql/binlog.cc):
6699   /*
6700     If the flush finished successfully, we can call the after_flush
6701     hook. Being invoked here, we have the guarantee that the hook is
6702     executed before the before/after_send_hooks on the dump thread
6703     preventing race conditions among these plug-ins.
6704   */
6705   if (flush_error == 0)
6706   {
6707     const char *file_name_ptr= log_file_name + dirname_length(log_file_name);
6708     DBUG_ASSERT(flush_end_pos != 0);
6709     if (RUN_HOOK(binlog_storage, after_flush,
6710                  (thd, file_name_ptr, flush_end_pos)))
6711     {
6712       sql_print_error("Failed to run 'after_flush' hooks");
6713       flush_error= ER_ERROR_ON_WRITE;
6714     }
6715
6716     signal_update();
6717     DBUG_EXECUTE_IF("crash_commit_after_log", DBUG_SUICIDE(););
6718   }
repl_semisync.writeTranxInBinlog会进一步的调用repl_semisync.writeTranxInBinlog(如果主库的semisync打开的话)来存储当前的binlog文件名和偏移量, 更新当前最大的事务binlog位置,存储在repl_semisync对象的commit_file_name_和commit_file_pos_中(commit_file_name_inited_被设置为TRUE);然后将该事务的位点信息存储到active_tranxs中(active_tranxs_->insert_tranx_node(log_file_name, log_file_pos)),这是一个链表,用来存储所有活跃的事务的位点信息,每个新加的节点都能保证位点在已有节点之后;另外还维持了一个key->value的数组,数组下标即为事务binlog坐标计算的hash,值为相同hash值的链表
这些操作都在锁LOCK_binlog_的保护下进行的, 即使semisync退化成同步状态,也会继续更新位点(但不写事务节点),主要是为了监控后续SLAVE时候能够跟上当前的事务Bilog状态;

事实上,有两个变量来控制SEMISYNC是否开启:
state_  为true表示同步,为false表示异步状态,semi sync退化时会修改改变量(ReplSemiSyncMaster::switch_off)

另外一个变量是master_enabled_,这个是由参数rpl_semi_sync_master_enabled来控制的。 

2.在事务commit之后,调用函数repl_semi_report_commit
HOOK位置 (sql/binlog.cc)
6388 void
6389 MYSQL_BIN_LOG::process_after_commit_stage_queue(THD *thd, THD *first)
6390 {
6391   Thread_excursion excursion(thd);
6392   for (THD *head= first; head; head= head->next_to_commit)
6393   {
6394     if (head->transaction.flags.run_hooks &&
6395         head->commit_error == THD::CE_NONE)
6396     {
6397       if (excursion.attach_to(head))
6398       {
6399         head->commit_error= THD::CE_COMMIT_ERROR;
6400         sql_print_error("Out of memory while attaching to session thread "
6401                         "during the group commit phase.");
6402       }
6403       if (head->commit_error == THD::CE_NONE)
6404       {
6405         bool all= head->transaction.flags.real_commit;
6406         (void) RUN_HOOK(transaction, after_commit, (head, all));
6407         /*
6408           When after_commit finished for the transaction, clear the run_hooks flag.
6409           This allow other parts of the system to check if after_commit was called.
6410         */
6411         head->transaction.flags.run_hooks= false;
6412       }
6413     }
6414   }
6415 }
process_after_commit_stage_queue在GROUP COMMIT的第三个阶段,即COMMIT阶段,leader线程来调用,依次为其他线程调用repl_semi_report_commit,进一步的,调用repl_semisync.commitTrx(binlog_name, param->log_pos)

ReplSemiSyncMaster::commitTrx的参数是事务的binlog坐标, 该函数是实现客户端同步等待的主要部分,主要做以下事情:
1)用户线程进入新的状态:

”Waiting for semi-sync ACK from slave“ 
从SHOW PROCESSLIST的输出可以看到如上信息

2)检查当前的semisync状态:

    /* This is the real check inside the mutex. */

    if (!getMasterEnabled() || !is_on())
      goto l_end;

这是在持有锁的状态下进行的检查;

3)根据wait_timeout_ 设置超时时间变量

随后进入while循环,
while (is_on())
{
……

只要semisync没有退化到异步状态,就会一直在while循环中等待,直到超时或者获得备库反馈;

while循环内的工作包括:

(1)判断:
当reply_file_name_inited_为true时,如果reply_file_name_及reply_file_pos_大于当前事务等待的位置,表示备库已经收到了比当前位置更后的事务,这时候无需等待,直接返回;

当wait_file_name_inited_为true时,比较当前事务位置和(wait_file_name_,wait_file_pos_)的大小,如果当期事务更小,则将wait_file_pos_和wait_file_name_设置为当前事务的值;
否则,若wait_file_name_inited_为false,将wait_file_name_inited_设置为TRUE,同样将上述两个变量设置为当前事务的值;
这么做的目的是为了维持当前需要等待的最小binlog位置

(2)增加等待线程计数rpl_semi_sync_master_wait_sessions++
wait_result = cond_timewait(&abstime);  线程进入condition wait
在唤醒或超时后rpl_semi_sync_master_wait_sessions–

如果是等待超时的,rpl_semi_sync_master_wait_timeouts++,并关闭semisync (switch_off(),将state_/wait_file_name_inited_/reply_file_name_inited_设置为false,rpl_semi_sync_master_off_times++,同时唤醒其他等待的线程(COND_binlog_send_))

如果是被唤醒的,则增加计数:rpl_semi_sync_master_trx_wait_num++、rpl_semi_sync_master_trx_wait_time += wait_time, 然后回到1),去检查相关变量;

4).退出循环后,更新计数

    /* Update the status counter. */
    if (is_on())
      rpl_semi_sync_master_yes_transactions++;
    else
      rpl_semi_sync_master_no_transactions++;

 

然后返回;

可以看到,上述关键的一步是对(reply_file_name_,reply_file_pos_)的判断,以决定是否需要继续等待;该变量的更新由dump线程来触发

d.dump线程的处理逻辑

那么在执行一条事务后,dump线程会有哪些调用逻辑呢?

1.开始发送binlog之前需要重置packet(reset_transmit_packet)

调用函数repl_semi_reserve_header,用于在Packet的头部预留字节,以维护和备库的交互信息,目前共预留3个字节

这里在packet的头部拷贝两个字节,值为ReplSemiSyncBase::kSyncHeader,固定值,作为MAGIC NUMBER

const unsigned char  ReplSemiSyncBase::kSyncHeader[2] =
  {ReplSemiSyncBase::kPacketMagicNum, 0};
只有备库开启了semisync的情况下,才会去保留额外的packet头部比特位,不管主库是否开启了semisync

2.在发送binlog事件之前调用repl_semi_before_send_event,确认是否需要备库反馈,通过设置之前预留的三个字节的第3个字节

HOOK位置(函数mysql_binlog_send)
1659             if (RUN_HOOK(binlog_transmit, before_send_event,
1660                          (thd, 0/*flags*/, packet, log_file_name, pos)))
1661             {
1662               my_errno= ER_UNKNOWN_ERROR;
1663               errmsg= "run 'before_send_event' hook failed";
1664               GOTO_ERR;
1665             }
repl_semi_before_send_event->

   repl_semisync.updateSyncHeader(packet,

                                        log_file,
                                        log_pos,
                                        param->server_id);

该函数执行以下步骤,目的是填充上一步保留的头部字节:

1)检查主库和备库是否同时打开了semisync,如果没有,直接返回

2)加锁LOCK_binlog_,再次检查主库是否开启semisync

设置sync为false;

3)如果当前semisync是同步状态(即state_为TRUE)

同样的先检查当前的binlog位点是否有其他备库已经接受到(reply_file_name_inited_为true,并且<reply_file_name_, reply_file_pos_>比当前dump线程的位点要大);则sync为false,goto l_end

如果当前正在等待的事务最小位点(wait_file_name_,wait_file_pos_)比当前dump线程的位点要小(或者wait_file_name_inited_为false,只有当前dump线程),再次检查当前dump线程的bin log位点是否是事务的最后一个事件(通过遍历由函数repl_semisync.writeTranxInBinlog产生的事务节点Hash链表来检查),如果是的话,则设置sync为true

4)如果当前semisync为异步状态(state_为FALSE)

当commit_file_name_inited_为TRUE时(事务提交位点信息被更新过,在函数repl_semisync.writeTranxInBinlog中),如果dump线程的位点大于等于上次事务提交的位点(commit_file_name_, commit_file_pos_),表示当前dump线程已经追赶上了主库位点,因此sync被设置为TRUE,

当commit_file_name_inited_为false时,表示还没有事务提交位点信息,同样设置sync为TRUE;

5)当sync为TRUE时,设置packet头部,告诉备库需要其提供反馈

  if (sync)
  {
    (packet)[2] = kPacketFlagSync;
  }

 

plugin/semisync/semisync.cc: 
const unsigned char ReplSemiSyncBase::kPacketFlagSync = 0x01;

 

3.在发送事件后,调用函数repl_semi_after_send_event来读取备库的反馈

HOOK位置(mysql_binlog_send)
1687             if (RUN_HOOK(binlog_transmit, after_send_event, (thd, 0/*flags*/, packet,
1688                                                              log_file_name, skip_group ? pos : 0)))
1689             {
1690               my_errno= ER_UNKNOWN_ERROR;
1691               errmsg= "Failed to run hook 'after_send_event'";
1692               GOTO_ERR;
1693             }

 

如果该事件需要skip,调用ReplSemiSyncMaster::skipSlaveReply,否则调用ReplSemiSyncMaster::readSlaveReply;前者只需要判断是否设置了事件的头部需要sync,如果是的,则调用reportReplyBinlog; 后者则先读取备库传递的数据包,从中读出备库传递的binlog坐标信息,函数ReplSemiSyncMaster::readSlaveReply主要有如下流程:

1)如果无需等待,直接返回,即没有设置sync标记;
此时可以保证这是事务的最后一个事件;

2)做一次net_flush将缓冲的数据刷走,然后net_clear,等待备库的反馈的数据包(my_net_read)
从my_net_read返回后,更新统计信息

      rpl_semi_sync_master_net_wait_num++;
      rpl_semi_sync_master_net_wait_time += wait_time;

 

从数据包中读取备库传递过来的binlog位点信息,然后调用reportReplyBinlog:
result = reportReplyBinlog(server_id, log_file_name, log_file_pos); 

3)ReplSemiSyncMaster::reportReplyBinlog的流程如下:

<1>检查主库semisync是否打开,如果没有,goto l_end;

<2>如果当前semisync为异步状态,尝试将其切换为同步状态,(try_switch_on(server_id, log_file_name, log_file_pos);)

<3>如果reply_file_name_inited_为true(大多数情况)
比较当前dump线程接收到备库反馈的位点信息与(reply_file_name_, reply_file_pos_)做对比,如果小于后者,说明已经有别的备库读到更新的事务了,这时候无需更新(reply_file_name_, reply_file_pos_)

semisync只保证全局至少有一个备库读到更新的事务

<4>如果需要,更新reply_file_pos_和reply_file_name_
 556     strcpy(reply_file_name_, log_file_name);
 557     reply_file_pos_ = log_file_pos;
 558     reply_file_name_inited_ = true;

 

然后清理当前位点之前的事务节点信息
562     active_tranxs_->clear_active_tranx_nodes(log_file_name, log_file_pos);

<5> 若当前等待的开启semisync的备库(rpl_semi_sync_master_wait_sessions > 0) ,且当前(reply_file_name_, reply_file_pos_) 大于(wait_file_name_, wait_file_pos_),即接收到的备库反馈位点信息大于等于当前等待的事务的最小位点,则设置
    can_release_threads=true;
    wait_file_name_inited_ = false;  –>这意味着新的等待事务,需要重新设置等待位点信息

<6>释放锁,如果can_release_threads为true,进行一次broadcast,唤醒等待的用户线程

备库

当接受到主库发送的binlog后,开启了semisync的备库需要为其发送反馈
备库同样也是为Binlog_relay_IO_delegate增加一个observer,即relay_io_observer,通过HOOK的方式回调PLUGIN的函数,主要包括如下接口函数

Binlog_relay_IO_observer relay_io_observer = {
  sizeof(Binlog_relay_IO_observer), // len

  repl_semi_slave_io_start,     // start
  repl_semi_slave_io_end,       // stop
  repl_semi_slave_request_dump, // request_transmit
  repl_semi_slave_read_event,   // after_read_event
  repl_semi_slave_queue_event,  // after_queue_event
  repl_semi_reset_slave,        // reset
};
a.开启io线程时,在连接主库之前调用repl_semi_slave_io_start
HOOK位置 (handle_slave_io)

4081   if (RUN_HOOK(binlog_relay_io, thread_start, (thd, mi)))
4082   {
4083     mi->report(ERROR_LEVEL, ER_SLAVE_FATAL_ERROR,
4084                ER(ER_SLAVE_FATAL_ERROR), "Failed to run 'thread_start' hook");
4085     goto err;
4086   }

 

主要就是设置全局变量rpl_semi_sync_slave_status,如果开启了备库的semisync则设置该变量为TRUE;

b.连接完主库后,请求发起dump时,调用repl_semi_slave_request_dump

HOOK位置 (handle_slave_io->request_dump)

3026   if (RUN_HOOK(binlog_relay_io,
3027                before_request_transmit,
3028                (thd, mi, binlog_flags)))
3029     goto err;

 

该函数用户检查主库是否支持SEMISYNC(检查是否存在rpl_semi_sync_master_enabled变量),如果支持的话,设置用户变量为rpl_semi_sync_slave:
 query= "SET @rpl_semi_sync_slave= 1";
  if (mysql_real_query(mysql, query, strlen(query)))
在主库上就是通过@rpl_semi_sync_slave来鉴别一个dump请求的SLAVE是否是开启semisync的;

初始化成功后,我们给主库一些DML,继续debug

c.读取事件,调用函数repl_semi_slave_read_event
HOOK位置 (handle_slave_io)
4274       if (RUN_HOOK(binlog_relay_io, after_read_event,
4275                    (thd, mi,(const char*)mysql->net.read_pos + 1,
4276                     event_len, &event_buf, &event_len)))
4277       {
由于我们在主库上是对packet头部有附加了3个比特的,这里需要将其读出来,同时需要更新event_buf及event_len的值;
如果rpl_semi_sync_slave_status为false,表示开启io线程时未打开semisync,直接使用packet的长度即可,否则调用ReplSemiSyncSlave::slaveReadSyncHeader,去读取packet的头部信息,如果需要给主库一个ack,则设置semi_sync_need_reply为TRUE

d.当将binlog写入relaylog之后(即完成函数queue_event之后),调用repl_semi_slave_queue_event
HOOK位置 (handle_slave_io)

4295       if (RUN_HOOK(binlog_relay_io, after_queue_event,
4296                    (thd, mi, event_buf, event_len, synced)))
4297       {
4298         mi->report(ERROR_LEVEL, ER_SLAVE_FATAL_ERROR,
4299                    ER(ER_SLAVE_FATAL_ERROR),
4300                    "Failed to run 'after_queue_event' hook");
4301         goto err;
4302       }
如果备库开启了semisync且需要ack时(pl_semi_sync_slave_status && semi_sync_need_reply),调用ReplSemiSyncSlave::slaveReply,向主库发送数据包,包括当前的binlog文件名及偏移量信息

e.停止IO线程时,调用函数repl_semi_slave_io_end,将rpl_semi_sync_slave_status设置为false,这里判断的mysql_reply实际上不会用到;

存在的问题主要集中在主库上:

1.锁的粒度太粗,看看能不能细化,或者使用lock-free的算法来优化

2.字符串比较的调用,大部分是对binlog文件名的比较,实际上只要比较后面的那一串数字就足够了;尝试下看看能否有性能优化

原创文章,转载请注明: 转载自Simple Life

本文链接地址: MySQL semisync replication配置及源代码实现

Post Footer automatically generated by wp-posturl plugin for wordpress.


Comments

Leave a Reply

Your email address will not be published. Name and email are required


Current month ye@r day *