MySQL半同步源码解析

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第19天,点击查看活动详情

业务背景

MySQL半同步机制是5.5之后加入进的一个功能,代码由google提供,以插件形式在MySQL中工作的,所以功能相对来说比较独立。

前言

binlog:mysql导出的binlog文件是有序号的,且序号严格递增,所以就会存在smallest file name或者larget file name这样的概念。同时每条事务在文件中是有position的,依照执行顺序依次记录。所以,相同binlog文件中,position大的发生的越晚。不同binlog文件中,binlog序号越大的发生的越晚。 这是很重要的一点,这个先后关系贯穿于整个半同步逻辑中。

ack:slave接收到master的binlog信息之后会返回一个ack报文,里面包含之前master发送过来的binlog文件名和事务在binlog中对应的position,便于master来查找。

mysql开启半同步操作步骤

master上执行 → 

mysql > install plugin rpl_semi_sync_master soname 'semisync_master.so';

mysql > set global rpl_semi_sync_master_enabled=ON;

slave上执行 → 

mysql > install plugin rpl_semi_sync_slave soname 'semisync_slave.so';

mysql > set global rpl_semi_sync_slave_enabled=ON;

核心类

2.1 全局变量

核心变量 类型 用于
rpl_semi_sync_master_enabled bool 控制半同步功能是否打开,值来自配置(文件)
rpl_semi_sync_master_clients ulong 维护参与半同步的slave的数量(slave安装了rpl_semi_sync_slave插件并且开启了半同步才会统计在内)
rpl_semi_sync_master_wait_for_slave_count uint 维护必须要完成同步的slave数量,值来自配置(文件)

2.1.1 rpl_semi_sync_master_clients和rpl_semi_sync_master_wait_for_slave_count的区别

注意,rpl_semi_sync_master_wait_for_slave_count数据是来源与mysql配置,而rpl_semi_sync_master_clients跟当前部署有关,所以会出现rpl_semi_sync_master_clients  < rpl_semi_sync_master_wait_for_slave_count的情况,这种情况下,半同步复制不启动,如果之前生效了,则会自动关闭半同步。

2.2 ReplSemiSyncMaster类

半同步功能的管理类,由其管理整个半同步功能,这个类被设计成单例

2.2.1 变量及方法

核心成员 类型 用途
active_tranxs_ ActiveTranx* 需要进行同步的事务列表(→2.4
state_ bool true表示当前是工作在半同步模式下,fasle表示工作在异步同步模式下,→2.2.2
master_enabled_ bool true表示master已经准备好开始半同步,而false表示master还未准备好。请注意与state_的区别,state_表示的实际状态,而master_enabled_表示设置状态,这两者是有不一致的情况的具体分析见→2.2.2
ack_container_ AckContainer 用于临时存储slave返回的ack信息,用于判断是否已经同步完成。AckContainer设计的非常有意思,详细解释在→2.3节中,与其他方法的执行关系见→2.6.4
reportReplyPacket() int 从返回报文中解析信息。与其他方法的执行关系见→2.6.4
reportReplyBinlog() void 用于解除处于等待事务同步的线程阻塞。这个方法需要根据ack_container的返回,然后到active_tranxs对象所维护的同步事务列表中查找,所以不能单独使用,与其他方法的执行关系见→2.6.4
add_slave() void 用于在全局变量rpl_semi_sync_master_clients上加1。注意,此方法并不会添加具体的slave信息,说明mysql的半同步并不是按照特定的slave来进行同步的,同时注意2.1.1的数值范围说明
remove_slave() void 用于在全局变量rpl_semi_sync_master_clients上减1,同时检查当前的slave数量是否小于rpl_semi_sync_master_wait_for_slave_count,如果小于则意味着不可能达到半同步完成的需求(响应的slave数量大于rpl_semi_sync_master_wait_for_slave_count),则关闭半同步。
writeTranxInBinlog() int 在binlog写入数据的时候,将binlog file和file position注入同步事务列表里。该行为是在监听binlog日志变动时触发

2.2.2 state_,master_enabled_和rpl_semi_sync_master_enabled

这三个值都是用于控制半同步打开关闭的,但又有所不同

最终是否执行半同步,是基于state_字段的。

为什么需要用三个字段?我认为这是为了将设置,准备,执行区分开来,在不同时期检查不同的状态值,避免发生误判。同时配置和执行分开,系统在某些时候需要临时关闭半同步,在条件满足之后自动再开启半同步,而不需要人工来介入。

2.3 AckContainer类

AckContainer是用来临时维护slave的ack信息的,它的组织结构决定了它奇特的逻辑

  1. 内部维护了一个数组,数组的大小等于rpl_semi_sync_master_wait_for_slave_count - 1。解释一下为什么会减1,这是因为最后一个ack到达的时候,该数据可以和数组中保存的数据可以一起使用,没有必要多此一举再保存一遍,同时也能节约空间。如果rpl_semi_sync_master_wait_for_slave_count == 1,也就意味着AckContainer是不需要的,AckContainer对象不进行工作。
  2. 数组内部每个节点维护了slave id,binlog file name, file position,每个节点的slave id是不同的,如果传过来的新的ack所在的slave id已经在数组中存在,则要么丢弃,要么更新该节点

了解了以上两点之后,我们看看,AckContainer怎么帮助我们确认同步是否完成

假设我们有4台slave,设置要求半同步必须要有3台完成(rpl_semi_sync_master_wait_for_slave_count == 3),假设我们先后收到slave的ack如下

  1. slave 2, binlog_002, #100

  2. slave 1, binlog_001, #890

  3. slave 2, binlog_002, #80

  4. slave 2, binlog_003, #005

  5. slave 4, binlog_002, #120

  6. slave 3, binlog_001, #900

我们来看一下数组是如何存放这些数据的

首先创建数组,由于需要等待3个slave的回复,所以数组只有2个空槽(3 - 1 = 2)

slot 1 slot 2
(empty) (empty)

当收到1)的数据之后,由于数组是空的,会放入到数组的第一个空槽中

