MySql复制总结

引言

​ 说MySql复制技术为其高可用性奠基一点不为过,当然实现MySql高可用性有很多种实现方式,比如共享存储、使用分布式协议在引擎级实现的高可用性等等。但是原生MySql复制技术是实现MySql高可用性,保证数据库安全稳定运行的最基础也是最重要的技术,它简单、原生,与MySql无限兼容,随着最新版本的发布,其已经能全方位满足大多数业务场景下的使用。因此有必要总结,总结,在总结!!!

常见概念

异步复制(Asynchronous replication)

async-replication-diagram.png

  • 逻辑上

    ​ MySql默认的复制即是异步的,主库在执行完客户端提交的事务后会立即将结果返给给客户端,并不关心从库是否已经接收并处理,这样就会有一个问题,主如果crash掉了,此时主上已经提交的事务可能并没有传到从库上,如果此时,强行将从提升为主,可能导致新主上的数据不完整。

  • 技术上

    ​ 主库将事务 Binlog 事件写入到 Binlog 文件中,此时主库只会通知一下 Dump 线程发送这些新的 Binlog,然后主库就会继续处理提交操作,而此时不会保证这些 Binlog 传到任何一个从库节点上。

全同步复制(Fully synchronous replication)

  • 逻辑上

    ​ 指当主库执行完一个事务,所有的从库都执行了该事务才返回给客户端。因为需要等待所有从库执行完该事务才能返回,所以全同步复制的性能必然会收到严重的影响。

  • 技术上

    ​ 当主库提交事务之后,所有的从库节点必须收到、APPLY并且提交这些事务,然后主库线程才能继续做后续操作。但缺点是,主库完成一个事务的时间会被拉长,性能降低。

半同步复制(Semisynchronous replication,5.5)

semisync-replication-diagram.png

  • 逻辑上

    ​ 是介于全同步复制与全异步复制之间的一种,主库只需要等待至少一个从库节点收到并且 Flush Binlog 到 Relay Log 文件即可,主库不需要等待所有从库给主库反馈。同时,这里只是一个收到的反馈,而不是已经完全完成并且提交的反馈,如此,节省了很多时间。

  • 技术上

    ​ 介于异步复制和全同步复制之间,主库在执行完客户端提交的事务后不是立刻返回给客户端,而是等待至少一个从库接收到并写到relay log中才返回给客户端。相对于异步复制,半同步复制提高了数据的安全性,同时它也造成了一定程度的延迟,这个延迟最少是一个TCP/IP往返的时间。所以,半同步复制最好在低延时的网络中使用。

问题

很显然,从上面的概念来看,早期的MySql版本或者说早期MySql复制技术存在两个很显眼的问题:

  • 主从数据一致性
    • 异步复制:显然异步复制不能保证在master发生宕机或者网路异常不能把binglog同步到slave时,切换主备会造成数据丢失。
    • 全同步复制:如果我们在有多个slave的情况下,会严重影响master对客户端的响应速度,因此生产上基本很少用。
    • 半同步复制:从5.5版本开始,可以使用半同步插件,能解决部分全同步复制影响客户端响应及数据一致性问题,至于为什么只能解决部分,会在增强半同步段落说明。
  • 数据同步延迟
    • 复制的另外一个影响使用的地方在于数据延迟,特别在网络不稳定、master处理大事务的时候表现特别突出。这是因为MySql最早的主备复制只有两个线程,IO 线程负责从主库接收 Binlog 日志,并保存在本地的 relay log 中,SQL线程负责解析和重放 relay log 中的 event。当主库并行写入压力较大时,备库 IO 线程一般不会产生延迟,因为写 relay log 是顺序写,但是 SQL 线程重放的速度经常跟不上主库写入的速度,会造成主备延迟。如果延迟过大,relay log 一直在备库堆积,还可能把磁盘占满。

MySql的解决之道

增强半同步复制

为了解决数据一致性问题,5.7 的半同步最关键的一个新增参数是 rpl_semi_sync_master_wait_point ,主从一致性加强,支持在事务commit前等待ACK。

  • AFTER_COMMIT

    after_commit..png

    after commit是MySQL5.6半同步参数,区别于after sync,after sync是在接收ack确认以后主库在引擎层做提交,而after commit是先在引擎层做提交后等待ACK确认。因此,在写入数据后并且在从库确认之前,其他的客户端可以看到在这一事务。
    故障分析:
    1.binlog 未发送到从库:
    事务B获取到事务A提交的内容, 此时宕机故障切换到slave,事务B获取到的内容却丢失了。事务A commit没有收到反馈信息(则需要业务判断了)。
    2.binlog 已经发送给从库 :
    事务B获取到事务A提交的内容,故障切换到salve ,B仍然获取到A提交的内容,没毛病。事务A commit没有收到反馈信息,若重新执行该事务,则相当于执行两次A事务(则需要业务判断了)。

  • AFTER_SYNC (5.7的默认值)

    after_sync..png

    master 将每个事务写入Binlog , 传递到slave 刷新到磁盘(relay log)。master等待slave 反馈接收到relay log的ack之后,再提交事务并且返回commit OK结果给客户端。 即使主库crash,所有在主库上已经提交的事务都能保证已经同步到slave的relay log中。

    实际上,客户端发出commit请求后,在主库上写入binlog并推送给slave,slave接收到binlog并写入relaylog,发送ACK确认已经接收binlog后,master在引擎层commit,客户端接收commit完成,此时其他会话才可以看见已提交的数据。
    故障分析:假设master在接收ACK确认时宕机,因为在引擎层并没有提交,HA切换到从库,因为binlog已经写入从库的relaylog,因此不会造成数据丢失,个人认为是目前比较完美的解决方式。

