mysql半同步(semi-sync)源码实现
mysql复制简单介绍了mysql semi-sync的出现的原因,并说明了semi-sync如何保证不丢数据。这篇文章主要侧重于semi-sync的实现,结合源码将semi-sync的实现过程展现给大家。最新的semi-sync源码可以参考官方5.7版本的实现,https://github.com/mysql/mysql-server。
打开semi-sync的正确姿势
默认情况下的mysql复制都是异步复制,mysql通过参数来控制semi-sync开关。具体而言,主库上通过rpl_semi_sync_master_enabled参数控制,备库上通过rpl_semi_sync_slave_enabled参数控制,打开这两个参数后,mysql semi-sync的特性就打开了。注意对于备库而言,为了保证半同步立即生效,需要重启slave的IO线程。另外,还有一个比较重要的参数是rpl_semi_sync_master_timeout,这个参数用于控制master等待semi-slave ack报文的时间,单位是毫秒,默认是10000。master等待超时,则切换为普通的异步复制。
master:
set global rpl_semi_sync_master_enabled=1;
set global rpl_semi_sync_master_timeout=xxx;
slave:
stop slave io_thread;
set global rpl_semi_sync_slave_enabled=1;
start slave io_thread;
另外需要注意的是,打开了上述两个参数只说明master-slave已经具备打开semi-sync的基本条件了,但复制是否依照半同步运行,还需要根据Rpl_semi_sync_master_status的状态值确定。因为比如slave较master有很大延迟(超过rpl_semi_sync_master_timeout),那么复制切换为普通复制。对于需要调试代码的童鞋而言,rpl_semi_sync_master_trace_level和rpl_semi_sync_slave_trace_level非常重要,通过设置level取值,可以打印日志记录半同步的详细过程,方便定位问题。
semi-sync的实现
semi-sync说到底也是一种复制,只不过是在普通的复制基础上,添加了一些步骤来实现。因此semi-sync并没有改变复制的基本框架,我们的讨论也从master,slave两方面展开。
1.master(主库)
主库上面主要包含三个部分,(1).负责与slave-io线程对接的binlog dump线程,将binlog发送到slave,(2).主库上写事务的工作线程,(3).收取semi-slave报文的ack_receiver线程。
(1).binlog dump流程
主要执行逻辑在mysql_binlog_send函数中。
1.判断slave是否是semi_slave,调用add_slave将semi-slave加入到ack_receiver线程的监听队列中。判断的逻辑是slave对应的会话上是否设置了参数rpl_semi_sync_slave。
2.根据slave的请求偏移和binlog文件,从指定位点读取binlog
3.根据文件和位点,捞binlog文件中的数据
4.调用updateSyncHeader设置数据包头semi-sync标记
根据实时semi-sync运行状态来确定是否设置(这个状态由ack_receiver线程根据是否及时收到slave-ack报文设置)
5.调用my_net_write发送binlog
6.调用net_flush确认网络包发送出去
如果当前所有产生的binlog已经处理完,需调用wait_for_update_bin_log等待binlog更新。
(2).半同步事务提交流程
mysql5.6以后提交采用组提交方式,主要分为三个阶段,每个阶段有一个队列,增加semi-sync后,又增加了一个semi-sync阶段。
1.flush阶段:
队列能保证写binlog的顺序与innodb-commit的顺序一致。通过队列,可以保证顺序写每个事务的binlog-cache,然后只进行一次write操作(flush_cache_to_file)。flush阶段后,如果sync_binlog不是1,则通知master有新binlog产生;如果sync_binlog为1,则等待sync阶段后,再通知dump线程有新binlog产生。这里我理解是因为如果不为1,则可能没有后续的sync阶段,而操作系统缓存也有binlog数据,所以可以在flush阶段后通知;而对于sync_binlog为1的情况,可以保证主库的binlog先落地,永远比备库多。但如果sync_binlog不为1,比如1000,则主机异常情况下,则可能出现备库的binlog比主库还多的情况。根据sync_binlog的设置,确认是否要跳过sync阶段。
2.sync阶段:
sync_binlog_file
3.semi_sync阶段:
call_after_sync,等待备库应答。调用cond_timewait等待条件变量&COND_binlog_send_,注意这里只是leader线程在等,将leader线程唤醒后,才会结束semi_sync阶段,进而唤醒其它的follower线程。
4.commit阶段:
innodb-commit
waitAfterCommit,等待备库应答。调用cond_timewait等待条件变量&COND_binlog_send_ 。最终我们根据semi-sync复制模式的设置(AFTER_COMMIT,AFTER_SYNC),来确定是第(3)步还是第(4)步进行等待。
(3).接收slave-ack报文流程
这个流程的工作主要在ack_receiver线程中,这个线程的主要作用是监听semi-slave的ack包,确认master-slave链路是否工作在半同步状态,并根据实际运行状态将普通复制与半同步复制进行切换。打开主库rpl_semi_sync_master_enabled参数后,该线程启动,关闭参数后,该线程消亡。
流程如下:
1.遍历semi-slave数组
2.通过select函数监听每个slave是否有网络包过来
3.调用my_net_read读取包数据
4.调用reportReplyPacket处理semi-sync复制状态
若备库已经获取了最新的binlog位点,则唤醒等待的工作线程
5.调用reportReplyBinlog唤醒等待的线程,mysql_cond_broadcast(&COND_binlog_send_);
2.slave(备库)
主要实现在(handle_slave_io)
1.启动io-thread后,调用safe_connect建立与master的连接
2.调用request_dump函数处理请求binlog逻辑
(1).执行命令SET @rpl_semi_sync_slave= 1,设置一个局部变量,通过这个参数标记slave为semi-slave
(2).发送命令COM_BINLOG_DUMP请求日志
循环从master端收取日志,处理日志
{
1.调用read_event,从master端收取日志(如果没有网络包,会阻塞等待)
2.调用slaveReadSyncHeader,确定网络包头是否有semi-sync标记
3.调用queue_event将日志写入relay-log,在这个过程中会过滤自身server-id的日志
4.如果有semi-sync标记,调用slaveReply函数,发送ack报文
}