slot 1 slot 2
slave 2 binlog_002

当收到2)的数据之后,会放入到第二个空槽中

slot 1 slot 2
slave 2 binlog_002

当收到3)的数据之后,首先会检查数组,有没有相同slave id的数据,这个时候发现有,但发现编号小于数组中的数据(80 < 100,,说明3的数据发生在1之前),则废弃(由于tcp协议具有顺序性的原因,这种情况可能不会发生,但机制中还是做了这样的处理),这个时候数组中的数据没有变化

slot 1 slot 2
slave 2 binlog_002

当收到4)的数据之后,同样会检查数据,发现存在相同的slave id的数据,但发现编号大于数据中的数据(binlog编号003 > 002,说明4的数据发生在1之后),则更新数据

slot 1 slot 2
slave 2 binlog_003

当收到5)的数据之后,检查数据,发现是全新的数据,则开始进入比较逻辑

slave 2|binlog_003|005

slave 1|binlog_001|890

slave 4|binlog_002|120

这三组数据表达了一个什么样的含义呢?根据这三个值,我们能确信binlog_001的890已经收到的了至少3个slave的回复,所以所有等待的是小于等于binlog_001|890的事务的节点都可以释放(具体怎么释放不是AckContainer所关心的,具体如何释放阻塞是reportReplyBinlog方法来执行的),所以AckContainer对象返回binlog_001|890用于后续的线程释放工作。同时对数组进行清理,将拥有最小值的slot清空

slot 1 slot 2
slave 2 binlog_003

然后由于5)本身不是最小值,所以再将5存入数组

slot 1 slot 2
slave 2 binlog_003

当收到6)的数据之后,检查发现是全新的数据,执行跟收到5)一样的逻辑,所以最终确定binlog_001|900已经等到3个slave的回复,返回用于后续线程释放,由于6)的数据本身就是最小的,则不会进入数组,数组仍然没有变化

slot 1 slot 2
slave 2 binlog_003

2.4 ActiveTranx类

ActiveTranx是用来记录所有在等待同步事务的管理类

2.4.1 变量及方法

核心成员 类型 用途
allocator_ TranxNodeAllocator 用于事务节点分配
trx_front_ TranxNode* 指向一个有序列表的第一个位置
trx_rear_ TranxNode* 指向一个有序列表的最后一个位置
trx_htb_ TranxNode** 一个hash表的头结点
num_entries_ int 指定trx_htb这个哈希表的数据量的上限

2.4.2 trx_front, trx_rear_, trx_htb_到底在构造一个什么结构?

实际上,这三个值外加num_entries_就是在构造一个sorted hash table,类似于C++中std::map的功能,实现一个有序的hash表。

其中顺序链表的作用是为了唤醒:

