MySQL semi-sync 实现原理分析

本文分析基于Oracle MySQL 5.6.15版本源码,是在阅读MySQL源码以及印风博客基础上总结的,作为新年的第一篇博文,总结去年底的一点学习所得,将重点总结以下内容:
1). binlog group commit
2). semi-sync replication

binlog group commit

这是一个很古老的问题,简单啰嗦一下:为了保证数据安全,事务提交时需要将binlog刷盘,这个其实是分为2歩来做的,write和fsync,其中第二步保证数据持久化,非常耗时,会成为性能瓶颈,如果每提交一个事务都做一次fsync,系统TPS将会大大降低,为了解决这个问题,需要减少fsyn调用的次数,原理:活跃系统中,在一个线程做fsync时,其它线程将等待,等待过程中从这些线程中选出一个leader(进入等待的第一个线程),其它线程由leader代为完成write+fync,leader完成工作后通知所有等待的线程继续运行

在Percona server 5.5.18代码中,binlog group commit分为两个阶段:flush/commit,代码实现基本与上述描述一致,在Oracle MySQL 5.6.15中,binlog group commit模块被重写,这个过程分为3个stage:flush/sync/commit,本文不打算对这部分详细分析,只介绍一下区别:
1). 5.5中只有两个阶段,leader线程write+sync做完后才通只dump线程复制binlog到备库,5.6中在write做完后fsync之前通过dump线程,提前了开始复制binlog到备库的时间
2). 5.6中引入了Stage_manager,代码逻辑更加清晰
3). 5.6中每个阶段都可以group,理论上性能更好

关于binlog group commit的介绍就到这里,进入本文重点部分

semi-sync replication

semi-sync在MySQL中是以plugin方式实现的,如果从软件设计的角度来看,是一个标准的Observer模式,在代码执行到某些关键位置时通知相关Observer,通过RUN_HOOK宏调用注册到某类Observer中的所有plugin的相应函数,MySQL中有四类这样的Observer,与semi-sync相关的函数调用逻辑总结如下(每个函数的实现细节请参考印风博客):

Trans_observer: 监听事务commit/rollback

after_commit--> repl_semi_report_commit--> ReplSemiSyncMaster::commitTrx --> cond_timewait
after_rollback --> repl_semi_report_rollback --> ReplSemiSyncMaster::commitTrx --> cond_timewait

Binlog_storage_observer:监听binlog flush/fync动作

after_flush --> repl_semi_report_binlog_update --> repl_semisync.writeTranxInBinlog --> (active_tranxs_->insert_tranx_node)
after_sync --> repl_semi_report_binlog_sync --> ReplSemiSyncMaster::commitTrx --> cond_timewait

Binlog_transmit_observer:监听dump线程发送binlog的过程

start --> repl_semi_binlog_dump_start --> repl_semisync.add_slave
stop --> repl_semi_binlog_dump_end --> repl_semisync.remove_slave
reserve_header --> repl_semi_reserve_header --> repl_semisync.reserveSyncHeader
before_send_event --> repl_semi_before_send_event --> repl_semisync.updateSyncHeader
after_send_event --> repl_semi_after_send_event --> repl_semisync.readSlaveReply --> ReplSemiSyncMaster.reportReplyBinlog --> cond_broadcast
before_reset_master --> repl_semi_before_reset_master --> repl_semisync.beforeResetMaster
after_reset_master --> repl_semi_reset_master --> repl_semisync.resetMaster

Binlog_relay_IO_observer:监听slave IO线程接收binlog的过程

start --> repl_semi_slave_io_start --> repl_semisync.slaveStart
stop --> repl_semi_slave_io_end --> repl_semisync.stopStart
request_transmit --> repl_semi_slave_request_dump
after_read_event --> repl_semi_slave_read_event
after_queue_event --> repl_semi_slave_queue_event
reset --> repl_semi_reset_slave

semi-sync的实现需要与三类线程交互,下面详细分析

用户线程

提交事务时需要写binlog,在group commit的第一个阶段(flush)完成后,用户线程通知binlog dump线程复制binlog到所有的备库,之后继续执行sync/commit阶段,等待收到备库ack(或者确认备库已收到更大的的ack)后返回用户,等待ack时只需要确认有一个slave收到binlog即可

 
MYSQL_BIN_LOG::ordered_commit
// 进入flush阶段
|--> change_stage(thd, Stage_manager::FLUSH_STAGE, thd, NULL, &LOCK_log)
|--> if follower 
   |--> MYSQL_BIN_LOG::finish_commit 
   |--> RUN_HOOK(transaction, after_commit, (head, all))
   |--> RETURN
 
|--> process_flush_stage_queue
// 执行write操作
|--> flush_cache_to_file
 
// 添加到活跃事务链表中
|--> RUN_HOOK(binlog_storage, after_flush, (thd, file_name_ptr, flush_end_pos))
 
// 通知所有binglog dump线程复制binlog到备库
|--> signal_update
 
