一主多从架构主要应用场景:读写分离。读写分离的主要目标是分摊主库的压力。
读写分离架构
读写分离架构一
架构一结构图:
这种结构模式下,一般会把数据库的连接信息放在客户端的连接层,由客户端主动做负载均衡。也就是说由客户端来选择后端数据库进行查询。
读写分离架构二
架构二结构图:
该架构模式下,在MySQL和客户端之间有一个中间代理层proxy, 客户端只连接proxy, 由proxy根据请求类型和上下文决定请求的分发路由。
两种架构的区别
1)客户端直连方案
因为少了一层proxy转发, 所以查询性能稍微好一点儿, 并且整体架构简单, 排查问题更方便。 但是这种方案, 由于要了解后端部署细节, 所以在出现主备切换、 库迁移等操作的时候, 客户端都会感知到, 并且需要调整数据库连接信息。
你可能会觉得这样客户端也太麻烦了, 信息大量冗余, 架构很丑。 其实也未必, 一般采用这样的架构, 一定会伴随一个负责管理后端的组件, 比如Zookeeper, 尽量让业务端只专注于业务逻辑开发。
2)带proxy架构
带proxy的架构, 对客户端比较友好。 客户端不需要关注后端细节, 连接维护、 后端信息维护等工作, 都是由proxy完成的。 但这样的话, 对后端维护团队的要求会更高。 而且, proxy也需要有高可用架构。 因此, 带proxy架构的整体就相对比较复杂。
注:无论使用哪种架构都会遇到主从延迟问题(即在从库上会读到系统的一个过期状态),且主从延迟是不能100%避免的。
问:处理主从延迟有哪些方案?
答:主从延迟处理方案有以下几种:
- 强制走主库方案
- Sleep方案
- 判断主备无延迟方案
- 配合semi-sync方案
- 等主库位点方案
- 等GTID方案
下面对这几种方案详细介绍。
强制走主库方案
强制走主库方案思路:对查询请求进行分类,对于必须要拿到最新结果的请求,强制将其发到主库上。对于可以读到旧数据的请求,才将其发到从库上。
这个方案最大的问题在于, 有时候你会碰到“所有查询都不能是过期读”的需求, 比如一些金融类的业务。 这样的话, 你就要放弃读写分离, 所有读写压力都在主库, 等同于放弃了扩展性。
尽管如此,该方案仍是用的最多的一种方案。
Sleep方案
Sleep方案思路:主库更新后, 读从库之前先sleep一下。 具体的方案就是, 类似于执行一条select sleep(1)命令。
注:这个方案的假设是, 大多数情况下主备延迟在1秒之内, 做一个sleep可以有很大概率拿到最新的数据。
示例:
以卖家发布商品为例, 商品发布后, 用Ajax(Asynchronous JavaScript + XML, 异步JavaScript和XML) 直接把客户端输入的内容作为“新的商品”显示在页面上, 而不是真正地去数据库做查询。
这样, 卖家就可以通过这个显示, 来确认产品已经发布成功了。 等到卖家再刷新页面, 去查看商品的时候, 其实已经过了一段时间, 也就达到了sleep的目的, 进而也就解决了过期读的问题。
但从严格意义上来说,这个方案存在不精确问题。这个不精确主要包含两层意思:
1)假设一个查询请求原本可以在0.5s就可以在从库上拿到正确结果,此时也会等1s。
2)如果延迟超过1s,还是会出现主从延迟。
判断主备无延迟方案
确保备库无延迟,通常有以下三种做法:
查看seconds_behind_master确保主备无延迟
每次从库执行查询请求前,先执行show slave status\G判断seconds_behind_master是否已经等于0。 如果还不等于0 , 那就必须等到这个参数变为0才能执行查询请求。注:seconds_behind_master的单位是秒。show slave status\G结果的部分截图:
对比位点确保主备无延迟
1)Master_Log_File和Read_Master_Log_Pos, 表示的是读到的主库的最新位点。
2)Relay_Master_Log_File和Exec_Master_Log_Pos, 表示的是备库执行的最新位点。
如果Master_Log_File和Relay_Master_Log_File、 Read_Master_Log_Pos和Exec_Master_Log_Pos这两组值完全相同, 就表示接收到的日志已经同步完成。
show slave status\G结果部分截图:
对比GTID集合确保主备无延迟
1)Auto_Position=1 , 表示这对主备关系使用了GTID协议。
2)Retrieved_Gtid_Set, 是备库收到的所有日志的GTID集合。
3)Executed_Gtid_Set, 是备库所有已经执行完成的GTID集合。
如果这两个集合相同, 也表示备库接收到的日志都已经同步完成。
可见, 对比位点和对比GTID这两种方法, 都要比判断seconds_behind_master是否为0更准确。
show slave status\G结果部分截图:
问:在执行查询请求之前, 先判断从库是否同步完成的方法, 相比于sleep方案, 准确度确实提升了不少, 但还是没有达到“精确”的程度。 为什么这么说呢?
一个事务的binlog在主备库之间的状态:
1)主库执行完成, 写入binlog, 并反馈给客户端。
2)binlog被从主库发送给备库, 备库收到。
3)在备库执行binlog完成。
上面判断主备无延迟的逻辑, 是“备库收到的日志都执行完成了”。 但是, 从binlog在主备之间状态的分析中, 不难看出还有一部分日志, 处于客户端已经收到提交确认, 而备库还没收到日志的状态。
示例场景如下:
这时, 主库上执行完成了三个事务trx1、 trx2和trx3, 其中:
1)trx1和trx2已经传到从库, 并且已经执行完成了。
2)trx3在主库执行完成, 并且已经回复给客户端, 但是还没有传到从库中。
如果这时候你在从库B上执行查询请求, 按照我们上面的逻辑, 从库认为已经没有同步延迟, 但还是查不到trx3的。 严格地说, 就是出现了主从延迟。
配合semi-sync方案
该方案可以解决“对比GTID集合确保主备无延迟”中提到的不“精确”问题。
semi-sync replication(半同步复制)主备间的状态:
1)事务提交的时候,主库把binlog发给从库。
2)从库收到binlog后,给主库返回一个ack,表示收到了。
3)主库收到这个ack后,才能给客户端返回“事务完成”的确认。
也就是说,如果启用了semi-sync, 就表示所有给客户端发送过确认的事务, 都确保了备库已经收到了这个日志。
问1:如果主库掉电的时候,有些binlog还来不及发给从库,会不会导致系统数据丢失?
1)如果使用的是普通的异步复制模式,则有可能会丢失数据。
2)如果使用的是semi-sync+位点判断的方案,则可以解决数据丢失问题。
需要注意的是,semi-sync+位点判断的方案,只对一主一备的场景成立。在一主多从场景中,主库只要等到一个从库的ack,就会开始给客户端返回确认。此时,在从库上执行查询请求,有以下两种情况:
1)如果查询落在这个响应了ack的从库上,能够确保读到最新数据。
2)如果查询落到其它从库上,他们可能还没有收到最新的日志,就可能会产生主从延迟。
注:如果在业务高峰期, 主库的位点或者GTID集合更新很快, 那么上面的两个位点等值判断就会一直不成立, 很可能出现从库上迟迟无法响应查询请求的情况。
问2:当发起一个查询请求后,如果要求得到准确结果,是否需要等到“主备完全同步”,才能执行查询请求?
答:不需要。
示例如下:
图中备库B下的虚线框, 分别表示relaylog(备库执行的最新位点)和binlog中的事务。 可以看到, 图中从状态1 到状态4, 一直处于延迟一个事务的状态。
备库B一直到状态4都和主库A存在延迟, 如果用上面必须等到无延迟才能查询的方案, select语句直到状态4都不能被执行。
但是, 其实客户端是在发完trx1更新后发起的select语句, 我们只需要确保trx1已经执行完成就可以执行select语句了。 也就是说, 如果在状态3执行查询请求, 得到的就是预期结果了。
semi-sync+主备无延迟方案,存在两个问题:
1)一主多从的时候, 在某些从库执行查询请求会存在主从延迟问题。
2)在持续延迟的情况下, 可能出现过度等待的问题。
等主库位点方案
该方案能解决“semi-sync+主备无延迟方案”存在的两个问题。
下面先来看一条命令:
select master_pos_wait(file, pos[, timeout]);
这条命令的逻辑如下:
1)它是在从库执行的。
2)参数file和pos指的是主库上的文件名和位置。
3)timeout可选, 设置为正整数N表示这个函数最多等待N秒。
这条命令返回结果如下:
1)如果执行期间, 备库同步线程发生异常, 则返回NULL。
2)如果等待超过N秒, 就返回-1。
3)如果刚开始执行的时候, 就发现已经执行过这个位置了, 则返回0。
4)如果返回的是一个正整数M, 表示从命令开始执行, 到应用完file和pos表示的binlog位置, 执行了多少事务。
对于上图中先执行trx1, 再执行一个查询请求的逻辑, 要保证能够查到正确的数据, 我们可以使用这个逻辑:
1)trx1事务更新完成后, 马上执行show master status得到当前主库执行到的File和Position。
2)选定一个从库执行查询语句。
3)在从库上执行select master_pos_wait(File, Position, 1)。
4)如果返回值是>=0的正整数, 则在这个从库执行查询语句。
5)否则, 到主库执行查询语句。
上述逻辑流程图:
这里我们假设, 这条select查询最多在从库上等待1秒。 那么, 如果1秒内master_pos_wait返回一个大于等于0的整数, 就确保了从库上执行的这个查询结果一定包含了trx1的数据。
步骤5到主库执行查询语句, 是这类方案常用的退化机制(兜底方案)。 因为从库的延迟时间不可控, 不能无限等待, 所以如果等待超时, 就应该放弃, 然后到主库去查。
GTID方案
MySQL中同样提供了一个类似的命令:
select wait_for_executed_gtid_set(gtid_set, 1);
这条命令的逻辑是:
1)等待, 直到这个库执行的事务中包含传入的gtid_set, 返回0。
2)超时返回1。
在前面等位点的方案中, 我们执行完事务后, 还要主动去主库执行show master status。 而MySQL 5.7.6版本开始, 允许在执行完更新类事务后, 把这个事务的GTID返回给客户端, 这样等GTID的方案就可以减少一次查询。
等GTID的执行流程:
1)trx1事务更新完成后, 从返回包直接获取这个事务的GTID, 记为gtid1。
2)选定一个从库执行查询语句。
3)在从库上执行 select wait_for_executed_gtid_set(gtid1, 1)。
4)如果返回值是0, 则在这个从库执行查询语句。
5)否则, 到主库执行查询语句。
上述逻辑流程图:
问:怎么能够让MySQL在执行事务后, 返回包中带上GTID呢?
答:把参数session_track_gtids设置为OWN_GTID, 然后通过API接口mysql_session_track_get_first从返回包解析出GTID的值即可。
小结:思考题
思考:假设你的系统采用了等GTID的方案, 现在你要对主库的一张大表做DDL,在做读写分离时可能会出现什么情况呢? 为了避免这种情况, 你会怎么做呢?
假设,这条语句在主库上要执行10分钟,提交后传到备库就要10分钟(典型的大事务)。那么,在主库DDL之后再提交的事务的GTID,去备库查的时候,就会等10分钟才出现。
这样,这个读写分离机制在这10分钟之内都会超时,然后走主库。
这种预期内的操作,应该在业务低峰期的时候,确保主库能够支持所有业务查询,然后把读请求都切到主库,再在主库上做DDL。等备库延迟追上以后,再把读请求切回备库。
通过这个思考题,我主要想让关注的是,大事务对等位点方案的影响。
当然了,使用gh-ost方案来解决这个问题也是不错的选择。