唤醒过程为从trx_front开始查找,如果需要环境的binlog编号和file position(数据来自AckContainer对象)小于当前TranxNode节点的数据,则对挂起线程进行唤醒。然后通过next访问下一个TranxNode,同样进行比较,进行唤醒工作。如果发现binlog file name或者file position大于当前节点的数据,则退出流程。

hash table的作用是为了快速查找指定数据是否在节点中以及进行清理操作。注意的是,利用trx_htb清理的时候只处理hash表中的指针,而不会去释放指向对象的空间,对象空间的维护在TranxNodeAllocator(→2.4.3)中处理

2.4.3 TranxNodeAllocator类

allocator_(TranxNodeAllocator)是一个事务节点分配类,它存在的目的就是创建一个TranxNode对象池,2.4.2中涉及的节点都是由其提供,并且在释放的时候回收空间。这样做的目的是批量分配批量回收,避免程序过于频繁的创建TranxNode对象,提高系统效率。

TranxNodeAllocator内部空间维护是按照Block为单位的,如果不够用则会分配更多的Block。一个Block提供16个TranxNode节点备用。只要TranxNodeAllocator对象被创建,至少保留一个Block(这个保留Block是不会被系统回收的,可设置保留Block的数量,但无法设置为没有保留Block)。

Block组织结构如下:

image2021-4-2_19-6-18.png 一个Block预先分配了16个TranxNode

image2021-4-2_19-10-0.png 其分配逻辑为

  1. 查看当前Block(current_block)中的对象是否都分配了,如果没有分配,则依旧在当前Block中获取TranxNode对象
  2. 如果当前Block已经没有空余的节点可供分配,则指针移动到下一个Block上,如果没有下一个Block,则分配一个新的Block。将得到的这个空闲的Block设置为当前Block。
  3. 继续执行1,进行对象分配。

从这个逻辑上看,其实该方案并没有做到极致,所有TranxNode对象只进行一次分配,而没有存在复用的情况,在高QPS情况下,仍然可能存在空间分配过频的情况

2.5 TranxNode结构体

TranxNode是一个结构体,设计的很简单,之所以单独列出来是因为它是数据同步的最小单位,最终的线程挂起和唤醒都在TranxNode对象上执行。

其变量有

成员 类型 用途
log_name_ char [] binlog file name,用于进行顺序比较判断,在之前的篇章中已经反复提及
log_position_ ulonglong binlog file position,数据在当前binlog中的sequence,同样用于顺序比较判断,在之前的篇章中已经反复提及
cond mysql_cond_t 一个mysql封装的条件变量,工作原理类似于c++的std::condition_variable或者java的java.util.concurrent.locks.Condition,用于挂起和唤醒线程
n_waiters int 挂起线程数(该值理论上只有会有1,设计为int类型稍显浪费)
next_ TranxNode* 指向顺序链表的下一个节点,→2.4.2
hash_next_ TranxNode* 哈希冲突时指向下一个节点,→2.4.2

线程访问该结构体,会将自身挂起,等待同步完成时被唤醒

挂起(以wait after commit模式为例, wait after commit见→ 3.2):

image2021-4-6_11-57-31.png log_name_ : [Binlog]

log_position : [filepos]

n_waiters : 1

2.6 Ack_receiver类

Ack_receiver接收类,用于维护与master进行同步的replication代理线程,同时记录slave信息。此类未被设计为单例,但由于只能全局使用,所以等同于单例。与ReplSemiSyncMaster类对应。

2.6.1 核心变量及方法

核心成员 类型 用途
m_status uint8 标记receiver的状态,有ST_UP, ST_DOWN, ST_STOPPING三种状态。→2.6.2
m_slaves Slave_vector slave的代理线程的列表,列表中的每个线程都指代一个slave
add_slave() void 添加slave代理线程。
remove_slave() void 移除slave代理线程。
run() void 执行receiver的监听功能,获取Ack信息,→2.6.3
start() bool 启动receiver,→2.6.3
stop() void 停止receiver,→2.6.3

2.6.2 status状态关系

image2021-4-6_16-25-53 (1).png

2.6.3 start(), stop(), run()的关系

执行的生命周期 start → run → stop。

start:在plugin工作线程中执行,将run方法注册到mysql线程中

run:在mysql的线程池中执行,监听socket消息

stop:发生异常或者主动关闭时结束ack监听功能

Use case.png

