主从复制方式

在最开始,MySQL是以容易学习和方便的高可用架构,被开发人员青睐的。
而它的几乎所有的高可用架构,都直接依赖于binlog。
虽然这些高可用架构已经呈现出越来越复杂的趋势,但都是从最基本的一主一备演化过来的。

主从复制传统方式

切换流程

如图所示就是基本的主备切换流程。
MySQL主备切换流程
MySQL主备切换流程
在状态1中,客户端的读写都直接访问节点A,而节点B是A的备库,只是将A的更新都同步过来,到本地执行。这样可以保持节点B和A的数据是相同的。
当需要切换的时候,就切成状态2。这时候客户端读写访问的都是节点B,而节点A是B的备库。
在状态1中,虽然节点B没有被直接访问,但是依然建议把节点B(也就是备库)设置成只读(readonly)模式。这样做,有以下几个考虑:
  1. 有时候一些运营类的查询语句会被放到备库上去查,设置为只读可以防止误操作;
  1. 防止切换逻辑有bug,比如切换过程中出现双写,造成主备不一致;
  1. 可以用readonly状态,来判断节点的角色。
 
你可能会问,我把备库设置成只读了,还怎么跟主库保持同步更新呢?
这个问题,你不用担心。因为readonly设置对超级(super)权限用户是无效的,而用于同步更新的线程,就拥有超级权限。
接下来,我们再看看节点A到B这条线的内部流程是什么样的。下图中画出的就是一个update语句在节点A执行,然后同步到节点B的完整流程图。
主备流程图
主备流程图
可以看到:主库接收到客户端的更新请求后,执行内部事务的更新逻辑,同时写binlog。
备库B跟主库A之间维持了一个长连接。主库A内部有一个线程,专门用于服务备库B的这个长连接。一个事务日志同步的完整过程是这样的:
  1. 在备库B上通过change master命令,设置主库A的IP、端口、用户名、密码,以及要从哪个位置开始请求binlog,这个位置包含文件名和日志偏移量。
  1. 在备库B上执行start slave命令,这时候备库会启动两个线程,就是图中的io_threadsql_thread。其中io_thread负责与主库建立连接
  1. 主库A校验完用户名、密码后,开始按照备库B传过来的位置,从本地读取binlog,发给B。
  1. 备库B拿到binlog后,写到本地文件,称为中转日志(relay log)。
  1. sql_thread读取中转日志,解析出日志里的命令,并执行。
这里需要说明,后来由于多线程复制方案的引入,sql_thread演化成为了多个线程,跟我们今天要介绍的原理没有直接关系,暂且不展开。
分析完了这个长连接的逻辑,我们再来看一个问题:binlog里面到底是什么内容,为什么备库拿过去可以直接执行。

数据恢复

现在越来越多的场景要求把MySQL的binlog格式设置成row。这么做的理由有很多,我来给你举一个可以直接看出来的好处:恢复数据
接下来,我们就分别从delete、insert和update这三种SQL语句的角度,来看看数据恢复的问题。
即使执行的是delete语句,row格式的binlog也会把被删掉的行的整行信息保存起来。所以,如果你在执行完一条delete语句以后,发现删错数据了,可以直接把binlog中记录的delete语句转成insert,把被错删的数据插入回去就可以恢复了。
如果你是执行错了insert语句呢?那就更直接了。row格式下,insert语句的binlog里会记录所有的字段信息,这些信息可以用来精确定位刚刚被插入的那一行。这时,你直接把insert语句转成delete语句,删除掉这被误插入的一行数据就可以了。
如果执行的是update语句的话,binlog里面会记录修改前整行的数据和修改后的整行数据。所以,如果你误执行了update语句的话,只需要把这个event前后的两行信息对调一下,再去数据库里面执行,就能恢复这个更新操作了。
其实,由delete、insert或者update语句导致的数据操作错误,需要恢复到操作之前状态的情况,也时有发生。MariaDB的Flashback工具就是基于上面介绍的原理来回滚数据的。
虽然mixed格式的binlog现在已经用得不多了,但这里我还是要再借用一下mixed格式来说明一个问题,来看一下这条SQL语句:
insert into t values(10,10, now());
如果我们把binlog格式设置为mixed,你觉得MySQL会把它记录为row格式还是statement格式呢?
先不要着急说结果,我们一起来看一下这条语句执行的效果。
notion image
可以看到,MySQL用的居然是statement格式。你一定会奇怪,如果这个binlog过了1分钟才传给备库的话,那主备的数据不就不一致了吗?
接下来,我们再用mysqlbinlog工具来看看:
TIMESTAMP 命令
TIMESTAMP 命令
从图中的结果可以看到,原来binlog在记录event的时候,多记了一条命令:SET TIMESTAMP=1546103491。它用 SET TIMESTAMP命令约定了接下来的now()函数的返回时间
因此,不论这个binlog是1分钟之后被备库执行,还是3天后用来恢复这个库的备份,这个insert语句插入的行,值都是固定的。也就是说,通过这条SET TIMESTAMP命令,MySQL就确保了主备数据的一致性。
我之前看过有人在重放binlog数据的时候,是这么做的:用mysqlbinlog解析出日志,然后把里面的statement语句直接拷贝出来执行。
你现在知道了,这个方法是有风险的。因为有些语句的执行结果是依赖于上下文命令的,直接执行的结果很可能是错误的
所以,用binlog来恢复数据的标准做法是,用 mysqlbinlog工具解析出来,然后把解析结果整个发给MySQL执行。类似下面的命令:
mysqlbinlog master.000001 --start-position=2738 --stop-position=2973 | mysql -h127.0.0.1 -P13000 -u$user -p$pwd;
这个命令的意思是,将 master.000001 文件里面从第2738字节到第2973字节中间这段内容解析出来,放到MySQL去执行。
 
