基于binlog位点主从复制痛点分析
痛点 1:首次开启主从复制的步骤复杂
- 第一次开启主从同步时,要求从库和主库是一致的。
- 找到主库的 binlog 位点。
- 设置从库的 binlog 位点。
- 开启从库的复制线程。
痛点 2: 恢复主从复制的步骤复杂 - 找到从库复制线程停止时的位点。
- 解决复制异常的事务。无法解决时就需要手动跳过指定类型的错误,比如通过设置 slave_skip_errors=1032,1062。当然这个前提条件是跳过这类错误是无损的。(1062 错误是插入数据时唯一键冲突;1032 错误是删除数据时找不到行)
不论是首次开启同步时需要找位点和设置位点,还是恢复主从复制时,设置位点和忽略错误, 这些步骤都显得过于复杂,而且容易出错 。 所以 MySQL 5.6 版本引入了 GTID,彻底解决了这个困难。
基于全局事务标识符(GTID)复制
官网: https://dev.mysql.com/doc/refman/8.0/en/replication-gtids.html
GTID是一个基于原始mysql服务器 生成的一个已经被成功执行的全局事务ID,它由服务器ID以及事务ID组合而成。这个全局事务ID不仅仅在原始服务器器上唯一,在所有存在主从关系 的mysql服务器上也是唯一的。正是因为这样一个特性使得mysql的主从复制变得更加简单,以及数据库一致性更可靠。
- 一个GTID在一个服务器上只执行一次,避免重复执行导致数据混乱或者主从不一致。
- GTID用来代替传统复制方法,不再使用MASTER_LOG_FILE+MASTER_LOG_POS开启复制。而是使用MASTER_AUTO_POSTION=1的方式开始复制。
- 在传统的replica端,binlog是不用开启的,但是在GTID中replica端的binlog是必须开启的,目的是记录执行过的GTID(强制)。
GTID 的优势
- 更简单的实现 failover,不用以前那样在需要找位点(log_file 和 log_pos)。
- 更简单的搭建主从复制。
- 比传统的复制更加安全。
- GTID 是连续的没有空洞的,保证数据的一致性,零丢失。
GTID结构
GTID表示为一对坐标,由冒号(:)分隔,如下所示:
GTID = source_id:transaction_id
-
source_id标识source服务器,即源服务器唯一的server_uuid,由于GTID会传递到replica,所以也可以理解为源ID。
-
transaction_id是一个序列号,由事务在源上提交的顺序决定。序列号的上限是有符号64位整数(2^63-1)
例如,最初要在UUID为3E11FA47-71CA-11E1-9E33-C80AA9429562的服务器上提交的第23个事务具有此GTID3E11FA47-71CA-11E1-9E33-C80AA9429562:23
GTID集合是由一个或多个GTID或GTID范围组成的集合。来自同一服务器的一系列gtid可以折叠成单个表达式,如下所示:
3E11FA47-71CA-11E1-9E33-C80AA9429562:1-5
源自同一服务器的多个单一gtid或gtid范围也可以包含在单个表达式中,gtid或范围以冒号分隔,如下例所示:
3E11FA47-71CA-11E1-9E33-C80AA9429562:1-3:11:47-49
GTID集合可以包括单个GTID和GTID范围的任意组合,也可以包括来自不同服务器的GTID。
2174B383-5441-11E8-B90A-C80AA9429562:1-3, 24DA167-0C0C-11E8-8442-00059A3C7B00:1-19
GTID存储在mysql数据库中名为gtid_executed的表中。该表中的一行包含它所代表的每个GTID或GTID集合的起始服务器的UUID,以及该集合的开始和结束事务id。

GTID工作原理
主库计算主库 GTID 集合和从库 GTID 的集合的差集,主库推送差集 binlog 给从库。
当从库设置完同步参数后,主库 A 的 GTID 集合记为集合 x,从库 B 的 GTID 集合记为 y。从库同步的逻辑如下:

