今天要记录的就是,在一主多从架构下,主库故障后的主备切换问题。
A和A‘ 互为主备;从库B,C,D指向的是主库A。在主库A发生故障,需要将A’切换为主库;从库B,C,D也需要改接到A‘库中。
当把节点B设置为节点A’的从库的需要执行change master 命令:
CHANGE MASTER TO
MASTER_HOST=$host_name
MASTER_PORT=$port
MASTER_USER=$user_name
MASTER_PASSWORD=$password
MASTER_LOG_FILE=$master_log_name
MASTER_LOG_POS=$master_log_pos
MASTER_HOST、MASTER_PORT、MASTER_USER和MASTER_PASSWORD四个参 数,分别代表了主库A’的IP、端口、用户名和密码。
最后两个参数MASTER_LOG_FILE和MASTER_LOG_POS表示,要从主库的 master_log_name文件的master_log_pos这个位置的日志继续同步。而这个位置就是我们所 说的同步位点,也就是主库对应的文件名和日志偏移量。
原来节点B是主库A的从库,在从库B中记录的这两个参数的位点是主库A的位点,现在切换到A’,就需要找到原来主库A中的位点在现在主库A’的位点,所以在从库B中切换主库的过程需要先找同步位点。
在主库A’中找到的位点是是一个大概位置,不是准确的,因为考虑到切换过程中不能丢失数据,也就是binlog日志不能少;所以在找这个同步位点的时候,尽量稍微往前找,然后在找到的位点之后的事务中,判断哪些事务是在从库B中已经执行过的。
一种取同步位点的方法是这样的:
- 等待主库A‘把之前主库A的中转日志(relay log)全部同步完成
- 在A’上执行showmaster status命令,得到当前A’上最新的File 和 Position;
- 得到A发生故障的那个时刻T
- 通过mysqlbinlog工具解析A’的binlog,得到T时刻中A‘的binlog的位置
因为之前说过同步位点是不准确的,是尽量靠前的。所以在主库A发生故障前,从库B和备库A’会记录从主库A传过来的binlog,这一部分的binlog中的事务在两个库中都执行过的,所以同步位点之后的记载的事务,有可能是从库B和主库A’都有的,那么在从库B中执行同步位点之后的事务的时候,就可能会违反唯一值的限制从而抛出错误。
可以选择跳过这个事务,写法是:
set global sql_slave_skip_counter=1;
start slave;
遇到的错误可能不止一个,所以碰到错误就需要执行一次。
也可以选择设置slave_skip_errors参数,直接设置跳过指定的错误。在执行主备切换的过程中,经常发生的错误是(1062错误是插入数据时唯一键冲突)和(1032错误是删除数据时找不到行。),就可以把slave_skip_errors 设置为 “1032,1062”,这样在执行的过程中就会跳过这几个错误。这两种方法都是有一定问题的,而且也有点复杂。
GTID
GTID的全称是Global Transaction Identifier,也就是全局事务ID,是一个事务在提交的时候生成 的,是这个事务的唯一标识。它由两部分组成,格式是:
GTID=server_uuid:gno
其中server_uuid是这个实例在创建的时候生成的,是一个全局唯一的值,gno是事务提交后生成的值,每次提交之后gno都会递增1。
在MYSQL官方文档中GTID的格式是这样的:
GTID=source_id:transaction_id
source_id和server_uuid是同一个,这个transaction_id并不是事务的id,因为事务的id是在执行的时候创建的,并且每创建一个也会递增,如果某一个事务发生了回滚,这个事务id还是继续递增的;而gno只是在事务提交的时候才会分配。
所以gno是连续的,但是事务id并不一定是连续的。
GTID模式的启动也很简单,只需要在启动一个MySQL实例的时候,加上参数gtid_mode=on 和enforce_gtid_consistency=on就可以了。GTID的生成方式和变量gtid_next的值有关。
如果gtid_next=automatic,代表使用默认值,mysql会把server_uuid:gno赋给事务
- 在记录事务的时候,先记录一行SET@@SESSION.GTID_NEXT=‘server_uuid:gno’;
- 将这个生成的GTID的值加入到GTID集合中。
如果gtid_next= ‘chose_id’
- gtid_netx指定了某一个事务id,如果这个事务id是不存在与当前实例的GTID集合中的,那么接下来执行事务的gtid就是这个指定事务id的值,gno也不会递增;如果这个事务id是存在于当前实例的GTID集合中的,那么接下来执行的事务会被忽略。
注意,一个current_gtid只能给一个事务使用。这个事务提交后,如果要执行下一个事务,就要 执行set
命令,把gtid_next设置成另外一个gtid或者automatic。
每个MySQL实例都维护了一个GTID集合,用来对应“这个实例执行过的所有事务”。
就拿上面说过的,当找到的同步位点之后的事务是在从库B和主库A‘都执行过的事务,那么在从库B执行主库A’发过来的这一段binglo日志中的事务的时候,肯定会出现主键冲突的错误,这个时候可以:
set gtid_next=重复执行的事务id
beging;
commit;
set gtid_next=automatic;
start slave;
先给gtid_next指定一个事务id,这样在执行到重复事务的时候,因为这个事务gno已经维护在这个实例的GTID集合中了,所以会忽略掉这个事务,然后再将gtid_next设置为了默认值,不影响之后事务的执行。
使用GTID之后主备切换的时候,在实例B上执行change master命令,指定要切换的主库A’,建立连接,实例B将GTID集合set_B发送给给主库A’,主库A‘算出当前实例中GTID集合set_A和set_B的差集,也就是所有存在于set_a,但是不存在于set_b的GITD的 集合,判断A’本地是否包含了这个差集需要的所有binlog事务。
- 如果不包含,也就是set_a中存储的事务,已经在主库A’中找不到了,那就是说在主库A‘中有可能有一些数据的事务过程已经无法在从库B中复现了,所以直接返回错误。
- 如果全部包含,主库A’就在这些binlog日志中找出第一个在set_a中,但是不在set_b的事务发给B,继续将剩下的事务都发给B。
这里有一个逻辑就是,主库A‘发送给从库B的binlog日志必须是完整的,如果有一个日志是被删除了,那A’就拒绝发送日志给从库B。
这和基于位点的主备协议不同,基于位点的主备协议中,是备库主动指定需要从哪一个位置开始读,然后主库A’就发送哪一段日志,不会做日志的完整性判断。所以在主备切换中其实找位点这个工作,在实例A’内部就已经自动完成了。