// 执行fsync将数据刷盘
|--> sync_binlog_file
 
// 进入sync阶段
|--> change_stage(thd, Stage_manager::SYNC_STAGE, wait_queue, &LOCK_log, &LOCK_sync)
|--> if follower 
   |--> MYSQL_BIN_LOG::finish_commit 
   |--> RUN_HOOK(transaction, after_commit, (head, all))
   |--> RETURN
 
// 进入commit阶段
|--> change_stage(thd, Stage_manager::COMMIT_STAGE, final_queue, &LOCK_sync, &LOCK_commit)
|--> if follower 
   |--> MYSQL_BIN_LOG::finish_commit 
      |--> RUN_HOOK(transaction, after_commit, (head, all))
   |--> RETURN
 
process_after_commit_stage_queue --> RUN_HOOK(transaction, after_commit, (head, all))
 
|--> MYSQL_BIN_LOG::finish_commit 
   // 等待备库ack收到
   |--> RUN_HOOK(transaction, after_commit, (head, all))

binlog dump线程

binglog dump线程在一个while循环中发送binlog到一个备库,发送之前在binlog event头部写入semi-sync相关信息(3个字节),决定是否需要备库回复ack,数据发送到备库后等待相应的ack(包括binlog文件名和pos)

mysql_binlog_send
 
// 初始化semi-sync
|--> RUN_HOOK(binlog_transmit, transmit_start, (thd, flags, log_ident, pos))
 
while loop
|--> mysql_bin_log.wait_for_update_bin_log(thd, heartbeat_ts)
|--> reset_transmit_packet --> RUN_HOOK(binlog_transmit, reserve_header, (thd, flags, packet))
// 设置binlog头部,要求备库ack
|--> RUN_HOOK(binlog_transmit, before_send_event, (thd, flags, packet, log_file_name, pos))
// 发送binlog到备库
... my_net_write(net, (uchar*) packet->ptr(), packet->length())
// 等待读取备库ack,唤醒等待ack的用户线程,从活跃事务链表中清除
|--> RUN_HOOK(binlog_transmit, after_send_event, (thd, flags, packet))
end while
 
// dump停止
|--> RUN_HOOK(binlog_transmit, transmit_stop, (thd, flags))

slave io线程

slave io线程在while循环中不断接收到主库发来的binlog后,检测event头部以确定是否需要回复主库ack,如果需要,将对应的binlog文件名和pos回复主库

handle_slave_io
|--> RUN_HOOK(binlog_relay_io, thread_start, (thd, mi))
|--> register_slave_on_master(mysql, mi, &suppress_warnings)
 
while (!io_slave_killed(thd,mi))
|--> request_dump
   |--> RUN_HOOK(binlog_relay_io, before_request_transmit, (thd, mi, binlog_flags))
|--> read_event
// 读取semi-sync头部并进行校验
|--> RUN_HOOK(binlog_relay_io, after_read_event,
                   (thd, mi,(const char*)mysql->net.read_pos + 1,
                    event_len, &event_buf, &event_len))
|--> queue_event
// 回复主库ack
|--> RUN_HOOK(binlog_relay_io, after_queue_event,
                   (thd, mi, event_buf, event_len, synced))
end while
 
RUN_HOOK(binlog_relay_io, thread_stop, (thd, mi))

实现细节

1). 备库发送到主库的ack信息:io线程收到的来自主库的binlog文件名和pos,根据event header确定是否回复主库ack,ack由slave io发送(after_queue_event),由主库dump线程接收(after_send_event

2). reply_file_name_/reply_file_pos_: 主库已经收到的最大ack,在收到ack后更新(after_send_event),若dump线程发送给备库的binlog的文件名和pos小于该组合,设置event header以告诉备库不需要回复ack(before_send_event),因为主库已经收到来自其他备库更大的ack了;用户线程在最终提交事务时若对应的binlog位点小于该组合也不需要dump线程等待接收ack(after_commit

3). wait_file_name_/wait_file_pos_: 所有dump线程等待的最小ack,若dump线程发送给备库的binlog的文件名和pos小于该组合,设置event header以告诉备库不需要回复ack(before_send_event),因为已经存在用户线程在等待一个更大的位点了;用户线程需要等待dump线程接收到ack时更新(after_commit),当dump线程收到ack时,确定是否需要唤醒等待的用户线程(after_send_event

4). commit_file_name_/commit_file_pos_: 所有用户线程正在提交事务的最大binlog位点,在写入binlog之后更新(after_flush),当semi-sync因为等待ack超时自动切换到异步复制时,如果dump线程发送的binlog位点赶上该组合,重新启动semi-sync

5). active_tranxs_:活跃事务binlog位点,使用链表管理,同时保存为一个hash,key为,在写入binlog之后插入数据(after_flush),在dump线程收到更大的ack时,清除active_tranxs_中该ack之前的数据(after_send_event

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注