2.6.4 Ack_receiver对象获取信息后如何通知ReplSemiSyncMaster对象

Vertical Cross Functional Template.png

3.半同步逻辑

了解了各大主要类的功能之后,就能够逐步明白mysql的半同步过程。

首先我们需要了解mysql的半同步是什么意思。半同步简单来说就是主库主需要等待部分的slave返回数据同步成功的通知,即可认为对此数据的同步已经完成

那么第一个问题就产生了,部分slave是哪些slave?

这个问题就是2.1.1来解决的,当返回的slave数量达到rpl_semi_sync_master_wait_for_slave_count设置,即可认为半同步完成。如果slave的总数和半同步要求的返回服务数一致,其实就相当于全同步,而如果rpl_semi_sync_master_wait_for_slave_count为0,就相当于异步同步。

(*实际上rpl_semi_sync_master_wait_for_slave_count是不能设置为小于1的,但并不妨碍这样理解,因为mysql还提供了rpl_semi_sync_master_wait_no_slave这个参数来实现无需等待的半同步)

由于Data Store, Binlog, Sender, Replica都是mysql本身的功能,不是半同步特有,所以整个半同步功能都集中在Plugin中。

3.1 Plugin流程

Plugin生命周期:初始化 → 同步等待 → 返回释放 → 销毁

主要流程:

------全局事件------

创建Replica的代理线程(connection handle,入口在rpl_slave.cc的handle_slave_sql方法中) 

------Plugin初始化------

创建ReplSemiSyncMaster对象(2.2)

创建Ack_receiver对象(2.6)

注册binlog观察者事件repl_semi_binlog_dump_start到Replica代理线程中(binlog dump前触发)

注册binlog观察者事件repl_semi_report_binlog_update到Replica代理线程中 (binlog dump后触发)

注册binlog观察者事件repl_semi_report_commit到user thread工作线程中(wait after commit模式, 见→ 3.2)(data commit后触发)

OR 注册binlog观察者事件repl_semi_report_binlog_sync到user thread工作线程中(wait after sync模式,见→ 3.2)(binlog 开始同步后触发)

------Data store------

开始准备dump binlog

------同步等待------

repl_semi_binlog_dump_start被触发(Replica代理线程中,此事件发生在dump前)

将replica代理的slave信息添加到ReplSemiSyncMaster(2.2)和Ack_receiver(2.6)对象中****

repl_semi_report_binlog_update被触发,调用ReplSemiSyncMaster::writeTranxInBinlog(2.2.1)将同步数据写入等待列表ActiveTranx中(2.4)

repl_semi_report_commit或者repl_semi_report_binlog_sync事件触发,调用ReplSemiSyncMaster::commitTrx将当前user thread线程挂起

------返回释放------

Ack_receiver::run利用添加的slave信息构建监听

收到slave的ack消息,调用ReplSemiSyncMaster::reportReplyPacket进行解析

将处理过的ack消息存入AckContainer (2.3)对象中,如果满足解锁条件则继续,不满足则继续等待

在等待列表ActiveTranx(2.4)中找到对应数据

调用ReplSemiSyncMaster::reportReplyBinlog进行线程释放

移除ReplSemiSyncMaster(2.2)和Ack_receiver(2.6)对象中的slave信息

……

3.2 wait after commit模式和wait after sync模式

这两种模式的主要区别在于到底在什么阶段进行同步等待

wait after commit模式

08b666b3acb193dc1a0aa5ca506a345c.png

wait after sync模式

4153fcafc3c14bfa7ff94770d41b49a5.png

wait after commit即等待点在数据commit之后才开始等待slave的ack返回,如果master此时down机,有可能导致slave未能获取到该同步数据,导致主从切换后新的master未持有该最新数据。

wait after sync即在发送同步消息时等待,而不进行数据的commit。只有当master接收到了slave的ack之后,确认同步完成,才开始数据commit

3.3 主要对象之间的关系

image2021-4-6_20-52-47.png

3.4 线程模型

image2021-4-7_12-59-45.png

3.5 异常情况处理

mysql默认设置的同步延迟为10s,如果超过10s,则会自动关闭半同步复制而改为异步复制。

这个策略除了能在网络抖动的时候避免服务假死,也能处理如master下所有slave移除或者down机时,新加入的slave从0同步数据耗时较长,也能自动切换避免服务冻结

猜你喜欢

转载自juejin.im/post/7108377280895778824