持续创作,加速成长!这是我参与「掘金日新计划 · 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信息的,它的组织结构决定了它奇特的逻辑
- 内部维护了一个数组,数组的大小等于rpl_semi_sync_master_wait_for_slave_count - 1。解释一下为什么会减1,这是因为最后一个ack到达的时候,该数据可以和数组中保存的数据可以一起使用,没有必要多此一举再保存一遍,同时也能节约空间。如果rpl_semi_sync_master_wait_for_slave_count == 1,也就意味着AckContainer是不需要的,AckContainer对象不进行工作。
- 数组内部每个节点维护了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如下
-
slave 2, binlog_002, #100
-
slave 1, binlog_001, #890
-
slave 2, binlog_002, #80
-
slave 2, binlog_003, #005
-
slave 4, binlog_002, #120
-
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组织结构如下:
一个Block预先分配了16个TranxNode
其分配逻辑为
- 查看当前Block(current_block)中的对象是否都分配了,如果没有分配,则依旧在当前Block中获取TranxNode对象
- 如果当前Block已经没有空余的节点可供分配,则指针移动到下一个Block上,如果没有下一个Block,则分配一个新的Block。将得到的这个空闲的Block设置为当前Block。
- 继续执行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):
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状态关系
2.6.3 start(), stop(), run()的关系
执行的生命周期 start → run → stop。
start:在plugin工作线程中执行,将run方法注册到mysql线程中
run:在mysql的线程池中执行,监听socket消息
stop:发生异常或者主动关闭时结束ack监听功能
2.6.4 Ack_receiver对象获取信息后如何通知ReplSemiSyncMaster对象
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模式
wait after sync模式
wait after commit即等待点在数据commit之后才开始等待slave的ack返回,如果master此时down机,有可能导致slave未能获取到该同步数据,导致主从切换后新的master未持有该最新数据。
wait after sync即在发送同步消息时等待,而不进行数据的commit。只有当master接收到了slave的ack之后,确认同步完成,才开始数据commit
3.3 主要对象之间的关系
3.4 线程模型
3.5 异常情况处理
mysql默认设置的同步延迟为10s,如果超过10s,则会自动关闭半同步复制而改为异步复制。
这个策略除了能在网络抖动的时候避免服务假死,也能处理如master下所有slave移除或者down机时,新加入的slave从0同步数据耗时较长,也能自动切换避免服务冻结