并行复制的演进

为了缓解数据延迟问题,很自然的想法是提高 SQL 线程重放的并行度,引入并行复制。

5.6 Schema 级别的并行复制

开启并行复制后,会启动多个 Worker 线程,原有的 SQL 线程变为 Coordinator 线程。
可以并行的事务分发给 Worker 线程执行;
不能并行的事务等待 Worker 线程全部结束后,再由 Coordinator 线程自己执行。
DDL 语句或者是跨 Schema 的语句不能并行执行。
这种并行复制的模式,对于多个 DB 同时更新才能有较高的并行度,但是更常见的情况是更新集中在同个一个 DB。
一个简单的改进,把 Schema 级别的并行复制改成 Table 级别,可以大幅度提高单库多表环境下的并行度。但是对于只有一个热点表的情况依然处理不了。

5.7 基于 Group Commit 的并行复制
Group Commit

引入 Group Commit之前,Binlog 和 InnoDB 日志是内部XA,为了保证 InnoDB 和 Binlog 提交顺序一致,实际是串行提交,执行序列如下:

  • InnoDB prepare
  • write/sync Binlog
  • InnoDB commit
    官方的 Group Commit 分为三个阶段,每个阶段有一个线程操作,三个阶段可以并发执行。
  • flush stage:Binlog 从 cache 写入文件
  • sync stage: 对 Binlog 做 fsync
  • commit stage:引擎层 commit
    这样 InnoDB prepare 成功的事务可以进入队列,每个阶段可以对队列事务统一做操作,提高了并行度。
    写Binlog 和 InnoDB commit 都是按照队列中的顺序,可以保证 Binlog 和事务提交顺序一致。
    image.png
    Binlog 中记录了 sequence_number 和 last_commited,如上图,MySqlBinlog 解析日志可以看到这两个值。
    sequence_number 是自增事务 ID,last_commited 代表上一个提交的事务 ID。
    如果两个事务的 last_commited 相同,说明这两个事务是在同一个 Group 内提交的。
LOGICAL_CLOCK 并行复制

slave-parallel-type=LOGICAL_CLOCK : Commit-Parent-Based模式(同一组的事务[last-commit相同],没有锁冲突. 同一组,肯定没有冲突,否则没办法成为同一组)
slave-parallel-type=LOGICAL_CLOCK : Lock-Based模式(即便不是同一组的事务,只要事务之间没有锁冲突[prepare阶段],就可以并发。 不在同一组,只要N个事务prepare阶段可以重叠,说明没有锁冲突)

5.7 引入了变量 slave_parallel_type,可选值 DATABASE、LOGICAL_CLOCK,DATABASE 就是和 5.6 中相同,Schema 级别的并行复制,而 LOGICAL_CLOCK 是基于 Group Commit 的并行复制,相比 5.6 极大提高了并行度。
Group Commit 实现了主库事务的并行提交。很显然的,主库能同时进入prepare阶段的事务之间不会冲突,那么这些事务在备库回放时也不会冲突。
Group Commit 中,last_commited 相同的事务,可以在备库并行回放。
MySql bin log 里面维护了两个变量
Logical_clock max_committed_transaction:记录上次 Group commit 时最大的 sequence_number,即上述 MySqlBinlog 中的 last_committed
Logical_clock transaction_counter:sequence_number 来源,每次分配 sequence_number 时 transaction_counter 进行递增,即当前最大的 sequence_number
5.7 的并行复制还有一点点弊端,如果如果主库并行度低,那么备库回放时也很难并行。

为此,5.7 引入了两个参数:

  • binlog_group_commit_sync_delay:等待延迟提交的时间,binlog提交后等待一段时间再 fsync。让每个 group 的事务更多,人为提高并行度。
  • binlog_group_commit_sync_no_delay_count:等待提交的最大事务数,如果等待时间没到,而事务数达到了,就立即 fsync。达到期望的并行度后立即提交,尽量缩小等待延迟。
8.0 基于 WriteSet 的并行复制

