本文分析基于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为