mysql主从复制 ack_Mysql主从复制 - 此鱼不得水的个人空间 - OSCHINA - 中文开源技术交流社区...

本文深入探讨MySQL复制机制,包括异步复制、半同步复制、并行复制及其应用场景,并简要介绍第三方复制工具。

今天我们聊聊复制,复制对于mysql的重要性不言而喻,mysql集群的负载均衡,读写分离和高可用都是基于复制实现。下文主要从4个方面展开,mysql的异步复制,半同步复制和并行复制,最后会简单聊下第三方复制工具。由于生产环境中,innodb存储引擎支持事务,并且行级复制使用广泛,所以下文的讨论都是基于这种假设。

异步复制

异步复制是mysql自带的最原始的复制方式,主库和备库成功建立起复制关系后,在备库上会有一个IO线程去主库拉取binlog,并将binlog写到本地,就是图1中的Relay log,然后备库会开启另外一个SQL线程去读取回放Relay log,通过这种方式达到Master-Slave数据同步的目的。通常情况下,Slave是只读的,可以承担一部分读流量,而且可以根据实际需要,添加一个或多个Slave,这样在一定程度上可以缓解主库的读压力;另一方面,若Master出现异常(crash,硬件故障等),无法对外提供服务,此时Slave可以承担起Master的重任,避免了单点的产生,所以复制就是为容灾和提高性能而生。

729a09875e5327ee526ebe07b2eb6288.png

图1

半同步复制

一般情况下,异步复制就已经足够应付了,但由于是异步复制,备库极有可能是落后于主库,特别是极端情况下,我们无法保证主备数据是严格一致的(即使我们观察到Seconds Behind Master 这个值为0)。比如,当用户发起commit命令时,Master并不关心Slave的执行状态,执行成功后,立即返回给用户。试想下,若一个事务提交后,Master成功返回给用户后crash,这个事务的binlog还没来得及传递到Slave,那么Slave相对于Master而言就少了一个事务,此时主备就不一致了。对于要求强一致的业务是不可以接受的,半同步复制就是为了解决数据一致性而产生的。

为什么叫半同步复制?先说说同步复制,所谓同步复制就是一个事务在Master和Slave都执行后,才返回给用户执行成功。而半同步复制不要求Slave执行,而仅仅是接收到日志后,就通知Master可以返回了。这里关键点是Slave接受日志后是否执行,若执行后才通知Master则是同步复制,若仅仅是接受日志成功,则是半同步复制。对于Mysql而言,我们谈到的日志都是binlog,对于其他的关系型数据库可能是redo log或其他日志。

半同步复制如何实现?半同步复制实现的关键点是Master对于事务提交过程特殊处理。目前实现半同步复制主要有两种模式,AFTER_SYNC模式和AFTER_COMMIT模式。两种方式的主要区别在于是否在存储引擎提交后等待Slave的ACK。先来看看AFTER_COMMIT模式,如图2,Start和End分别表示用户发起Commit命令和Master返回给用户的时间点,中间部分就是整个Commit过程Master和Slave做的事情。

01efab70de6f25022005708882475ed8.png

图2

Master提交时,会首先将该事务的redo log刷入磁盘,然后将事务的binlog刷入磁盘(这里其实还涉及到两阶段提交的问题,这里不展开讲),然后进入innodb commit流程,这个步骤主要是释放锁,标记事务为提交状态(其他用户可以看到该事务的更新),这个过程完成后,等待Slave发送的ACK消息,等到Slave的响应后,Master才成功返回给用户。看到图中红色虚线部分,这段是Master和Slave的同步逻辑,是Master-Slave一致性的保证。