mysql主从复制存在的问题
主库宕机后,数据可能丢失
从库只有一个SQL Thread,主库写压力大,复制很可能延时
解决方法:
  • 半同步复制---解决数据丢失的问题
  • 并行复制----解决从库复制延迟的问题

半同步复制

为了提升数据安全,MySQL让Master在某一个时间点等待Slave节点的 ACK(Acknowledgecharacter)消息,接收到ACK消息后才进行事务提交,这也是半同步复制的基础,
MySQL从5.5版本开始引入了半同步复制机制来降低数据丢失的概率。
介绍半同步复制之前先快速过一下 MySQL 事务写入碰到主从复制时的完整过程,主库事务写入分为 4个步骤:
  1. InnoDB Redo File Write (Prepare Write)
  1. Binlog File Flush & Sync to Binlog File
  1. InnoDB Redo File Commit(Commit Write)
  1. Send Binlog to Slave
当Master不需要关注Slave是否接受到Binlog Event时,即为传统的主从复制。
当Master需要在第三步等待Slave返回ACK时,即为 after-commit,半同步复制(MySQL 5.5引入)。
当Master需要在第二步等待 Slave 返回 ACK 时,即为 after-sync,增强半同步(MySQL 5.7引入)。
下图是 MySQL 官方对于半同步复制的时序图,主库等待从库写入 relay log 并返回 ACK 后才进行Engine Commit。
notion image
 

并行复制

不论是偶发性的查询压力,还是备份,对备库延迟的影响一般是分钟级的,而且在备库恢复正常以后都能够追上来。
但是,如果备库执行日志的速度持续低于主库生成日志的速度,那这个延迟就有可能成了小时级别。而且对于一个压力持续比较高的主库来说,备库很可能永远都追不上主库的节奏
由于主备延迟的存在,所以在主备切换的时候,就相应的有不同的策略。
备库并行复制能力
 
1 什么情况下,备库的主备延迟会表现为一个45度的线段?
一般现在的数据库运维系统都有备库延迟监控,其实就是在备库上执行 show slave status,采集seconds_behind_master的值。
假设,现在你看到你维护的一个备库,它的延迟监控的图像类似图6,是一个45°斜向上的线段,你觉得可能是什么原因导致呢?你又会怎么去确认这个原因呢?
备库延迟
备库延迟
 
备库的同步在这段时间完全被堵住了。
产生这种现象典型的场景主要包括两种:
  • 一种是大事务(包括大表DDL、一个事务操作很多行);
  • 还有一种情况比较隐蔽,就是备库起了一个长事务,比如
begin; select * from t limit 1;
然后就不动了。
这时候主库对表t做了一个加字段操作,即使这个表很小,这个DDL在备库应用的时候也会被堵住,也不能看到这个现象。