5.7 为了提高备库回放的速度,需要在主库尽量提高并行度。
8.0解决了这个问题,即使主库在串行提交的事务,只有互相不冲突,在备库就可以并行回放。
8.0 引入了参数 binlog_transaction_dependency_tracking 来控制事务依赖模式,让备库根据 commit timestamps 或者 write sets 并行回放事务,有三个取值:

  • COMMIT_ORDERE:使用 5.7 Group commit 的方式决定事务依赖
  • WRITESET:使用 WriteSet 的方式决定判定事务直接的冲突,发现冲突则依赖冲突事务,否则按照 COMMIT_ORDERE 方式决定依赖
  • WRITESET_SESSION:在 WRITESET 方式的基础上,保证同一个 session 内的事务不可并行
    WRITESET 是一个 hash 数组,大小由参数 binlog_transaction_dependency_history_size 决定。
    参数 transaction_write_set_extraction 决定 hash 算法,可选值:OFF、MURMUR32、XXHASH64,默认值 XXHASH64,如果
    WriteSet 记录了事务的更新行信息,决定 commit_parent时,使用事务自己的 session WriteSet 和 history WriteSet 进行比对,找到最近的冲突行,设为 commit_parent。如果 WriteSet 找不到 commit_parent,则还是使用 COMMIT_ORDERE 决定 commit_parent
  • 如何启用write-set并行复制
1
2
3
4
5
6
7
8
MySQL 5.7.22+ 支持基于write-set的并行复制  
# master
loose-binlog_transaction_dependency_tracking = WRITESET
loose-transaction_write_set_extraction = XXHASH64
binlog_transaction_dependency_history_size = 25000 #默认
#slave
slave-parallel-type = LOGICAL_CLOCK
slave-parallel-workers = 32
  • 核心原理
1
2
3
4
5
6
7
8
9
10
# master  
master端在记录binlog的last_committed方式变了
基于commit-order的方式中,last_committed表示同一组的事务拥有同一个parent_commit
基于write-set的方式中,last_committed的含义是保证冲突事务(相同记录)不能拥有同样的last_committed值
当事务每次提交时,会计算修改的每个行记录的WriteSet值,然后查找哈希表中是否已经存在有同样的WriteSet

1. 若无,WriteSet插入到哈希表,写入二进制日志的last_committed值保持不变,意味着上一个事务跟当前事务的last_committed相等,那么在slave就可以并行执行
2. 若有,更新哈希表对应的writeset的value为sequence number,并且写入到二进制日志的last_committed值也要更新为sequnce_number。意味着,相同记录(冲突事务)回放,last_committed值必然不同,必须等待之前的一条记录回放完成后才能执行
# slave
slave的逻辑跟以前一样没有变化,last_committed相同的事务可以并行执行
  • 要不要开启并行复制
1
2
3
4
5
1. 基于order-commit的模式,本身并行复制已经很好了,如果并发量非常高,那么order-commit可以有很好的表现,如果并发量低,order-commit体现不了并行的优势。       
但是大家想想,并发量低的MySQL,根本也不需要并行复制吧
2. 基于write-set的模式,这是目前并发度最高的并行复制了,基本可以解决大部分场景,如果并发量高,或者新搭建的slave需要快速追主库,这是最好的办法。
3. 单线程复制 + 安全参数双0,这种模式同样拥有不随的表现,一般压力均可应付。
以上三种情况,是目前解决延迟的最普遍的方法,目前我用的最多的是最后一种
  • 如何让slave的并行复制和master的事务执行的顺序一致

    5.7.19 之后,可以通过设置 slave_preserve_commit_order = 1

总结

随着 MySql 版本迭代,备库回放效率越来越高,为了保证主备同步时效性,可以尽量更新版本 MySql
同时,为了保证备库回放效率,应该根据业务模型适当设置复制相关参数。
比如 5.7 可以适当调大 binlog_group_commit_sync_delay 以提高主库并行度,同时设置 binlog_group_commit_sync_no_delay_count 在已满足并行度要求时主动提交,尽量减小延迟
在 8.0 中根据数据库配置高低设置 binlog_transaction_dependency_history_size,性能有富余的实例可以适当调大该参数,找到更小的 commit parent,提高备库回放并行度。内存和CPU紧张的实例最好避免在 WriteSet上消耗太多资源。binlog_transaction_dependency_history_size 过大,不光消耗内存,还会降低冲突查询的效率。

参考

MySql · 特性分析 · 8.0 WriteSet 并行复制
MySQL并行复制的深入浅出 | Focus on MySQL,Focus on Life
【MySql】5.7增强半同步AFTER SYNC&AFTER COMMIT - 简书
MySql并行复制的深入浅出 | Focus on MySql,Focus on Life
MySql系列(四)异步复制、全同步复制与半同步复制_数据库_xihuanyuye的博客-CSDN博客
【MySQL】5.7增强半同步AFTER SYNC&AFTER COMMIT - 简书