随着业务量的增加,单机mysql可能不能再支持系统的运行,我们需要部署主备架构的mysql,主库可以提供读写功能,备库只能提供读。
Mysql主备的基本原理
在状态1时,A是主库,给客户端提供读写,B是备库,可以将A的更新都同步过来,保证AB的数据一致。当需要切换时,就切成状态2,这时B是主库了,给客户端提供读写,A是备库。
接下来我们看看节点A到节点B这条线的内部流程是什么样的,如下图:
根据上图我们来具体说说备库怎么通过binlog做数据同步。
首先备库通过一个命令与主库建立一个长连接,同时设置binlog文件名以及开始同步的起始位置。具体命令如下:
1
change master to master_host=$ip, master_user=$username, master_password=$pwd, master_port=$port, master_log_file='master-mysql-bin.000001', master_log_pos=200
在备库上执行start slave命令,这时在备库上会启动两个线程,就是上图中的sql_thread和io_thread,其中io_thread负责与主库连接。
- 主库按照备库设定的binlog文件名和起始位置,读取binlog,发送给备库。
- 备库拿到binlog文件后,写到本地文件,称为中转日志(relay log)。
- sql_thread读取中转日志,解析出日志里的命令,并执行。(后期sql_thread演变成了多线程复制的模式)
循环复制问题
通过上面我们大致了解了主备之间同步数据的流程,上面介绍的是M-S结构,但是生产上用的比较多的是双M结构,如下图:
上面AB节点中多了一条线,其实就是AB节点互为主备,这样在切换主节点的时候不需要修改主备关系。
但是上面这种双M结构有一个问题需要解决:A中一个更新生成binlog发送给B,B执行完之后也会生成binlog,会发送回A,这时A也需要执行这个binlog,如此下去,这个binlog就会在AB中不停复制下去,也就是循环复制。
解决上面的问题就是通过server_id,我们知道每个库的server_id必须不同,A生成binlog的时候会带有A的server_id,然后发送到备库B执行,备库B执行之后生成的binlog还是A的server_id,这样再发回A时,A发现server_id和自己的一样,就会丢弃这个binlog日志。
主备延迟
主备同步虽然可以达到数据最终一致性,但是这期间会有延迟,在延迟期间,主备上的数据就是不一致的。与数据同步的时间点主要有下面三个:
- 主库A执行完一个事务,写入binlog,我们把这个时刻记为T1。
- 之后传给备库B,备库B接收完binlog的时刻记为T2。
- 备库B执行完binlog的时刻记为T3。
主备延迟指的就是事务在主备上执行完的时间差,也就是T3-T1。在备库上执行show slave status 命令,我们在其中可以看到一项seconds_behind_master,这个表示主备延迟时间,单位是秒。
主备延迟原因
上面备库B接收binlog的过程在网络正常的时候其实是很快的,即T2-T1的时间很小,那主备延迟的主要原因就是备库执行接收来的binlog比较慢。所以说,主备延迟的最直接表现就是备库消费中转日志(relay log)的速度慢于主库生成binlog的速度。那具体原因主要有以下几方面:
- 有些部署条件下,备库机器的性能比主库的要差
有些人可能会认为备库上没有更新请求,所以可以使用差一点的机器。其实备库上也需要执行binlog,在IOPS上压力和主库其实是没有差别的。所以备库的机器性能最好和主库一致。当然现在大多采用双M互为主备的架构,这样的问题应该比较少。
- 备库上压力大
一般的想法是主库既然提供了写能力,那备库就应该提供读能力,然后将大量查询都放到备库上执行,甚至一些运营类报表查询都放到备库执行,这样备库的CPU压力就会很大,影响同步速度,造成主备延迟。这个问题的解决一般就是减少备库的查询压力或者部署多个从库分担查询压力。
- 大事务
大事务一般会执行很长时间,主库上执行完发到备库上执行一般也需要很长时间,这样就会造成主备延迟很长。比如一些归档类表,之前没有删除数据,空间快满时,然后就删除大量的数据,会导致事务执行很长时间,造成主备延迟。还有就是大表DDL,这个过程也是一个大事务的过程。
主备切换策略
由于有主备延迟的存在,我们在做主备切换时,就会有不同的策略,主要有下面两种:
可靠性优先策略
这个策略的具体切换过程如下:
- 判断备库B的seconds_behind_master的值,当小于某一个值时(比如5秒),我们就进行下一步,否则重试这一步。
- 设置主库A为只读模式。
- 等待备库B的seconds_behind_master的值变为0为止。
- 把备库B设置为读写模式。
- 把业务请求切换到新的主库B上。
从上面流程可以看到,这个过程是有不可用时间的,即从步骤3到步骤5完成切换这段时间,系统是不可用的,当然主要耗时是步骤3。虽然有不可用时间,但是这保证了数据的一致性,所以称之为可靠性优先策略。
可用性优先策略
在上面的流程中,如果我们把步骤4步骤5提到步骤2之后执行,这样系统的不可用时间就会很短,但是这时候主备之间数据还没完成同步,很有可能造成数据不一致的情况。
使用row格式的binlog时,数据不一致的情况很容易发现,但是使用statement或者mixed格式的binlog时,这种数据不一致的情况很可能悄悄发生,过了很久我们才有可能发现。
主备切换可用性优先有可能造成数据不一致,因此大多数情况都建议采用可靠性优先策略。毕竟对于数据服务来说,数据的可靠性还是优于可用性的。
备库并行复制策略
在前面介绍备库同步主库数据时,我们知道备库是通过sql_thread线程来将中转日志(relay log)解析并执行。如果是使用单线程,有可能造成备库应用日志不够快,造成主备延迟。在Mysql5.5版本之前,mysql只支持单线程复制,所以在主库并发高和TPS高的时候就有可能造成严重的主备延迟。
要把单线程改成多线程模型,其实就是把sql_thread改造成下面这个模型:
图中coordinator就是原来的sql_thread,不过它现在不直接更新数据了,只负责读取中转日志和分发事务,真正更新数据的是后面的worker线程。worker线程的个数,由参数slave_parallel_workers决定,一般把这个值设置成8~16(32核物理机的情况下)。
那coordinator怎么把事务分发给worker线程呢?最先想到可能就是轮询,事务1分发给worker1,事务2给worker2,但是由于是并行的,worker有可能比worker1先执行,如果两个事务更新的是同一行,就会导致数据错误。那一个事务中的多个更新语句可以分给多个worker执行吗?答案也是不行的,这样会造成客户端可能看到事务一半的更新结果。
所以coordinator在分发事务时必须满足下面两个条件:一是同一个事务必须放到一个worker执行,不能被拆开;二是更新同一行的事务必须放到同一个worker中,防止更新被覆盖。
Mysql5.6的并行复制策略
从Mysql5.6开始支持并行复制策略,支持的粒度是按库并行,即同一个库的事务都分到同一个worker中执行,这样就可以保证上面说的两个条件。
这个策略的并行效果,取决于压力模型。如果主库上有多个DB,并且每个DB的压力比较均衡,这个策略的效果就会很好。如果我们一个主库上只有一个DB,或者说数据库压力大部分集中于某个DB,这个策略就没有效果了。
MariaDB的并行复制策略
我们知道binlog提交的时候有一个优化,组提交(多个事务的binlog一起提交)。这里一个组中的事务满足下面两个特性:
- 同一个组中的事务一定不会修改同一行。
- 同一组中的事务在主库中能并发执行,在备库中也一定能并发执行。
MariaDB的并行复制策略就是利用了这个特性,同一个组提交的事务有相同的commit id,然后这些事务就被分到同一worker中执行。MariaDB的这个策略,目标就是模拟主库的并行模式。
但是这个策略有个问题,它并没有实现真正的模拟主库的并行模式。因为在主库中一组事务在提交时,下一组事务其实也是在执行中的状态。当一组事务提交后,下一组事务也能很快进行提交。而MariaDB这个策略中,一组事务分配给worker执行时,另一组事务只能处于等待状态,只有等前面的一组事务执行完成,下一组才开始执行。不过即便如此,这个策略仍然是一个很漂亮的创新,它对原系统的改造很少,实现也很优雅。
Mysql5.7的并行复制策略
在MariaDB并行复制实现之后,mysql5.7也提供了类似的功能,它由参数slave_parallel_type来控制并行复制策略:
- 配置为DATABASE,表示使用的是Mysql5.6的并行复制策略。
- 配置为LOGICAL_CLOCK,表示的就是类似MariaDB的策略,不过Mysql5.7这个策略,针对并行度做了优化。
主备切换
大多数互联网的应用场景都是读多写少,因此我们mysql一般都采用一主多从的架构
图中,虚线箭头表示的是主备关系,也就是A和A’互为主备,从库B,C,D指向的是主库A。一主多从的设置,一般多用于读写分离。主库负责所有的写和一部分读,从库负责其他的读请求。
在一主多从架构中,主库故障后,如何进行主备切换,下图说明了主备切换的结果:
图中主库A切换为A‘后,从库B,C,D也重新指向了A’。正是因为这个过程,一主多从的主备切换复杂性也相应增加了。
基于位点的主备切换
上面我们介绍过设置从库的命令是change master,这个命令后面主要有六个参数,其中master_host,master_port,master_user,master_password分别表示主库的ip,端口,用户名,密码。另外两个参数master_log_file和master_log_pos表示主库binlog文件名和同步开始的起始位置。
所以我们在主备切换时最重要的就是找到这个同步的位点。
考虑到切换过程中不能丢失数据,所以我们找位点的时候,总要找一个“稍微往前”的,然后再通过判断跳过那些在从库B上已经执行过的事务。
一种取同步的方法是这样的:
- 等待新主库A’把中转日志全部同步完成。
- 在新主库A’上执行show master status命令,得到A’当前最新的File和position。
- 取原主库A的故障的时刻T。
- 用mysqlbinlog工具解析A’的file,得到T时刻的位点。
1
mysqlbinlog File --stop-datetime=T --start-datetime=T
通过上面的步骤,我们可以的到A’在时刻T写入binlog的位置,然后把这个位置作为$master_log_pos,用在节点B的change master命令里。
GTID
GTID的全称是Global Transacation Identifier,也就是全局事务ID。是一个事务在提交的时候生成的,是这个事务的唯一标识。它由两部分构成,格式是:GTID = server_uuid:gno
其中 server_uuid是一个实例启动时自动生成的,是一个全局唯一的值;gno是一个整数,初始是1,每次事务提交时自动加1并分配给事务。
GTID的模式启动也很简单,只需要在启动实例的时候加上参数gtid_mode=on和enforce_gtid_consistency=on就可以了。