笔者近期对于实现消息的顺序消费实现整理中,针对MySQL如何保证主从复制顺序一致进行调研,结果记录如下。
流程
基于偏移量
- 从节点 start slave 开启主从复制,从节点的 I/O线程请求主节点,请求从指定binlog日志的指定位置(或最开始位置)之后的内容(binlog filename + position)
- 主节点收到请求后,为每个Slave开启一个 binlog dump 线程。该线程会读取指定位置日志内容返回给 slave,返回信息中包含下一次数据的读取位置
- 从节点的 I/O 进程接收到主节点发送过来的日志内容、日志文件及位置点后,将接收到的日志内容更新到本机的 relay log 文件(Mysql-relay-bin.xxx)的最末端,并将读取到的 Binlog文件名和位置保存到
master-info
文件中,以便在下一次读取的时候能够清楚的告诉 Master :“ 我需要从哪个 Binlog 的哪个位置开始往后的日志内容,请发给我”。 - Slave 的 SQL 线程检测到relay log 中新增加了内容后,会将 relay log 的内容解析成在能够执行 SQL 语句,然后在本数据库中按照解析出来的顺序执行,并在
relay log.info
中记录当前应用中继日志的文件名和位置点。
GTID 复制模式
- master 更新数据时,会在事务前产生 GTID,一同记录到 Binlog 日志中。
- slave 端的 I/O 线程将变更的 Binlog,写入到本地的 relay log 中,读取值是 master 根据 slave 请求时传入的 GTID 查找下一下 GTID,返回给 slave 应该从哪个 GTID 拉取事务。
- SQL 线程从 relay log 中获取 GTID,然后对比 slave 端的 Binlog 是否有记录。如果有记录,说明该 GTID 的事务已经执行,slave 会忽略。
- 如果没有记录,slave 就会从 relay log 中执行该 GTID 的事务,并记录到 Binlog。
GTID 复制对比传统偏移量复制
- 传统基于偏移量复制 Slave 端不用开启 binlog,但 GTID 复制需要 Slave 的binlog用于记录当前GTID 是否已经执行过(MySQL 5.7 允许不开启binlog)。
- GTID 更方便地实现 Failover。在 GTID 出现之前,在配置主备复制的时候,首先需要确认Event在那个Binlog文件,及其偏移量。假设有 A(MASTER)、B(SLAVE)、C(SLAVE) 三个实例,如果主库宕机后,需要通过 CHANGE MASTER TO MASTER_HOST=’XXX’, MASTER_LOG_FILE=’XXX’, MASTER_LOG_POS=NNNNN指向新库。
这里的难点在于,同一个事务在每台机器上所在的 Binlog 文件名和偏移都不同,这也就意味着需要知道新主库的文件以及偏移量,对于有一个主库+多个备库的场景,如果主库宕机,那么需要手动从备库中选出最新的备库,升级为主,然后重新配置备库。这就导致操作特别复杂,不方便实施,这也就是为什么需要MHA、MMM 这样的管理工具。之所以会出现上述的问题,主要是由于各个实例Binlog中的 Even t以及 Event 顺序是一致的,但 Binlog+Position 是不同的。
通过GTID提供了对于事物的全局一致 ID,主备复制时,只需要知道这个 ID 即可。MySQL会记录哪些事务已经执行,通过 GTID 也就知道接下来要执行那些事务。当有了 GTID 之后,就显得非常的简单,因为同一事务的 GTID 在所有节点上的值一致,那么就可以直接根据 GTID 就可以完成 Failover 操作。
当主机ServerA 挂了之后 ,此时ServerB执行完了所有从ServerA 传过来的事务,ServerC 延时一点。这个时候需要把 ServerB 提升为主机 ,Server C 继续为备机;当ServerC 链接ServerB 之后,首先在自己的二进制文件中找到从ServerA 传过来的最新的GTID,然后将这个GTID 发送到 ServerB ,ServerB(新主) 获得这个 GTID 之后,就开始从这个GTID的下一个GTID开始发送事务给ServerC。这种自我寻找复制位置的模式减少事务丢失的可能性以及故障恢复的时间。
- GTID 支持多线程复制(但局限在每个库一个 Thread,对于单实例只有一个库就没有意义了)。
- GTID 不支持非事务引擎。
GTID 概念
MySQL 5.6 引入全局事务标识符(Global Transaction Identifiers, GTID)
GTID = source_id:transaction_id
服务器的 UUID 和一个以事务提交顺序递增数字(形如:f03253e0-cd46-11ea-a2c4-000c292c767e:1)
UUID 通过调用uuid()函数生成,为了防止UUID冲突,MySQL还采取了一些措施,例如将服务器ID(server ID)和二进制日志文件名(binlog file name)一起写入UUID中。
这些机制保证了 GTID 在整个集群中是全局唯一的。
总结:如何保证顺序?
以基于偏移量为例,由于是 Slave 单线程拉取,并且偏移量每次请求都以上次请求的结果为准,并不会有顺序问题。GTID 同理。