主备切换之GTID

一主多从结构

A和A' 互为主备,从库B、C、D指向A,当A出问题后,B、C、D也需要重新指向A',切换复杂性也随之增加。

基于位点的主备切换

将节点B设置为A'的从库时,需要执行chang master 命令:

CHANGE MASTER TO 
MASTER_HOST=$host_name       #A'的IP
MASTER_PORT=$port                 #A'的端口
MASTER_USER=$user_name       #A'的用户名
MASTER_PASSWORD=$password #A'的密码
MASTER_LOG_FILE=$master_log_name #A'的对应日志文件
MASTER_LOG_POS=$master_log_pos  # A'的日志同步开始的位置(即同步位点,也就是日志偏移量)

那么就需要:

  • 找同步位点
  • 方法:
    1. 等待A’ 把中转日志(relay log)全部同步完成;
    2. 在A’ 上执行 show master status 命令,得到A’ 上最新的 File 和 Position;
    3. 取 A 故障时刻 T;
    4. 用 mysqlbinlog 工具解析 A’ 的File,得到 T 时刻位点(end_log_pos);

    mysqlbinlog File --stop-datetime=T --start-datetime=T

  • 缺点: 并不准确,可能在 T 时刻,A执行完SQL插入一行R,并将binlog 传给了 A’ 和 B,传完瞬间主机down掉了。此时:
    1. B 由于同步binlog,存在R行;
    2. 在A’ 中也存在R行,日志 end_log_pos 就是R行SQL的位置;
    3. 在B执行 change master 命令时,指向 A’ 的File 文件位置与R行的SQL是同一位置,此时就会把R行的binlog再一次同步到B(这时就会报主键冲突的错)
  • 解决办法(MYSQL5.5及以下,切换任务的时候,主动跳过这些错误):
    1. 主动跳过一个事务
      set global sql_slave_skip_counter=1;
      start slave;
      
    2. 跳过指定错误
    • 设置 slave_skip_errors 参数
    • 1062 错误:插入数据时唯一键冲突
    • 1032 错误:删除数据找不到行
    slave_skip_error = "1032,1062"
    

GTID(全局事务ID,MYSQL 5.6版本引入)

上面方法虽然可以解决问题,但是操作复杂,易出错。 GTID:Global Transaction Identifier,事务提交的时候生成,是事务唯一标识。

  • 组成:GTID=source_id:transaction_id(官方文档表示)
    • source_id: 一个实例第一次启动时自动生成的,是一个全局唯一的值,通常用server_uuid;
      • server_uuid:是一个32字节+1字节(/0)的字符串。MySQL 启动时会调用INIT_SERVER_AUTO_OPTION() 读取 AUTO.CNF 文件获取该值,如果没有读取到则调用 GENERATE_SERVER_UUID()生成一个SERVER_ID。
    • transaction_id:事务唯一标志(也可用 gno 表示),是一个整数,初始值为 1,每次提交事务时分配给这个事务,并加 1(并非事务执行过程中分配的事务 id,事务 id 在事务回滚时也会增加,为区别执行过程中的事务id,后面都用 gno 表示事务唯一标志)
  • 启动: 启动 MYSQL 实例时,加上参数 gtid_mode=on 和 enforce_gtid_consistency=on 即可。
GTID 的两种生成方式:
  1. gtid_next = automatic,代表使用默认值。
    • 记录 binlog 时,先记录一行 SET@@SESSION.GTID_NEXT= 'server_uuid:gno';
    • 把这个 GTID 加入本实例的 GTID 集合。
  2. gtid_next 是指定的 GTID 值,如set gtid_next='current_gtid',则有两种可能
    • current_gtid 已存在于实例的 GTID 集合中,则忽略接下来的执行的事务;
    • 不存在则将 current_id 分配给接下来要执行的事务,无需给这个事务生成新的 GTID。

    一个 current_id 只能分配给一个事务,执行下一个事务时,需要使用 set 命令,设置 gtid_next 为另一个 gtid 或 automatic。

基于 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 协议。

在B上执行 start slave 命令,取 binlog 的逻辑:
1. B 指定 A',基于主备协议建立连接
2. 实例B把 set_b 发给 A'(set_b 为 B 的 GTID 集合)
3. A' 算出 set_a 与 set_b 的差集,也就是存在于 set_a 但不存在于 set_b 的 GTID 的集合。判断 A' 本地是否包含了这个差集需要的所有 binlog 事务。
a. 如果不包含,表示 A' 已经把 B 需要的 binlog 给删掉了,直接返回错误;
b. 如果确认全部包含,A' 从自己的 binlog 文件里面,找出第一份不在 set_b 的事务,发给B;
4. 从该事务开始往后读文件,按顺序取 binlog 发给 B 去执行。
  • 两个主备关系的不同点:
    • 基于 GTID 的主备关系,只要建立主备关系,主库发给备库的日志就必须保证是完整的,所以如果 B 需要的日志已经不存在,A' 就会拒绝把日志发给 B
    • 基于位点的主备关系中,位点是由备库决定,备库指定哪个位点,主库就发哪个位点,不做日志完整性判断。

GTID 和在线 DDL

问题:业务高峰期,在线增加索引,双 M 结构中,备库Y执行的 DDL 语句也会传给主库X,需要通过 set sql_log_bin = off 关掉binlog,避免对主库X造成影响。但是,这时 binlog 没有记录下这个更新,会导致数据和日志不一致。使用 GTID 则可以避免这种尴尬。

X、Y都打开了 GTID 模式,主备切换流程:

  • 在 X 上执行 stop slave;
  • 在 Y 执行 DDL 语句。ps:这里不需要关闭 binlog
  • 执行完后,查出这个 DDL 语句对应的 GTID ,并记为 server_uuid_Y:gno
  • 到 X 上执行以下语句序列:
    set GTID_NEXT="server_uuid_Y:gno"
    begin;
    commit;
    set gtid_next=automatic;
    start slave;
    

这样,既可以让实例 Y 的更新有 binlog 记录,同时也可以确保不会在实例 X 执行这条更新。

  • 最后,执行完主备切换,照着上述流程在执行一遍即可。

思考:

你在 GTID 模式下设置主从关系的时候,从库执行 start slave 命令后,主库发现需要的 binlog 已经被删除掉了,导致主备创建不成功。这种情况下,你觉得可以怎么处理呢?

  • 业务允许主备不一致的情况,
    1. 可以在主库先 show global variables like 'gtid_purged'
    2. 然后在从库 执行 set global gtid_purged ='' ,指定从库开始同步的 gtid 位置,binlog 缺失的那部分数据,在从库上就会丢失,造成主从不一致。
  • 主备需要一致的情况
    1. 只有一台从库,就只能重新搭建从库
    2. 有其它从库,将保留有全量 binlog 的从库,设置为缺失 binlog 从库的主库(级联复制)
    3. binlog 有备份的话,在从库应用缺失的 binlog 再 start slave

如果数据库已经完成了很多事物,GTID 集合是不是会很大?GTID 是如何从 binlog 解析出所有事务的?

  • binlog 文件开头,有一个 Previous_gtids,用于记录生成这个 binlog 的时候,实例的 Executed_gtid_set,所以启动的时候只需要解析最后一个文件;
  • 由于有这个 Previous_gtids 可以快速定位 GTID 在哪个文件中。

Larwas
请先登录后发表评论
  • latest comments
  • 总共0条评论