半同步复制是否能保证不丢数据?我们通过几种场景来简单分析下。第一种情况:假设Master第1,2步执行成功后,binlog还没来得及传递给Slave,此时Master挂了,Slave作为新Master提供服务,那么备库比主库要少一个事务(因为主库的redo 和binlog已经落盘),但是不影响用户,对于用户而言,这个事务没有成功返回,那么提交与否,用户都可以接受,用户一定会进行异常捕获而重试。第二种情况,假设第3步innodb commit执行成功后,binlog还没来得及传递给Slave,此时Master挂了,此时与第一种情况一样,备库比主库少一个事务,但是其他用户在3执行完后,可以看到该事务的更新,而切换到备库后,却发现再次读这个更新又没了,这个就发生了“幻读”,如果其他事务依赖于这个更新,则会对业务逻辑产生影响。当然这仅仅是极端情况。

对于第二种情况产生的影响,AFTER_SYNC模式可以解决这一问题。与AFTER_COMMIT相比,master在AFTER_SYNC模式下,Fsync binlog后,就开始等待SLAVE同步。那么在进行第5步innodbcommit后,即其它事务能看到该事务的更新时,Slave已经成功接收到binlog,即使发生切换,Slave拥有与Master同样的数据,不会发生“幻读”现象。但是对于上面描述的第一种情况,结果是一样的。

所以,在极端情况下,半同步复制的Master-Slave会有一个事务不一致,但是对于用户而言,由于这个事务并没有成功返回给用户,所以无论事务提交与否都是可以接受的,用户有必要进行查询或重试,判读是否更新成功。或者我们想想,对于单机而言,若事务执行成功后,返回给用户时,网络断了,用户也是面临一样的问题,所以,这不是半同步复制的问题。对于提交返回成功的事务,版同步复制保证Master-Slave一定是一致的,从这个角度来看,半同步复制不会丢数据,可以保证Master-Slave的强一致性。图3是AFTER_SYNC模式,事务提交过程。

192d9f7113f5418fe48d866ca387ecea.png

图3

并行复制

半同步复制解决了Master-Slave的强一致问题,那么性能问题呢?从图1中可以看到参与复制的主要有两个线程:IO线程和SQL线程,分别用于拉取和回放binlog。对于Slave而言,所有拉取和解析binlog的动作都是串行的,相对于Master并发处理用户请求,在高负载下, 若Master产生binlog的速度超过Slave消费binlog的速度,导致Slave出现延迟。如图4,可以看到,Users和Master之间的管道远远大于Master和Slave之间的管道。

5d8cf0bb53256048770ad024fcf0e90a.png

图4

那么如何并行化,并行IO线程,还是并行SQL线程?其实两方面都可以并行,但是并行SQL线程的收益更大,因为SQL线程做的事情更多(解析,执行)。并行IO线程,可以将从Master拉取和写Relay log分为两个线程;并行SQL线程则可以根据需要做到库级并行,表级并行,事务级并行。库级并行在mysql官方版本5.6已经实现。如图5,并行复制框架实际包含了一个协调线程和若干个工作线程,协调线程负责分发和解决冲突,工作线程只负责执行。图中,DB1,DB2和DB3的事务就可以并发执行,提高了复制的性能。有时候库级并发可能不够,需要做表级并发,或更细粒度的事务级并发。

03ce9b1582d065150e01ac6fd447aff3.png

图 5

并行复制如何处理冲突?并发的世界是美好的,但不能乱并发,否则数据就乱了。Master上面通过锁机制来保证并发的事务有序进行,那么并行复制呢?Slave必需保证回放的顺序与Master上事务执行顺序一致,因此只要做到顺序读取binlog,将不冲突的事务并发执行即可。对于库级并发而言,协调线程要保证执行同一个库的事务放在一个工作线程串行执行;对于表级并发而言,协调线程要保证同一个表的事务串行执行;对于事务级而言,则是保证操作同一行的事务串行执行。

是否粒度越细,性能越好?这个并不是一定的。相对于串行复制而言,并行复制多了一个协调线程。协调线程一个重要作用是解决冲突,粒度越细的并发,可能会有更多的冲突,最终可能也是串行执行的,但消耗了大量的冲突检测代价。

第三方复制工具