- 从库 B 指定主库 A,基于主备协议建立连接。
- 从库 B 把集合 y 发给主库 A。
- 主库 A 计算出集合 x 和集合 y 的差集,也就是集合 x 中存在,集合 y 中不存在的 GTID 集合。比如集合 x 是 1~100,集合 y 是 1~90,那么这个差集就是 91~100。这里会判断集合 x 是不是包含有集合 y 的所有 GTID,如果不是则说明主库 A 删除了从库 B 需要的 binlog,主库 A 直接返回错误。
- 主库 A 从自己的 binlog 文件里面,找到第一个不在集合 y 中的事务GTID,也就是找到了 91。
- 主库 A 从 GTID = 91 的事务开始,往后读 binlog 文件,按顺序取 binlog,然后发给 B。从库 B 的 I/O 线程读取 binlog 文件生成 relay log,SQL 线程解析 relay log,然后执行 SQL 语句。
GTID 同步方案和位点同步的方案区别是:
- 位点同步方案是通过人工在从库上指定哪个位点,主库就发哪个位点,不做日志的完整性判断。
- 而 GTID 方案是通过主库来自动计算位点的,不需要人工去设置位点,对运维人员友好。
GTID的配置
1) 修改主库配置
修改主库的配置文件
#GTID:
#启用全局事务标识符(GTID)模式
gtid_mode=on
# 强制GTID的一致性。这意味着在执行事务时,MySQL将确保所有涉及的服务器都使用相同的GTID集。
enforce_gtid_consistency=on
2)修改从库配置
修改从库配置文件
#GTID:
gtid_mode=on
enforce_gtid_consistency=on
从节点设置主库信息
# 从库配置同步参数
mysql> CHANGE MASTER TO
> MASTER_HOST = host,
> MASTER_PORT = port,
> MASTER_USER = user,
> MASTER_PASSWORD = password,
> MASTER_AUTO_POSITION = 1;
Or from MySQL 8.0.23:
mysql> CHANGE REPLICATION SOURCE TO
> SOURCE_HOST = host,
> SOURCE_PORT = port,
> SOURCE_USER = user,
> SOURCE_PASSWORD = password,
> SOURCE_AUTO_POSITION = 1;
SOURCE_AUTO_POSITION = 1: 这告诉从服务器使用自动位置跟踪功能,以便它可以自动从主服务器获取最新的二进制日志事件,而无需手动指定位置。
基于GTID主从复制示例
文档:https://dev.mysql.com/doc/refman/8.0/en/replication-gtids-howto.html
在前面基于binlog日志主从复制的mysql服务上配置GTID复制。
- 同步所有的mysql服务器
在主从服务器上都执行下面的命令:
# 设置MySQL服务器的全局只读模式
mysql> SET @@GLOBAL.read_only = ON;
注意:只有在使用已经在进行复制而不使用gtid的服务器时才需要此步骤。对于新服务器,请继续执行步骤3。

-
停止所有的服务器
docker stop mysql-source mysql-replica1 mysql-replica2
-
主从节点都启用GTID
修改custom.cnf
# 启用GTID
gtid_mode=ON
enforce-gtid-consistency=ON
启动主从节点
docker start mysql-source mysql-replica1 mysql-replica2
4)从节点配置基于GTID的自动定位
进入从节点,执行下面命令:
mysql> stop replica;
mysql> change replication source to source_host='192.168.65.185', source_user='fox', source_password='123456', source_port=3307,source_auto_position=1;

-
开启从库复制,并禁用只读模式
开启从库复制
mysql> start replica;
只有在步骤1中将服务器配置为只读时,才需要执行以下步骤。要允许服务器再次开始接受更新
mysql> SET @@GLOBAL.read_only = OFF;
查看从库状态是否正常
mysql> show replica status \G

主从切换演练
场景1: 模拟主库down机、从库1数据同步完成、从库2数据未同步完成
1)从库2停止复制
mysql> stop replica;
2)主库创建测试数据
INSERT INTO `test`.`user` VALUES (12, 'fox', NULL, NULL, NULL);
- 查询数据
从库1

从库2

很显然,从库1同步了最新数据,比从库2数据新
场景2:将主库宕机,从库1升级为主库、从库2切换主库为从库1(新的主库),观察从库2是否同步未完成的事务
1)停止主库
docker stop mysql-source
2)设置新主库
设置replica1 为replica2的主库,因为replica1的数据是完整的。
按照普通复制方法,需要计算主库的log_pos和从库设置成主库的log_pos,可能出现错误
因为同一事务的GTID在所有节点上的值一致,那么根据replica2当前停止点的GTID就能定位到要主库的GTID,所以直接在replica2上执行change即可
# replica1上创建复制用户
CREATE USER 'fox'@'%' IDENTIFIED WITH mysql_native_password BY '123456';
GRANT REPLICATION SLAVE ON *.* TO 'fox'@'%';
flush privileges;
# replica2上执行 从replica1查询
mysql> stop replica;
mysql> change replication source to source_host='192.168.65.185',source_port=3308,source_user='fox',source_password='123456',source_auto_position=1;
mysql> start replica;

