一句话概括:
通过binlog完成主备同步,实现最终一致性,binlog有三种格式:statement、row、mixed。
涉及到的线程有主库上的dump_thread、备库上的io_thread、sql_thread;
涉及到的日志有binlog、relaylog。
如下图所示就是基本的主备切换流程。主库A负责处理客户端的读写操作,备库B只需要将A的更新都同步过来,到本地执行即可。这样可以保持节点B和A的数据是相同的。
当需要切换时,就切成状态2,此时客户端读写访问的都是备库B,而主库A成为节点B的备库。
注:
备库一般都设置为readonly状态,可以防止误操作及切换过程中出现双写造成主备不一致。还可以通过readonly状态来判断节点的角色。readonly不会影响主备同步,因为readonly对超级用户(root权限)是无效的,用于同步更新的线程就属于root权限。
图1:MySQL主备切换流程
下图所示为节点A到节点B的主备切换流程图:
图2:MySQL主备流程图
从图中可见,主库A上有三个线程:处理客户端读写请求的线程、bg_thread后台线程用于进行持久化、dump_thread用于传递binlog给备库;
备库B上有两个线程:io_thread用于处理与主库之间的网络连接和IO操作、sql_thread用于读取中转日志,解析出日志中的命令并执行。
一个事务日志同步的完整过程是这样的:
(1)在备库B上通过 change master
命令,设置主库A的IP、端口号、用户名、密码,以及要从哪个位置开始请求binlog,这个位置包含的文件名和日志偏移量offset;(此时只是设立建立连接需要的信息,包括IP、port等,还未真正发起连接,连接的建立需要后面的io_thread线程来处理)
(2)在备库B上执行 start slave
命令,此时备库B会启动两个线程,即:io_thread
和 sql_thread
。 其中 io_thread
负责与主库建立网络连接;
(3)主库A校验用户名、密码后,开始按照备库B传过来的位置,从本地读取binlog,并将其发送给备库B;
(4)备库B接收到binlog后,将其写到本地文件,称为中转日志 relay log
;
(5)备库B上的 sql_thread
线程读取中转日志 relay_log,解析出日志中的命令,并执行。
注:在后续的MySQL版本中,sql_thread 演化为多线程,但处理流程是不变的。
从上文中的主备同步流程中可以看到,其中关键的文件是binlog,binlog有三种格式,用于备库通过binlog完成数据同步。
(1)statement;
(2)row;
(3)mixed(statement 与 row 两种格式的混合)
为说明binlog的三种格式的区别,先创建一个表t,随后分别在 STATEMENT 和 ROW 格式下执行 delete 操作,观察生成的binlog中内容的异同。
首先,执行以下语句在主节点A上创建一个表并初始化一部分数据:
mysql> CREATE TABLE `t` (
`id` int(11) NOT NULL,
`a` int(11) DEFAULT NULL,
`t_modified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `a` (`a`),
KEY `t_modified`(`t_modified`)
) ENGINE=InnoDB;
insert into t values(1,1,'2018-11-13');
insert into t values(2,2,'2018-11-12');
insert into t values(3,3,'2018-11-11');
insert into t values(4,4,'2018-11-10');
insert into t values(5,5,'2018-11-09');
创建后的表t中的数据:
mysql> select * from t;
+----+------+---------------------+
| id | a | t_modified |
+----+------+---------------------+
| 1 | 1 | 2018-11-13 00:00:00 |
| 2 | 2 | 2018-11-12 00:00:00 |
| 3 | 3 | 2018-11-11 00:00:00 |
| 4 | 4 | 2018-11-10 00:00:00 |
| 5 | 5 | 2018-11-09 00:00:00 |
+----+------+---------------------+
5 rows in set (0.01 sec)
然后,设置MySQL的binlog格式为STATEMENT:
mysql> set global binlog_format = "STATEMENT";
Query OK, 0 rows affected (0.00 sec)
mysql> show global variables like "binlog_format";
+---------------+-----------+
| Variable_name | Value |
+---------------+-----------+
| binlog_format | STATEMENT |
+---------------+-----------+
1 row in set (0.00 sec)
执行 delete 语句:
mysql> delete from t /*comment*/ where a>=4 and t_modified<='2018-11-10' limit 1;
Query OK, 1 row affected (0.01 sec)
可以看到,在statement格式下 binlog会原封不动的记录下执行的SQL命令,甚至连注释也一并记录了。
执行 show warnings;
命令 查看delete语句的执行结果:
可以看到这个命令可能是 unsafe
的,这是因为delete带limit操作,可能会导致主备数据不一致的情况,例如主库上使用的是索引a,那么根据索引a找到的第一个满足条件的行是 a=4 这一行,而如果备库使用了索引 t_modified,那么删除的就是 a=5 这一行。
而如果使用 binlog_format=‘ROW’ 的格式,则不会出现上述问题,这是因为 ROW格式下 binlog中不会存储SQL语句的原文,而是替换成了两个event:Table_map 和 Delete_rows。其中 Table_map用于说明接下来的操作的表是test库的表t,Delete_rows用于定义删除的行为。
使用 mysqlbinlog
工具解析ROW格式的binlog,解析结果如下图所示,可见 当binlog_format使用row格式的时候,binlog里面记录了真实删除行的 主键id,这样binlog传到备库去的时候,就肯定会删除id=4的行,不会有主备删除不同行的问题。
(statement格式记录SQL语句本身,row格式记录SQL语句具体到对每一行的操作,row格式比statement格式增加了确定性。)
因为有些statement格式的binlog可能会导致主备不一致,所以需要使用row格式。但是row格式的缺点是很占空间,例如当使用delete语句删除10万行数据时,用statement的话只需要在binlog中记录一条SQL语句,占用几十个字节的空间,而使用row格式的binlog,就要把这10万行记录都写入到binlog中,这样做不仅会占用更大的空间,也会由于写binlog耗费IO资源而影响执行速度。
因此MySQL采取了一种折中方案,即mixed格式的binlog,mixed格式的意思是:MySQL会自行判断这条SQL语句是否可能会引起主备不一致,如果有可能,则使用row格式,否则就使用statement格式。
但目前越来越多的场景要求把MySQL的binlog格式设置为row,row格式的binlog还具有很多优点,例如:恢复数据(当误删除了某些数据时,直接将binlog中的delete操作改为对应的insert操作重新执行即可,反之亦然)。
因此,线程的MySQL如果设置的binlog格式是statement的话,那么基本可以认定为是一个不合理的设置,至少应该将其设置为mixed格式,而最佳的设置应该是row格式。
实际生产中使用的比较多的主备结构是“双M结构”,如下图所示,即 节点A和节点B之间总是互为主备关系,这样在切换的时候就不需要再修改主备关系。
但双M结构有一个问题需要解决,就是binlog的循环复制,解决方法为在binlog中记录一条命令第一次执行时所在实例的 server_id
,主备库的server_id必须配置为不同,当收到另一个库发来的binlog时,先判断server_id是否跟自己相同,如是则直接将收到的binlog丢弃,以此来避免发生循环复制。
一句话概括:
只有保证足够小的主备延迟,才能保证足够高的可用性。
主备切换策略有两种:基于可靠性、基于可用性。一般选择基于可靠性,存在一段时间整个系统不可用,但可靠。
如果是异常下主备切换,主备延迟越大,系统对外不可用的时间越长(需要备库从主库拉取数据),如果主备延迟超过阈值,将导致系统持续不可用。
(可用性:系统持续对外提供服务的能力,一般用几个9表示,例如 99.999% 表示的是全年99.999%的时间可用;)
(可靠性:系统保证数据不丢失的能力。)
正常情况下,只要主库执行更新生成的所有binlog,都可以传到备库并被正确的执行,备库就能达到跟主库一致的状态,这就是 最终一致性。
但是,MySQL要提高可用性,只有最终一致性是不够的,还需要考虑一些其他的因素,例如:主备延迟。
所谓主备延迟,是指同一个事务在备库执行完成的时间 和 主库执行完成时间之间的差值。
与主备间的数据同步有关的时间点包括以下三个:
(1)主库A执行完一个事务,写入binlog,记这个时刻为 T1;
(2)之后将binlog传递给备库B,记备库B接收完成这个binlog的时刻为 T2;
(3)备库B接收到binlog后开始执行并将数据写入完成,记这个时刻为 T3。
三个时间点将主备同步binlog的过程分为两个时间段,即:主库通过网络连接将binlog传递给备库并在备库上接收完成的时间、备库收到binlog后在本地解析并执行binlog的时间。
其中,通过“网络连接”这段时间是MySQL主备节点非可控的,且在网络正常连接的情况下,日志从主库传给备库的时间是很短的(主备之间一般都是通过局域网连接)。也就是说,主备延迟的主要来源是:备库接收完binlog后执行完这个事务所耗费的时间。
可能导致备库执行binlog所需的时长增加的因素有:
(1)主备节点所部署的主机性能差异;(解决方法:主备的主机做对称部署)
(2)备库的业务压力(读操作)更大;(解决方法:采用一主多从)
(3)一次性执行的事务过大,例如几种的delete删除太多数据。(解决方法:避免一次性集中操作大量数据,避免在高峰期操作归档、删除历史数据这类操作)
由于主备延迟的存在,切换策略有不同的选择,主要分为:
可靠性优先策略、可用性优先策略。
在实际应用中,更建议使用“可靠性优先策略”,毕竟保证数据正确,应该是数据库服务器的底线,在这个基础上,通过减少主备延迟,提升系统性能的可用性。
(1)判断备库 B 现在的 seconds_behind_master,如果小于某个值(比如 5秒)继续下一步,否则持续尝试这一步;
(2)把主库 A 改成只读状态,即把 readonly 设置为 true;
(3)判断备库 B 的 seconds_behind_master 的值,直到这个值变成0为止;
(4)把备库B改成可读写状态,也就是把 readonly 设置为 false;
(5)把业务请求切换到备库 B。
其中,seconds_behind_master
用于表示当前备库延迟了多少秒,可通过在备库上执行 show slave status
命令查看。
当主库将binlog传给从库时,会在其中带上主库写入时的时间,备库收到后将其与当前的系统时间做差值,得到 seconds_behind_master 的值,也就是 (T3 - T1)的值。
下图所示为MySQL可靠性优先主备切换流程演示图:(其中SBM为seconds_behind_master的缩写)
以图中双主多从的结构为例,其中 A 和 A’ 互为主备,从库 B、C、D 指向的是主库 A。主库A负责写操作,其他从库负责读操作。
相比于一主一备的切换流程(只有A和A‘),一主多从结构在切换完成后,A’ 成为新的主库,从库B、C、D也要重新接到A‘上,这个过程中有一个问题需要解决:从库上执行 change master
时的两个参数 MASTER_LOG_FILE
和 MASTER_LOG_POS
,即新主库的 同步位点(主库对应的文件名和日志偏移量) 如何确定。
有两种方式:
(1)基于位点:
A’上的binlog中可以解析出A发生故障时刻,从那之后开始同步即可,但需要配置跳过错误,slave_skip_errors,操作复杂且容易出错。
因此MySQL 5.6版本引入了GTID,彻底解决了 B -> A’ 的困难。
(2)GTID:
GTID = Global Transaction Identifier,“全局事务ID”