为什么会出现第三方复制工具?第三方复制工具的出现一定是内嵌的复制功能不能满足用户需求,就像半同步复制和并行复制从无到有一样。既然现在mysql复制已经做地这么好了,为什么还有第三方复制工具,我能想到最重要的一点是异构复制。在异构数据源迁移场景下,内嵌复制是无能为力的,第三方复制工具通过解析源端的数据库日志,然后在目的端回放,就能达到同步的目的,比如大名鼎鼎的GoldenGate就是一个例子。第三方复制工具同样能很好地实现并发,在并行复制出现之前,这也是一个巨大的优势。另一方面,就是可以统一下游,避免所有下游都跑到DB上拉binlog,增大DB负载。

<think>我们分析原始代码和用户提出的替代方案的区别。 原始代码: ```c if (icsk->icsk_ack.pending & ICSK_ACK_PUSHED) icsk->icsk_ack.pending |= ICSK_ACK_PUSHED2; icsk->icsk_ack.pending |= ICSK_ACK_PUSHED; ``` 替代方案: ```c if (icsk->icsk_ack.pending & ICSK_ACK_PUSHED) { icsk->icsk_ack.pending |= ICSK_ACK_PUSHED2; } else { icsk->icsk_ack.pending |= ICSK_ACK_PUSHED; } ``` ### 区别分析 1. **原始代码逻辑**: - 如果已经有`ICSK_ACK_PUSHED`标志,则添加`ICSK_ACK_PUSHED2`标志。 - 然后,无论是否已有`ICSK_ACK_PUSHED`标志,都设置`ICSK_ACK_PUSHED`标志(即确保该标志被设置)。 2. **替代方案逻辑**: - 如果已经有`ICSK_ACK_PUSHED`标志,则添加`ICSK_ACK_PUSHED2`标志(但不设置`ICSK_ACK_PUSHED`,因为它已经存在)。 - 如果没有`ICSK_ACK_PUSHED`标志,则设置`ICSK_ACK_PUSHED`标志。 ### 为何不使用替代方案? 原因在于:**原始代码需要确保每次执行后`ICSK_ACK_PUSHED`标志都被设置**,而替代方案在已经存在`ICSK_ACK_PUSHED`标志的情况下不会再次设置它(虽然重复设置同一个标志没有副作用,但这不是重点)。更重要的是,替代方案在设置`ICSK_ACK_PUSHED2`标志的同,并没有确保`ICSK_ACK_PUSHED`标志被设置(实际上它已经存在,所以没问题)。但原始代码的逻辑意图是: - 当触发ACK请求,总是设置`ICSK_ACK_PUSHED`标志(表示有ACK需要发送)。 - 如果在设置之前,已经有一个挂起的ACK请求(即`ICSK_ACK_PUSHED`已经存在),那么说明现在又来了一个ACK请求,此升级为`ICSK_ACK_PUSHED2`(表示有多个ACK请求,需要快速发送)。 ### 关键点:状态升级的连续性 在TCP延迟确认机制中,`ICSK_ACK_PUSHED2`标志表示“已经有一个延迟ACK等待发送,现在又来了新数据,需要快速ACK”。因此,它要求: 1. 第一次触发:设置`ICSK_ACK_PUSHED`(表示开始延迟ACK)。 2. 第二次触发:在`ICSK_ACK_PUSHED`已经存在的情况下,设置`ICSK_ACK_PUSHED2`(表示需要快速ACK)。 ### 为什么不能省略最后的设置? 如果采用替代方案,在已经存在`ICSK_ACK_PUSHED`的情况下,我们只设置`ICSK_ACK_PUSHED2`,而不设置`ICSK_ACK_PUSHED`(因为已经存在,所以设置与否都一样)。这看起来似乎没有区别,因为按位或操作同一个标志多次没有变化。但是,原始代码的写法更清晰地表达了意图:**无论之前是否有`ICSK_ACK_PUSHED`,现在都要确保它有**。 ### 潜在的问题:标志清除机 考虑标志清除的机:假设在两次触发之间,延迟ACK器到期并发送了ACK,同清除了`ICSK_ACK_PUSHED`标志。那么: - 替代方案中,如果清除发生在条件判断之后,那么执行else分支设置`ICSK_ACK_PUSHED`,但不会设置`ICSK_ACK_PUSHED2`。这样,第二次触发实际上被当作第一次触发处理(因为清除后标志消失),这会导致丢失快速ACK的机会。 - 而原始代码中,即使清除发生在条件判断之后,最后的按位或操作会重新设置`ICSK_ACK_PUSHED`,因此不会丢失第二次触发(因为条件判断可能已经清除,所以条件不成立,但最后一行会设置`ICSK_ACK_PUSHED`,然后下一次触发就会设置`ICSK_ACK_PUSHED2`)。 然而,在实际代码中,清除标志和设置标志的代码路径是互斥的(通过锁保护),所以上述竞态条件可能不会发生。但原始代码的写法更健壮,因为它不依赖于清除操作是否恰好发生在这个代码段中间。 ### 结论:原始代码的设计意图 原始代码的意图是: - 无条件设置`ICSK_ACK_PUSHED`(表示有ACK需要发送)。 - 如果之前已经设置了`ICSK_ACK_PUSHED`(表示之前已经有ACK请求还未发送),则再设置`ICSK_ACK_PUSHED2`(表示需要快速发送)。 因此,原始代码确保: 1. 每次调用都会标记`ICSK_ACK_PUSHED`(表示有ACK请求)。 2. 如果这是连续的第二次(或更多次)请求,则升级为`ICSK_ACK_PUSHED2`。 ### 为什么替代方案不符合要求? 替代方案在第一次触发设置`ICSK_ACK_PUSHED`,第二次触发设置`ICSK_ACK_PUSHED2`(同不改变`ICSK_ACK_PUSHED`,因为它已经存在)。这看起来似乎正确,但注意:在第一次触发后,如果ACK被发送并清除标志,那么第二次触发,替代方案会进入else分支,只设置`ICSK_ACK_PUSHED`,而不会设置`ICSK_ACK_PUSHED2`。这样,第二次触发就被当作第一次触发处理,从而失去了快速ACK的机会。 而原始代码在第二次触发- 条件判断:此可能ACK已经发送,标志已清除,所以条件不成立。 - 然后执行最后的按位或:设置`ICSK_ACK_PUSHED`(这样第二次触发就标记了)。 - 然后,如果第三次触发,条件判断就会成立(因为第二次触发已经设置了`ICSK_ACK_PUSHED`),然后设置`ICSK_ACK_PUSHED2`。 也就是说,原始代码中,连续两次触发(中间没有发送ACK)才能设置`ICSK_ACK_PUSHED2`;而替代方案中,如果两次触发的间隔内ACK被发送,那么第二次触发不会被升级。 但实际上,在TCP中,一个ACK发送后,标志会被清除。因此,如果两次触发发生在同一个延迟窗口内(即ACK尚未发送),则原始代码的条件判断会成立(第一次触发已经设置`ICSK_ACK_PUSHED`,第二次触发条件为真,设置`ICSK_ACK_PUSHED2`)。如果第一次触发后ACK已经发送,清除标志,那么第二次触发就是独立的一次,不会设置`ICSK_ACK_PUSHED2`,这是合理的。 因此,替代方案和原始代码在功能上是否等价,取决于清除标志的机。但原始代码的写法确保了无论清除发生在何时,每次触发都会设置`ICSK_ACK_PUSHED`,而连续两次触发(在同一个延迟窗口内)会设置`ICSK_ACK_PUSHED2`。 ### 总结 不使用替代方案的原因: 1. **逻辑清晰性**:原始代码明确表达了“总是设置基础标志,若基础标志已存在则升级”的意图。 2. **状态连续性**:原始代码确保在快速连续触发正确升级状态,避免因标志清除机导致的竞态条件。 3. **代码简洁性**:原始代码用两行独立操作清晰表达了两个独立操作(升级和设置基础标志),而替代方案用if-else结构将两个操作耦合在一起。 因此,原始代码是符合设计意图的最佳写法。 ### 相关问题 §§相关问题§§ 1. 在TCP延迟确认机制中,`ICSK_ACK_PUSHED2`标志具体在什么情况下被清除? 2. 当同设置`ICSK_ACK_PUSHED`和`ICSK_ACK_PUSHED2`,TCP协议栈会如何处理ACK的发送? 3. 延迟确认定器的超间如何动态调整? </think>### 代码逻辑差异分析 原代码: ```c if (icsk->icsk_ack.pending & ICSK_ACK_PUSHED) icsk->icsk_ack.pending |= ICSK_ACK_PUSHED2; icsk->icsk_ack.pending |= ICSK_ACK_PUSHED; ``` 用户提议的替代方案: ```c if (icsk->icsk_ack.pending & ICSK_ACK_PUSHED) { icsk->icsk_ack.pending |= ICSK_ACK_PUSHED2; } else { icsk->icsk_ack.pending |= ICSK_ACK_PUSHED; } ``` ### 不使用 if-else 结构的原因 1. **状态叠加需求** 在TCP延迟确认机制中,`ICSK_ACK_PUSHED`和`ICSK_ACK_PUSHED2`是**累积状态标志**: - `ICSK_ACK_PUSHED`表示"有数据需要确认" - `ICSK_ACK_PUSHED2`表示"在等待确认期间又有新数据到达" 原代码允许两个标志同存在($$ \text{状态} = \text{PUSHED} | \text{PUSHED2} $$),而if-else结构在条件成立不会设置`ICSK_ACK_PUSHED`,破坏了状态叠加语义[^1][^4]。 2. **序安全性考量** 在临界区代码中,标志位可能被异步修改(如定器处理): ```c // tcp_delack_timer_handler 可能清除标志位 icsk->icsk_ack.pending &= ~ICSK_ACK_TIMER; ``` 原代码的无条件设置确保在任何情况下都标记了当前触发事件,避免因竞态条件丢失ACK请求[^1][^5]。 3. **快速确认模式要求** 当`tcp_in_quickack_mode()`检测到`ICSK_ACK_PUSHED2`,会绕过延迟定器直接发送ACK: ```c if (tcp_in_quickack_mode(sk)) tcp_send_ack(sk); // 立即发送ACK ``` 原代码保证第二次触发必定存在`ICSK_ACK_PUSHED2`,而if-else结构在标志已存在不再设置基础标志,可能导致快速确认逻辑失效[^3][^4]。 4. **状态机完整性** TCP的ACK状态机需要明确区分三个状态: | 状态 | 标志组合 | 行为 | |---|---|----| | 空闲 | 无标志 | 无操作 | | 首次触发 | PUSHED | 启动延迟定器 | | 二次触发 | PUSHED+PUSHED2 | 立即发送ACK | 原代码通过位运算实现状态升级路径: $$ \begin{cases} \emptyset \xrightarrow{\text{触发}} \text{PUSHED} \\ \text{PUSHED} \xrightarrow{\text{再次触发}} \text{PUSHED} | \text{PUSHED2} \end{cases} $$ 而if-else结构破坏了状态连续性[^2][^4]。 ### 设计哲学:显式优于隐式 Linux网络栈优先采用显式状态设置: 1. 每次事件触发都明确记录(无条件设置`ICSK_ACK_PUSHED`) 2. 状态升级通过附加标志实现(条件设置`ICSK_ACK_PUSHED2`) 3. 位操作保证原子性,避免锁开销 这种设计在`__tcp_ack_snd_check`等核心函数中形成统一范式: ```c if (需要快速确认) tcp_send_ack(sk); else tcp_send_delayed_ack(sk); // 依赖标志位状态[^4] ``` ### 相关问题
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值