主从复制流程
MySQL 的主从复制是依赖于 binlog 的,也就是记录 MySQL 上的所有变化并以二进制形式保存在磁盘上二进制日志文件。主从复制就是将 binlog 中的数据从主库传输到从库上,一般这个过程是异步的,即主库上的操作不会等待 binlog 同步的完成。从库跟主库之间维持了一个长连接。主库内部有一个线程,专门用于服务从库的这个长连接。一个事务日志同步的完整过程是这样的:
- 在从库上执行 change master to 命令,设置主库的 IP、端口、用户名、密码,以及要从哪个位置开始请求 binlog,这个位置包含文件名和日志偏移量,可在主库上执行 show master status 命令查看。这些信息在连接时会记录到从库的数据目录下的 master.info 文件中,以后再连接主库时将不用再提供这些信息而是直接读取该文件进行连接。
- 在从库上执行 start slave 命令,这时候从库会启动两个线程,分别是 IO 线程和 SQL 线程,即下图中的 io_thread 和 sql_thread。其中 io_thread 负责与主库建立连接并接收主库的 binlog。
- 主库校验完从库发来的用户名、密码后,会启动一个 dump 线程,该线程会按从库传过来的位置,从本地读取(dump)出 binlog 发给从库。从库拿到 binlog 后,先写到本地文件,称为中转日志(relay log)。然后 sql_thread 读取中转日志,解析出日志里的命令并执行,将数据写入到从库中。
- 后续可通过命令 SHOW SLAVE STATUS 观察主从复制状态。
在这个方案中,主库使用独立的 dump 线程是一种异步的方式,可以避免对主库的主体更新流程产生影响,而从库在接收到信息后并不是写入从库的存储中,是写入一个 relay log,是避免写入从库实际存储会比较耗时,最终造成从库和主库延迟变长。整个执行流程如下图所示:
从复制的机制上可以知道,在复制进行前,slave 上必须具有 master 上部分完整内容作为复制基准数据。例如,master上有数据库 A,二进制日志已经写到了 pos1 位置,那么在复制进行前,slave 上必须要有数据库 A,且如果要从 pos1 位置开始复制的话,还必须有和 master 上 pos1 之前完全一致的数据。如果不满足这样的一致性条件,那么在 replay 中继日志时将不知道如何重放而导致数据混乱。也就是说,复制是基于 binlog 的 position 进行的,复制之前必须保证 position 一致。
MySQL 5.6 版本对比 MySQL 5.5 版本在复制上进行了很大的改进,主要包括支持 GTID(Global Transaction ID,全局事务 ID)复制和多 SQL 线程并行重放。GTID 的复制方式和传统的复制方式不一样,通过全局事务 ID,它不要求复制前 slave 有基准数据,也不要求 binlog 的 position 一致。在 MySQL 5.7.17 则提出了组复制(MySQL Group Replication,MGR)的概念,为 MySQL 集群中多主复制的很多问题提供了很好的方案,可谓是一项革命性的功能。
主从复制优化
1. 多线程复制
在官方的 5.6 版本之前,MySQL 只支持单线程复制,因此在主库并发高、TPS 高时就会出现严重的主备延迟问题。从单线程复制到最新版本的多线程复制,中间的演化经历了好几个版本。但说到底,所有的多线程复制机制,都是要把之前从库中的单个 sql_thread 线程拆成多个线程,io 线程还是只有一个。即下面这个模型:
当 IO 线程将主库的 binlog 写入 relay log 后,会有一个多线程协调器(multithreaded slave coordinator)对多个 SQL 线程进行调度,让它们按照一定的规则去执行 relay log 中的事件。即图中的 coordinator 就是原来的 sql_thread,不过现在它不再直接更新数据了,而是只负责读取中转日志和分发事务。真正更新日志的变成了 worker 线程。而 work 线程的个数由参数 slave_parallel_workers 决定,默认为 0,即关闭多线程功能。
为了保证事务执行的先后顺序,coordinator 在分发时,需要满足以下这两个基本要求:
- 不能造成更新覆盖。这就要求更新同一行的两个事务,必须被分发到同一个 worker 中串行执行。
- 同一个事务不能被拆开,必须放到同一个 worker 中。
在 MySQL 5.6 版本的并行复制中,支持的粒度是按库并行。这个策略的并行效果,取决于压力模型。如果在主库上有多个 DB,并且各个 DB 的压力均衡,使用这个策略的效果会很好。
2. 半同步复制
MySQL 的默认复制模式是异步模式,即 MySQL 的主服务器上的 I/O 线程,将数据写到 binlog 中就直接返回给客户端数据更新成功,不考虑数据是否传输到从服务器,以及是否写入到 relay log 中。在这种模式下,复制数据其实是有风险的,一旦数据只写到了主库的 binlog 中还没来得急同步到从库时主库宕机了,此时就会造成数据的丢失。但这种模式也是效率最高的,因为变更数据的功能都只是在主库中完成就可以了,从库复制数据不会影响到主库的写数据操作。
为了解决异步复制的数据可靠性问题,MySQL 从 5.5 版本开始允许通过以插件的形式开始支持半同步的主从复制模式。半同步复制模式是介于异步和同步之间的一种复制模式,主库在执行完客户端提交的事务后,要等待至少一个从库接收到 binlog 并将数据写入到 relay log 中才返回给客户端成功结果,可通过参数 rpl_semi_sync_master_wait_no_slave 配置至少得到 slave 的 ACK 个数,默认为 1。半同步复制模式比异步模式提高了数据的可用性,但也产生了一定的性能延迟。
半同步复制的原理是在 master 的 dump 线程去通知从库时,增加了一个 ACK 机制,也就是会确认从库是否收到事务的标志码,master 的 dump 线程不但要发送 binlog 到从库,还要负责接收 slave 的 ACK。当 slave 出现异常没有返回 ACK 时,主库将自动降级为异步复制,直到异常修复后再自动变为半同步复制。
但半同步复制模式也存在一定的数据风险,当事务在主库提交完后等待从库 ACK 的过程中,如果 master 宕机了,这个时候就会有两种情况的问题:
- 事务还没发送到 slave上:若事务还没发送 slave 上,客户端在收到失败结果后,会重新提交事务,因为重新提交的事务是在新的 master 上执行的,所以会执行成功,后面若是之前的 master 恢复后,会以 slave 的身份加入到集群中,此时,之前的事务就会被执行两次,第一次是之前此机器作为 master 时执行的,第二次是做为 slave 后从主库中同步过来的。
- 事务已经同步到 slave 上:因为事务已经同步到 slave 了,所以当客户端收到失败结果后,再次提交事务,那么此事务就会在当前 slave 机器上执行两次。
为了解决上面的隐患,MySQL 从 5.7 版本开始,增加了一种新的半同步方式。新的半同步方式的执行过程是将 “Storage Commit” 这一步移动到了 “Write Slave dump” 后面。这样保证了只有 slave 的事务 ACK 后,才提交主库事务。MySQL 5.7.2 版本新增了一个 rpl_semi_sync_master_wait_point 参数用来配置半同步方式,该参数有两个值可配置:
- AFTER_SYNC:参数值为AFTER_SYNC时,代表采用的是新的半同步复制方式。
- AFTER_COMMIT:代表采用的是之前的旧方式的半同步复制模式。
MySQL 从 5.7.2 版本开始,默认的半同步复制方式就是 AFTER_SYNC 方式了,但这种复制方式也不是万能的,因为 AFTER_SYNC 方式是在事务同步到 slave 后才提交主库事务的,若是当主库等待 slave 同步成功的过程中 master 挂了,这个 master 事务提交就失败了,客户端也收到了事务执行失败的结果了,但是 slave 上已经将 binlog 的内容写到 relay log 里了,此时 slave 数据就会多了,但是多了数据一般问题不算严重,多了总比少了好。MySQL 在没办法解决分布式数据一致性问题的情况下,它只能保证的是不丢数据。
半同步复制的相关参数如下:
mysql> show variables like '%Rpl%';
+-------------------------------------------+------------+
| Variable_name | Value |
+-------------------------------------------+------------+
| rpl_semi_sync_master_enabled | ON |
| rpl_semi_sync_master_timeout | 10000 |
| rpl_semi_sync_master_trace_level | 32 |
| rpl_semi_sync_master_wait_for_slave_count | 1 |
| rpl_semi_sync_master_wait_no_slave | ON |
| rpl_semi_sync_master_wait_point | AFTER_SYNC |
| rpl_stop_slave_timeout | 31536000 |
+-------------------------------------------+------------+
-- 半同步复制模式开关
rpl_semi_sync_master_enabled
-- 半同步复制,超时时间,单位毫秒,当超过此时间后,自动切换为异步复制模式
rpl_semi_sync_master_timeout
-- MySQL 5.7.3引入的,该变量设置主需要等待多少个slave应答,才能返回给客户端,默认为1。
rpl_semi_sync_master_wait_for_slave_count
-- 此值代表当前集群中的slave数量是否还能够满足当前配置的半同步复制模式,默认为ON,当不满足半同步复制模式后,全部Slave切换到异步复制,此值也会变为OFF
rpl_semi_sync_master_wait_no_slave
-- 代表半同步复制提交事务的方式,5.7.2之后,默认为AFTER_SYNC
rpl_semi_sync_master_wait_point
主从复制延时监控
在主从复制的过程中,与数据同步有关的时间点主要包括以下三个:
- 主库 A 执行完成一个事务,写入 binlog,我们把这个时刻记为 T1。
- 之后传给备库 B,我们把备库 B 接收完这个 binlog 的时刻记为 T2。
- 备库 B 执行完成这个事务,我们把这个时刻记为 T3。
所谓主备延迟,就是同一个事务,在备库执行完成的时间和主库执行完成的时间之间的差值,即 T3-T1。我们可以在备库执行 show slave status 命令,返回结果里的 seconds_behind_master 即当前备库延迟的秒数。
注意:seconds_behind_master 的计算方法是这样的:
- 每个事务的 binlog 里面都有一个时间字段,用于记录主库上写入的时间;
- 备库取出当前正在执行的事务的时间字段值,计算它与当前系统时间的差值即 seconds_behind_master。
可以看到,其实 seconds_behind_master 这个参数计算的就是 T3-T1,这个值的时间精度是秒。那如果主备库机器的系统时间设置不一致,会不会导致主备延迟的值不准?其实不会的。因为备库连接到主库时,会通过执行 SELECT UNIX_TIMESTAMP() 函数来获得当前主库的系统时间。如果这时候发现主库的系统时间与自己不一致,备库在执行 seconds_behind_master 计算的时候会自动扣掉这个差值。
主从读写分离
做了主从复制之后,我们就可以在写入时只写主库,在读数据时只读从库,这样即使写请求会锁表或者锁记录,也不会影响到读请求的执行。同时呢,在读流量比较大的情况下,我们可以部署多个从库共同承担读流量,这就是所说的“一主多从”部署方式。另外,从库也可以当成一个备库来使用,以避免主库故障导致数据丢失。
那是不是无限制地增加从库的数量就可以抵抗大量的并发呢?实际上并不是的。因为随着从库数量增加,从库连接上来的 IO 线程比较多,主库也需要创建同样多的 log dump 线程来处理复制的请求,对于主库资源消耗比较高,同时受限于主库的网络带宽,所以在实际使用中,一般一个主库最多挂 3~5 个从库。
读写分离的主要目标是分摊主库压力。通常的架构是在 MySQL 和客户端之间有一个中间代理层 proxy,客户端只连接 proxy, 由 proxy 根据请求类型和上下文决定请求的分发路由。
这类方案的代表比较多,如早期阿里巴巴开源的 Cobar、基于 Cobar 开发出来的 Mycat、360 开源的 Atlas、美团开源的基于 Atlas 开发的 DBProxy 等等。这一类中间件部署在独立的服务器上,业务代码如同在使用单一数据库一样使用它,实际上它内部管理着很多的数据源,当有数据库请求时,它会对 SQL 语句做必要的改写,然后发往指定的数据源。对客户端比较友好。
它一般使用标准的 MySQL 通信协议,所以可以很好地支持多语言。由于它是独立部署的,所以也比较方便进行维护升级,比较适合有一定运维能力的大中型团队使用。它的缺陷是所有的 SQL 语句都需要跨两次网络:从应用到代理层和从代理层到数据源,所以在性能上会有一些损耗。