大家好,我是小奇。
日常开发工作中,我们用的最多的数据库就是 MySQL,小奇之前也是如此。但随着我们业务量的增加,单实例的 MySQL 开始无法支撑业务的正常需求;甚至在无保护的场景下可能会直接把 DB 打挂,导致服务不可用数据丢失等问题。
有句话说的好:“好的架构不是设计出来的,而是不断演进出来的。”
MySQL 本身提供了主从复制 的功能,我们可以在持久层采用 主从架构 的方案提升我们系统的性能、以及可用性。
比较常见的形式一般是:一主一从、一主多从。这两种形式使用起来简单高效,我们可以采用读写分离(写主库、读从库)的方案提升系统的并发能力。
1. 同步模型
MySQL 的主从同步是基于 binlog 日志实现的。我们知道 MySQL 中的所有 DML 操作都会记录到 binlog 日志中,MySQL 的同步就是把主库(Master)上的 binlog 日志在从库(Slave)上做了一个重放。
具体操作流程:
- 客户端提交 DML 语句,主库(Master) 顺序写 到 binlog 日志;
- 从库(Slave)的 I/O Thread 线程会 顺序读 主库(Master)的 binlog 日志;
- 主库(Master)的 log dump Thread 线程会往从库的 I/O Thread 线程 顺序写 binlog 日志;
- 从库(Slave)的 I/O Thread 将获取到的 binlog 日志后 顺序写 Relay log (中继日志:其实就是主库的 binlog 日志,改个名儿~)中;
- 从库(Slave) 的 SQL Thread 线程 顺序读 Relay log 并进行数据的重放(重新执行一遍拷贝的 Master binlog,注意:这里是随机写);
1.1 从库(Slave)会产生 binlog 吗?
首先,我们需要区分 binlog 日志和 relay log 日志的区别。
- binlog 日志: 当我们在执行 DML 操作的时候,就会产生 binlog 日志。
- relay log 日志:主从复制中,从主库(Master)同步过来的 binlog 日志;主要是服务于主从复制。
所以我们应该明白,两者并没有什么关系。因此如果从库(Slave)打开了记录 binlog 日志的开关 (SHOW VARIABLES LIKE 'log_bin;' 可查看),那么从库是会产生 binlog 的。
题外话:之前在小米做数仓的数据实时同步,就是采用的解析从库的 binlog 日志(肯定不会让我们拉取主库的 binlog 的,无疑会加大主库的压力~)方式来做数据的重放。
1.2 什么是并行复制(MTS)?
并行复制(enhanced multi-threaded slave)简称为 MST,MySQL5.6 版本提出基于库级别的并行复制;在 5.7 版本中优化为基于行(Row)级别的并行复制。总之,5.7版本之后复制延迟问题基本不存在。
MySQL 为什么会提出并行复制的特性呢?我们可以来分析一下现有的同步模型存在什么问题~
我们看回上面同步模型中列举的 5 个步骤;其中步骤 1-4 都是顺序读写的操作,不会存在特别大的复制延迟问题。但我们可以看到 步骤 5 中的数据重放操作是一个 随机写 的过程;众所周知,随机写会导致性能直线下降。这就是同步延迟的原因!
为什么数据重放是一个随机写?
很简单,假设 binlog 日志所执行的语句(binlog 为二进制文件,这里简单的可视化为 SQL)为:
update table t_user set name = "kylin" where id = 1
update table t_user set name = "garwei" where id = 10000
从库的 SQL Thread 在顺序 的回放 binlog 日志,但是!!日志所操作的数据行在磁盘上可能相差十万八千里; id =1 的数据行可能在磁盘块1 中,而 id = 10000的数据行可能存储在磁盘块 10 中。 这就是随机写的原因。
所以我们明白了:复制延迟的最主要原因在数据重放的时候,由于单线程的 SQL Thread 在随机写磁盘导致性能急剧下降。
那有什么方式可以优化这个问题呢? MySQL 提出了 “并行复制” 的思想:“让多个工作线程(worker)并行执行数据重放。”
架构图如下:
- 引入了协调线程 (coordinator),承担 relay log 日志的分发(当然自己也可以进行回放)。
- 引入多个工作线程(worker),主要执行 binlog 日志的重放。
这里协调线程在分发日志会存在很多问题,为避免并发、数据更新丢失等问题;日志分发规则为:
- 更新同一行的多个事务,必须分发到同一个 worker 中执行
- 同一个事务需要在一个 worker 中执行
更多细节就待各位看官自己去了解拉~
2. 同步方式
MySQL 的同步方式有: 异步复制、半同步复制、全同步复制。
可能有同鞋会犯迷糊,这里的同步方式和上面的 “并行复制” 有啥区别?
并行复制描述的是:数据在从库(Slave)进行回放的过程;而这里的同步方式描述的是:主库(Master)和从库(Slave)binlog 同步的过程。
2.1 异步复制
一个主库、一个/多个从库的模型中,binlog 日志异步的发送到各个从库。
客户端提交 DML 语句到主库 Master,主库Master 执行 SQL 写 binlog 日志;事务执行完毕后立刻返回客户端结果,启异步通知 Log dump Thread 进行 binlog 的传输;并不关心 binlog 是否成功到达从库(不可靠的网络)。
异步复制的优点很明显,就是性能高;但同时缺点也很突出:数据丢失。
假设主库(Master)在成功的响应返回后宕机了,而发送的 binlog 日志由于网络的问题丢失了;这样在新选举出来的主库就会出现数据不完整的情况(主库宕机前处理的事务并没有在从库成功进行回放)。
2.2 全同步复制
一个主库、一个/多个从库的模型中,binlog 日志同步的发送到各个从库中,并等待所有从库响应(ACK)后,主库才应答客户端。
客户端提交 DML 语句到主库 Master,主库Master 执行 SQL 写 binlog 日志;主库同步通知 Log dump Thread 执行 binlog 传输,从库成功接收后发送应答 ACK 给主库;主库收到所有的从库发送的应答 ACK 后,返回客户端结果。
全同步的方式可以很强的保证主库和各个从库之间的数据一致性,但是也有很明显的问题:性能会大幅度下降。主库和每个从库都有 IO 操作,同时主库需要等待所有的从库应答后才能返回客户端。假设从库A因为网络问题迟迟无法响应,那么主库就会陷入阻塞等待。
2.3 半同步复制
半同步复制介于异步复制和全同步复制之间,主库在执行完客户端提交的事务后不是立刻返回给客户端,而是等待至少一个从库接收到并写到 relay log 中才返回成功信息给客户端(只能保证主库的 Binlog 至少传输到了一个从节点上),否则需要等待直到超时时间然后切换成异步模式再提交。
相对于异步复制,半同步复制提高了数据的安全性,一定程度的保证了数据能成功备份到从库,同时它也造成了一定程度的延迟,但是比全同步模式延迟要低,这个延迟最少是一个 TCP/IP 往返的时间。所以,半同步复制最好在低延时的网络中使用。
3. 灵魂拷问
主要介绍面试 or 工作中在使用主从模型,应该考虑的一些问题~
3.1 常见的主从同步延迟的原因有哪些?
这里可分为 2 个维度来看,数据库层以及应用层。
数据库层:由于从库在做数据回放的过程是一个单线程的随机写操作,性能会急剧降低;故而 MySQL 在 5.6 提出了并行复制的方案。
应用层:
- 大事务的执行,主库大事务执行 10 分钟,binlog 的写入必须等待事务完成后才会写入, 这个时候从库就会延迟 10 分钟 【重点】(基本无解,需要避免大事务)
- 主库的 TPS 并发非常高的时候,产生的 DDL 数量超过了一个线程所能承受的最大范围,可能影响从库同步效率【重点】 TPS:每秒处理事务的数目。
- 从库的机器性能可能会比主库的差,如果此时机器的资源不足的话可能会影响从库的同步效率
- 从库充当了读库,如果从库的查询压力过大的话,占用过多的 cpu 也会影响同步效率
- 从库在同步数据时,可能会和其他查询的线程发生锁抢占的情况,影响同步效率
3.2 主从同步出现延迟如何解决?
当我们在使用主从架构的时候,我们就应该思考:“当出现主从延迟的时候会产生什么不良影响?” 如果系统并不在意的话,那自然是最好。 但往往由于主从复制的延迟会带来各种奇奇怪怪的问题。
3.2.1 读写一致性
适用场景:读密集、偶尔写的系统。
假设我们搭建了一套数据库采用一主两从的读写分离博客系统(类比掘金平台),有如下场景:“用户A 创建一篇文章并且发布成功;然后用户A 再次刷新自己的博客列表,发现文章消失了!” 这看来十分的诡异,用户体验也十分的不好…
究其原因,我们可以尝试做如下分析:
- 用户A 创建文章并发布成功 -----> 数据写入到主库
- 用户A 刷新自己的博客文章列表 ----> 查询查的从库(用户A 新编写的文章数据未同步)
这就是比较常见的一种主从延迟带来的问题,为解决这种情况,我们需要保证 “读写一致性”。该机制保证如果用户重新加载页面,他们总能看到自己最近提交的更新。但对其他用户则没有任何保证,这些用户的更新可能会在稍后才能刷新看到。
基于主从复制的系统要如何保证读写一致性呢? 目前有比较多的方案,需要我们根据自己的系统 “因地制宜” :
- 如果用户访问会被修改的内容,从主节点获取;否则,在从节点获取。这就需要我们在设计接口的时候需要判断接口返回的数据是否有可能会被修改。例如,掘金平台的个人首页信息只能由所有者编辑,而其他用户无法编辑。这可以形成一个简单的规则:总是从主节点读取用户自己的首页数据,在从节点读取其他用户的首页数据。
- 跟踪最近更新时间。 如果更新发生在一分钟之内,则从主节点读取,否则在从节点读取。同时监控从节点的复制滞后程度,避免从滞后时间超过一分钟的从节点读取。这种方案是基于应用内的数据大部分都会被所有用户修改的前提,那么方案(1)则不适用;最终会导致大量的查询请求仍然需要走的主节点,丧失了从节点的读扩展。
- 客户端还可以记住最近更新时的逻辑时间戳(如:用来指示写入顺序的日志序号),并附带在读请求中;服务器可以保证对该用户提供读服务时都应该至少包含了该时间戳的更新。如果数据不够新,要么交由其他的从节点处理,要么陷入阻塞直到本从节点接收到了最新的更新。
3.2.2 单调读一致性
还是上面的例子,“用户A 创建了一篇文章并发布成功,然后刷新自己的文章列表,第一次刷新文章成功展示出来,用户A很满意;但用户A 手欠又再次刷新了一次(相信大多数人都试过),这时候发现最新发布的文章不见了!” 这个情况用户一定会很诧异:啥玩意???,再刷新一次页面,文章又出来;再再再刷新文章又丢失…
我们可以做如下分析:
- 用户A 创建文章并成功发布 ----> 数据写入到主库
- 用户A 第一次刷新自己的文章列表,最新文章成功展示 ---> 查询走的从库1,从库1 数据同步已完成
- 用户A 手欠第二次刷新自己的文章列表,最新文章丢失 ----> 查询走的从库2,从库2 数据同步未完成
- ......
- ......
我们可以发现:多次查询走的不同的从库,由于从库间数据同步的完整性不一致导致了上述问题。 基于这种情况,提出了 单调读一致性。这是一个比强一致性弱,但比最终一致性强的保证。在读取数据的时候,单调读保证:“如果某个用户依次进行多次读取,则他绝不会看到回滚的情况,即在读取较新的值之后又发生读取旧值的情况”。
比较常见的实现单调读的方式是:
- 确保每个用户总是从固定的某一从节点读取。例如我们可以对用户ID 进行一致性哈希,散列的打散在各个从节点上,但是可以保证同一个用户ID 总是读取的某一固定从节点。(PS:当然当从节点宕机的时候,需要将用户查询重新路由到新的从节点上)。
总而言之,对于复制滞后的问题,我们要在应用层提供比底层数据库更加强有力的保证。例如只在主节点上提供特定类型的读取等… 这些保证会使得应用层代码复杂度更高且容易出错;但是我们往往却不得不如此。
4. 总结
本文主要介绍了 MySQL 主从同步模型,分析了主从同步模型存在的同步延迟问题以及 MySQL 提供的优化方案——并行复制(MTS);同时也介绍了常见的同步方式,诸如:异步复制、全同步复制、半同步复制… 最后还分析了我们常见的主从同步延迟的原因,以及我们在使用主从架构时必须得思考的问题。
本书参考了《数据密集型应用系统设计》的一些思想,如我导师所言:“这本书在不同的阶段读取,都有不同的感悟。” 十分推荐!