27 | 主库出问题了,从库怎么办?
在前面的文章中,介绍了MySQL主备复制的基础结构,但这些都是一主一备的结构。 大多数的互联网应用场景都是读多写少,因此你负责的业务,在发展过程中很可能先会遇到读性能的问题。而在数据库层解决读性能问题,就要涉及到接下来两篇文章要讨论的架构:一主多从。
今天我们就先聊聊一主多从的切换正确性。然后,我们在下一篇文章中再聊聊解决一主多从的查询逻辑正确性的方法。如图所示,就是一个基本的一主多从结构。
图中,虚线箭头表示的是主备关系,也就是A和A’互为主备, 从库B、C、D指向的是主库A。一主多从的设置,一般用于读写分离,主库负责所有的写入和一部分读,其他的读请求则由从库分担。
今天我们要讨论的就是,在一主多从架构下,主库故障后的主备切换问题。如图就是主库发生故障,主备切换后的结果。
相比于一主一备的切换流程,一主多从结构在切换完成后,A’会成为新的主库,从库B、C、D也要改接到A’。正是由于多了从库B、C、D重新指向的这个过程,所以主备切换的复杂性也相应增加了。
接下来,我们再一起看看一个切换系统会怎么完成一主多从的主备切换过程。
基于位点的主备切换
这里,我们需要先来回顾一个知识点。 当我们把节点B设置成节点A’的从库的时候,需要执行一条change master命令,就不可避免地要设置位点的两个参数。
MASTER_LOG_FILE和MASTER_LOG_POS表示,要从主库的master_log_name文件的master_log_pos这个位置的日志继续同步。而这个位置就是我们所说的同步位点,也就是主库对应的文件名和日志偏移量。
但是这两个参数到底应该怎么设置呢?原来节点B是A的从库,本地记录的也是A的位点。但是相同的日志,A的位点和A’的位点是不同的。因此,从库B要切换的时候,就需要先经过“找同步位点”这个逻辑。
主备切换时,由于找不到精确的同步位点,一般选择直接跳过这些错误,采用这种方法来创建从库和新主库的主备关系。
GTID:全局事务ID
通过sql_slave_skip_counter跳过事务和通过slave_skip_errors忽略错误的方法,虽然都最终可以建立从库B和新主库A’的主备关系,但这两种操作都很复杂,而且容易出错。所以,MySQL 5.6版本引入了GTID,彻底解决了这个困难。
GTID如何解决找同步位点这个问题呢? GTID的全称是Global Transaction Identifier,也就是全局事务ID,是一个事务在提交的时候生成的,是这个事务的唯一标识。它由两部分组成,格式是:
GTID=source_id:transaction_id
其中:
- source_id是一个实例第一次启动时自动生成的,是一个全局唯一的值;
- transaction_id是一个整数,初始值是1,每次提交事务的时候分配给这个事务,并加1。
建议使用gno代替transaction_id。因为在MySQL里面transaction_id就是指事务id,事务id是在事务执行过程中分配的,如果这个事务回滚了,事务id也会递增,而gno是在事务提交的时候才会分配。
从效果上看,GTID往往是连续的,因此我们用gno来表示更容易理解。 GTID模式的启动也很简单,我们只需要在启动一个MySQL实例的时候,加上参数gtid_mode=on和enforce_gtid_consistency=on就可以了。
在GTID模式下,每个事务都会跟一个GTID一一对应。这个GTID有两种生成方式,而使用哪种方式取决于session变量gtid_next的值。
- 如果gtid_next=automatic,代表使用默认值。这时,MySQL就会把source_id:gno分配给这个事务。
- a. 记录binlog的时候,先记录一行SET@@SESSION.GTID_NEXT=‘source_id:gno’;
- b. 把这个GTID加入本实例的GTID集合。
- 如果gtid_next是一个指定的GTID的值,比如通过set gtid_next='current_gtid’指定为current_gtid,那么就有两种可能:
- a. 如果current_gtid已经存在于实例的GTID集合中,接下来执行的这个事务会直接被系统忽略;
- b. 如果current_gtid没有存在于实例的GTID集合中,就将这个current_gtid分配给接下来要执行的事务,也就是说系统不需要给这个事务生成新的GTID,因此gno也不用加1。
注意,一个current_gtid只能给一个事务使用。这个事务提交后,如果要执行下一个事务,就要执行set命令,把gtid_next设置成另外一个gtid或者automatic。
这样,每个MySQL实例都维护了一个GTID集合,用来对应“这个实例执行过的所有事务”。
基于GTID的主备切换
现在,我们已经理解GTID的概念,再一起来看看基于GTID的主备复制的用法。 在GTID模式下,备库B要设置为新主库A’的从库的语法如下:
CHANGE MASTER TO
MASTER_HOST=$host_name
MASTER_PORT=$port
MASTER_USER=$user_name
MASTER_PASSWORD=$password
master_auto_position=1
其中,master_auto_position=1就表示这个主备关系使用的是GTID协议。可以看到,前面让我们头疼不已的MASTER_LOG_FILE和MASTER_LOG_POS参数,已经不需要指定了。
我们把现在这个时刻,实例A’的GTID集合记为set_a,实例B的GTID集合记为set_b。接下来,我们就看看现在的主备切换逻辑。
我们在实例B上执行start slave命令,取binlog的逻辑是这样的:
- 实例B指定主库A’,基于主备协议建立连接。
- 实例B把set_b发给主库A’。
- 实例A’算出set_a与set_b的差集,也就是所有存在于set_a,但是不存在于set_b的GITD的集合,判断A’本地是否包含了这个差集需要的所有binlog事务。
- a. 如果不包含,表示A’已经把实例B需要的binlog给删掉了,直接返回错误;
- b. 如果确认全部包含,A’从自己的binlog文件里面,找出第一个不在set_b的事务,发给B;
- 之后就从这个事务开始,往后读文件,按顺序取binlog发给B去执行。
其实,这个逻辑里面包含了一个设计思想:在基于GTID的主备关系里,系统认为只要建立主备关系,就必须保证主库发给备库的日志是完整的。因此,如果实例B需要的日志已经不存在,A’就拒绝把日志发给B。
这跟基于位点的主备协议不同。基于位点的协议,是由备库决定的,备库指定哪个位点,主库就发哪个位点,不做日志的完整性判断。
基于上面的介绍,我们再来看看引入GTID后,一主多从的切换场景下,主备切换是如何实现的。
由于不需要找位点了,所以从库B、C、D只需要分别执行change master命令指向实例A’即可。其实,严谨地说,主备切换不是不需要找位点了,而是找位点这个工作,在实例A’内部就已经自动完成了。但由于这个工作是自动的,所以对HA系统的开发人员来说,非常友好。
之后这个系统就由新主库A’写入,主库A’的自己生成的binlog中的GTID集合格式是: server_uuid_of_A’:1-M。如果之前从库B的GTID集合格式是 server_uuid_of_A:1-N, 那么切换之后GTID集合的格式就变 成了server_uuid_of_A:1-N, server_uuid_of_A’:1-M。 当然,主库A’之前也是A的备库,因此主库A’和从库B的GTID集合是一样的。这就达到了我们预期。