查询同步结果

场景3:模拟从库删除测试表,主库对表进行插入操作。观察从库复制是否报错
1)replica1删除test.user表,主库插入新记录
# 从库1 删除user表
drop table test.user;
# 主库插入新记录
INSERT INTO `test`.`user` VALUES (14, 'AAA', NULL, NULL, NULL);
-
查看从库同步情况
从库1执行
mysql> show replica status\G

报错信息:事务aac92b21-b6a4-11ee-bab5-0242ac120002:6执行失败
Coordinator stopped because there were error(s) in the worker(s). The most recent failure being: Worker 1 failed executing transaction 'aac92b21-b6a4-11ee-bab5-0242ac120002:6' at master log mysql-bin.000008, end_log_pos 1335. See error log and/or performance_schema.replication_applier_status_by_worker table for more details about this failure or others, if any.
3)在主库继续进行其他事务,观察gitd是否复制成功
# 主库插入新记录
INSERT INTO `test`.`user` VALUES (15, 'BBB', NULL, NULL, NULL);
从库状态

可以看出从库复制中断 (注意:删除了表,无法插入)
4)复制中断修复:采用从库跳过错误事务修复
因为从库user表已经删了(user表中部分数据不是利用gtid复制过去的),先从主库将表数据拷贝到从库
获取从库最新状态

从库执行
# 1.停止从库1复制进程
mysql> stop replica;
# 2.设置事务号,事务号从 Retrieved_Gtid_Set 获取,在session里设置gtid_next,即跳过这个GTID
# 注意,选择跳过出现错误的事务
mysql> SET @@SESSION.GTID_NEXT= 'aac92b21-b6a4-11ee-bab5-0242ac120002:7';
# 3.设置空事物
mysql> BEGIN; COMMIT;
# 4.恢复自增事物号
mysql> SET SESSION GTID_NEXT = AUTOMATIC;
# 5.启动从库1复制进程
mysql> start replica;
# 再次查询会发现主库数据已经同步过来了
mysql> select * from test.user;

如果需要一次跳过多条,找出需要跳过的gtid,批量执行:
stop replica;
SET @@SESSION.GTID_NEXT= 'd58e6bad-ef3e-11ee-8285-0242ac120003:4';begin;commit;
SET @@SESSION.GTID_NEXT= 'd58e6bad-ef3e-11ee-8285-0242ac120003:5';begin;commit;
SET @@SESSION.GTID_NEXT= 'd58e6bad-ef3e-11ee-8285-0242ac120003:6';begin;commit;
SET SESSION GTID_NEXT = AUTOMATIC;
start replica;
完整的演示步骤:
1.从库删除user表
mysql> drop table user;
2.主库连续删除两条数据
mysql> select * from user;
+----+--------+-----------------+--------------+------------+
| id | name | address | last_updated | is_deleted |
+----+--------+-----------------+--------------+------------+
| 2 | 张三 | 广州白云山 | 1691563465 | 0 |
| 3 | fox | NULL | NULL | NULL |
| 4 | 李四 | NULL | NULL | NULL |
| 5 | 111 | NULL | NULL | NULL |
| 12 | fox | NULL | NULL | NULL |
| 14 | AAA | NULL | NULL | NULL |
| 15 | BBB | NULL | NULL | NULL |
| 16 | CCC | NULL | NULL | NULL |
| 20 | DDD | NULL | NULL | NULL |
| 22 | EEE | NULL | NULL | NULL |
+----+--------+-----------------+--------------+------------+
10 rows in set (0.00 sec)
mysql> delete from user where id=20;
Query OK, 1 row affected (0.19 sec)
mysql> select * from user;
+----+--------+-----------------+--------------+------------+
| id | name | address | last_updated | is_deleted |
+----+--------+-----------------+--------------+------------+
| 2 | 张三 | 广州白云山 | 1691563465 | 0 |
| 3 | fox | NULL | NULL | NULL |
| 4 | 李四 | NULL | NULL | NULL |
| 5 | 111 | NULL | NULL | NULL |
| 12 | fox | NULL | NULL | NULL |
| 14 | AAA | NULL | NULL | NULL |
| 15 | BBB | NULL | NULL | NULL |
| 16 | CCC | NULL | NULL | NULL |
| 22 | EEE | NULL | NULL | NULL |
| 23 | 333 | NULL | NULL | NULL |
+----+--------+-----------------+--------------+------------+
10 rows in set (0.00 sec)
mysql> delete from user where id=22;
Query OK, 1 row affected (0.29 sec)
mysql> select * from user;
+----+--------+-----------------+--------------+------------+
| id | name | address | last_updated | is_deleted |
+----+--------+-----------------+--------------+------------+
| 2 | 张三 | 广州白云山 | 1691563465 | 0 |
| 3 | fox | NULL | NULL | NULL |
| 4 | 李四 | NULL | NULL | NULL |
| 5 | 111 | NULL | NULL | NULL |
| 12 | fox | NULL | NULL | NULL |
| 14 | AAA | NULL | NULL | NULL |
| 15 | BBB | NULL | NULL | NULL |
| 16 | CCC | NULL | NULL | NULL |
| 23 | 333 | NULL | NULL | NULL |
+----+--------+-----------------+--------------+------------+
9 rows in set (0.00 sec)
-
借助Navicat将主库user表复制到从库,包括数据。 这样一来从库就有了当前的全量数据,只需要跳过错误的gtid,从最新的gtid开始执行就可以了。
查询从库,确定数据是否和主库一致
mysql> select * from user;
+----+--------+-----------------+--------------+------------+
| id | name | address | last_updated | is_deleted |
+----+--------+-----------------+--------------+------------+
| 2 | 张三 | 广州白云山 | 1691563465 | 0 |
| 3 | fox | NULL | NULL | NULL |
| 4 | 李四 | NULL | NULL | NULL |
| 5 | 111 | NULL | NULL | NULL |
| 12 | fox | NULL | NULL | NULL |
| 14 | AAA | NULL | NULL | NULL |
| 15 | BBB | NULL | NULL | NULL |
| 16 | CCC | NULL | NULL | NULL |
| 23 | 333 | NULL | NULL | NULL |
+----+--------+-----------------+--------------+------------+
9 rows in set (0.00 sec) -
获取从库需要配置跳过错误的事务
获取到错误的gtid为d58e6bad-ef3e-11ee-8285-0242ac120003:12,
因为连续删除过两条数据,所以d58e6bad-ef3e-11ee-8285-0242ac120003:13也是错误的事务
mysql> show replica status\G;
*************************** 1. row ***************************
Replica_IO_State: Waiting for source to send event
Source_Host: 192.168.65.223
Source_User: fox
Source_Port: 3308
Connect_Retry: 30
Source_Log_File: mysql-bin.000004
Read_Source_Log_Pos: 4229
Relay_Log_File: 3c946c781110-relay-bin.000012
Relay_Log_Pos: 790
Relay_Source_Log_File: mysql-bin.000004
Replica_IO_Running: Yes
Replica_SQL_Running: No
Replicate_Do_DB:
Replicate_Ignore_DB:
Replicate_Do_Table:
Replicate_Ignore_Table:
Replicate_Wild_Do_Table:
Replicate_Wild_Ignore_Table:
Last_Errno: 1146
Last_Error: Coordinator stopped because there were error(s) in the worker(s). The most recent failure being: Worker 1 failed executing transaction 'd58e6bad-ef3e-11ee-8285-0242ac120003:12' at master log mysql-bin.000004, end_log_pos 3906. See error log and/or performance_schema.replication_applier_status_by_worker table for more details about this failure or others, if any.
Skip_Counter: 0
Exec_Source_Log_Pos: 3645
Relay_Log_Space: 1932
Until_Condition: None
Until_Log_File:
Until_Log_Pos: 0
Source_SSL_Allowed: No
Source_SSL_CA_File:
Source_SSL_CA_Path:
Source_SSL_Cert:
Source_SSL_Cipher:
Source_SSL_Key:
Seconds_Behind_Source: NULL
Source_SSL_Verify_Server_Cert: No
Last_IO_Errno: 0
Last_IO_Error:
Last_SQL_Errno: 1146
Last_SQL_Error: Coordinator stopped because there were error(s) in the worker(s). The most recent failure being: Worker 1 failed executing transaction 'd58e6bad-ef3e-11ee-8285-0242ac120003:12' at master log mysql-bin.000004, end_log_pos 3906. See error log and/or performance_schema.replication_applier_status_by_worker table for more details about this failure or others, if any.
Replicate_Ignore_Server_Ids:
Source_Server_Id: 11
Source_UUID: d58e6bad-ef3e-11ee-8285-0242ac120003
Source_Info_File: mysql.slave_master_info
SQL_Delay: 0
SQL_Remaining_Delay: NULL
Replica_SQL_Running_State:
Source_Retry_Count: 86400
Source_Bind:
Last_IO_Error_Timestamp:
Last_SQL_Error_Timestamp: 240401 14:17:40
Source_SSL_Crl:
Source_SSL_Crlpath:
Retrieved_Gtid_Set: d58e6bad-ef3e-11ee-8285-0242ac120003:1-13,
dfb1f706-ef3e-11ee-82cc-0242ac120004:2
Executed_Gtid_Set: ce6baf4a-ef3e-11ee-99fb-0242ac120002:1-13,
d58e6bad-ef3e-11ee-8285-0242ac120003:1-11,
dfb1f706-ef3e-11ee-82cc-0242ac120004:1-2
Auto_Position: 1
Replicate_Rewrite_DB:
Channel_Name:
Source_TLS_Version:
Source_public_key_path:
Get_Source_public_key: 0
Network_Namespace:
1 row in set (0.00 sec) -
在主库再次执行删除操作
mysql> delete from user where id=12;
Query OK, 1 row affected (0.11 sec)mysql> select * from user;
+----+--------+-----------------+--------------+------------+
| id | name | address | last_updated | is_deleted |
+----+--------+-----------------+--------------+------------+
| 2 | 张三 | 广州白云山 | 1691563465 | 0 |
| 3 | fox | NULL | NULL | NULL |
| 4 | 李四 | NULL | NULL | NULL |
| 5 | 111 | NULL | NULL | NULL |
| 14 | AAA | NULL | NULL | NULL |
| 15 | BBB | NULL | NULL | NULL |
| 16 | CCC | NULL | NULL | NULL |
| 23 | 333 | NULL | NULL | NULL |
+----+--------+-----------------+--------------+------------+
6.在从库
mysql> select * from user;
+----+--------+-----------------+--------------+------------+
| id | name | address | last_updated | is_deleted |
+----+--------+-----------------+--------------+------------+
| 2 | 张三 | 广州白云山 | 1691563465 | 0 |
| 3 | fox | NULL | NULL | NULL |
| 4 | 李四 | NULL | NULL | NULL |
| 5 | 111 | NULL | NULL | NULL |
| 12 | fox | NULL | NULL | NULL |
| 14 | AAA | NULL | NULL | NULL |
| 15 | BBB | NULL | NULL | NULL |
| 16 | CCC | NULL | NULL | NULL |
| 23 | 333 | NULL | NULL | NULL |
+----+--------+-----------------+--------------+------------+
9 rows in set (0.00 sec)
mysql> stop replica;
Query OK, 0 rows affected (0.07 sec)
# 如果需要一次跳过多条,找出需要跳过的gtid,批量执行
mysql> SET @@SESSION.GTID_NEXT= 'd58e6bad-ef3e-11ee-8285-0242ac120003:12';begin;commit;
Query OK, 0 rows affected (0.00 sec)
Query OK, 0 rows affected (0.00 sec)
Query OK, 0 rows affected (0.12 sec)
mysql> SET @@SESSION.GTID_NEXT= 'd58e6bad-ef3e-11ee-8285-0242ac120003:13';begin;commit;
Query OK, 0 rows affected (0.00 sec)
Query OK, 0 rows affected (0.00 sec)
Query OK, 0 rows affected (0.00 sec)
mysql> SET SESSION GTID_NEXT = AUTOMATIC;
Query OK, 0 rows affected (0.00 sec)
mysql> start replica;
Query OK, 0 rows affected (1.10 sec)
mysql> select * from user;
+----+--------+-----------------+--------------+------------+
| id | name | address | last_updated | is_deleted |
+----+--------+-----------------+--------------+------------+
| 2 | 张三 | 广州白云山 | 1691563465 | 0 |
| 3 | fox | NULL | NULL | NULL |
| 4 | 李四 | NULL | NULL | NULL |
| 5 | 111 | NULL | NULL | NULL |
| 14 | AAA | NULL | NULL | NULL |
| 15 | BBB | NULL | NULL | NULL |
| 16 | CCC | NULL | NULL | NULL |
| 23 | 333 | NULL | NULL | NULL |
+----+--------+-----------------+--------------+------------+
8 rows in set (0.01 sec)