Replication enables data from one MySQL database server (the master) to be replicated to one or more MySQL database servers (the slaves).
虽然 InnoDB 也有 redo 日志,但是 MySQL 的复制并不是基于 redo 的,因为 MySQL 是多存储引擎的数据库,采用统一的 binlog 日志进行复制。

关于 binlog 日志,请参考下文:

逻辑复制与物理复制

.MySQLOracle Data Gurad/SQL Server Mirroring
类型逻辑复制物理逻辑复制
优点灵活复制速度快
缺点配置不当易出错要求物理数据严格一致
记录记录每次逻辑操作记录每次对于数据页的操作

为了解决逻辑复制慢的问题,MySQL 5.7 以后推出了并行复制(后文会讲),速度提升了很多。

主从复制架构

mysql_replication_arch_net.jpg

mysql_replication_arch.jpg

MySQL 的一次提交分为三个阶段:第一个阶段是 prepare,第二个阶段是写 binlog,第三个阶段是写 innodb 日志。第1个和第3个阶段发生在 InnoDB 层,第2个阶段发生在 MySQL 层。这三个步骤都是组提交的,所以性能不会差。

当日志写到 binlog 之后,会有一个 Dump 线程将日志推送到对端服务器,对端服务器 IO 线程用于接收日志,SQL 线程用于回放日志。从 MySQL 5.6 开始,回放线程可以有多个,并行回放。主往从发送内容是以 EVENT 为单进行发送的。

  1. set sync_binlog=1 to assure crash safe
  2. InnoDB group commit is fairly random
  3. set innodb_flush_log_at_trx_commit=1 to garantee crash safe
  4. set relay_log recovery=1 to enable IO thread crash safe
  5. semi-sync replication need send ACK to master
  6. set relay_log_info_repository=TABLE to make SQL threads crash safe
  • 在 MySQL 5.7 版本中, Prepare log 部分的日志也是组提交的
  • MySQL Dump Thread 把 binlog 推送到远程的 Slave 服务器(每一个Slave,就会对应有一个 dump 线程)
  • Master Thread 每隔1秒从 redo log buffer 中写入 redo file
  • 在 MySQL 5.6 中的多线程回放是基于库的, 单个库还是单线程,在 MySQL 5.7 中的多线程是在主上如何并行执行的,从机上也是如何并行回放的
  • master-info.log 存放了接受到的 binlog 的位置(event的位置)
  • relay-info.log 存放了回放到的 relay log 的位置(event的位置)

SQL线程高可靠问题

如果将 relay_log_info_repository 设置为 FILE ,MySQL 会把回放情况信息记录在参数 relay_log_info_file 指定的 relay-info.log 的文件中,其中包含SQL线程回放到的 Relay_log_name 和 Relay_log_pos,以及对应的 Master 的 Master_log_name 和 Master_log_pos。SQL 线程回放的基本单位是 event,参数 sync_relay_log_info = 10000 代表每回放 10000 个event, 写一次 relay-info.log [the slave synchronizes its relay-log.info file to disk(using fdatasync()) after every N transactions],这样就可能发生如下情况:event2 和 event3 回放写入成功(且已经落盘),但是在 relay-info.log 中的记录还是 event1 的位置。此时Slave宕机,然后重启,便会产生如下的状况:

  1. Slave 的库中存在 event2 和 event3
  2. Slave 读取 relay-info.log 中的 Relay_log_name 和 Relay_log_pos,此时记录的是回放到 event1 的位置
  3. Slave 从 event1 开始回放 ,继续回放 event2 和 event3
  4. 但是,此时的数据库中存在 event2 和 event3,于是发生了 1062 的错误(重复记录)

如果该参数设置为 1 ,则表示每回放一个 event , 就写一次 relay-info.log,那写入代价很大,性能很差,即使性能上可以接受,还是可能会丢最后一次的操作,恢复起来后还是有 1062 的错误(重复执行event)。

SQL 线程的数据回放是写数据库操作,relay-info.log 是写文件操作,这两个操作很难保证一致性。

解决办法:在 MySQL5.6+ 以后,将 relay_log_info_repository 设置为 TABLE ,relay-info 将写入到 mysql.slave_relay_log_info 这张表中。如此,可将 event 的回放和 relay-info 的更新放在同一个事务里面,变成原子操作,从而保证一致性(要么都写入,要么都不写)。

假设 sync_relay_log_info = N (N>0),当设置为 TABLE 时:

  • 如果存储引擎支持事务,则 the table is updated after each transaction,参数 sync_relay_log_info 被忽略。
  • 如果存储引擎不支持事务,the table is updated after every N events.

I/O线程高可靠问题

IO 线程也是接收一个个的 event ,更新 master-info 信息到参数 master_info_repository 指定的位置(文件 FILE 或者数据库 TABLE: mysql.slave_master_info),然后将接收到的 event 写入 relay log file。参数 sync_master_info=10000 表示每接收 10000 个 event,写一次 master-info。这里同样存在前面的问题:master-info.log 和 relay-log 无法保证一致性。例如,event2 和 event3 已经写入到了 relay-log,但是 master-info 只记录了 event1,此时如果服务宕机后,MySQL重启,I/O 线程会读取 master-info.log 的内容,读取到的位置为 event1 的位置,然后 I/O 线程会继续将 event2 和 event3 拉取过来,然后继续写入到 relay-log 中,当SQL线程回放时,就会产生 1062 的错误(重复记录),看到的现象还是 IO 线程正常,SQL 线程报错。

master_info_repository 设置为 TABLE 或者 FILE 对复制的可靠性是没有帮助的,因为 event 到 relay-log 中去还是文件操作,还是不能保证一致性,但是 master_info_repository 也一定要设置为 TABLE ,性能上比设置为 FILE 有很高的提升(官方BUG)。解决该问题的方法是设置参数 relay-log-recover = 1 ,该参数表示当发生 crash 时,将当前接收到的 relay-log 全部删除,然后从 SQL 线程回放到的位置重新拉取(SQL线程通过配置后是可靠的)。但是注意:这种删除 RELAY-LOG 的操作也有风险,因为主库可能因为 binlog 超过了保存时长而被删除了。

所以说,真正的MySQL复制的高可靠是从 5.6 版本开始的,通过设置如下三个参数来确保整体复制的高可靠:
• relay-log-recover = 1
• relay_log_info_repository = TABLE
• master_info_repository = TABLE
换言之,之前的版本复制不可靠是正常的。

read_only与super_read_only

如果在Slave机器上对数据库进行修改或者删除,会导致主从的不一致,需要对Slave机器设置为 read_only = 1 ,让Slave提供 只读 操作。但 read_only 仅仅对没有SUPER权限的用户有效(即mysql.user表的Super_priv字段为Y)(好在一般给 App 用户的权限是不会是 SUPER权限)。参数 super_read_only 可以将有 SUPER权限 的用户也设置为只读,且该参数设置为 ON 后, read_only 也跟着自动设置为 ON。

注意:MySQL5.7.11 中,在 /etc/my.cnf 中将 super_read_only=1 配置好后重启,还是可以插入或修改数据。需要在命令行中执行 set global super_read_only=1; 才能真正修改为只读。

并行复制(Multi-Threaded Slave,MTS)/ 并行回放

MySQL 的并行复制基于组提交:一个组提交中的事务都是可以并行执行的,因为既然处于组提交中,这意味着事务之间没有冲突(不会去更新同一行数据),否则不可能在同一个组里面。

开启并行复制,只需要在 Slave 上配置如下参数:

slave-parallel-type=LOGICAL_CLOCK    <-- 意思是主上怎么并行执行,从上也怎么并行执行。如果设置为 DATABASE,表示基于库级别的并行复制,如果只有一个库,就还是串行,DATABASE 只是为了兼容 5.6 而存在,否则使用 LOBICAL_CLOCK
slave-parallel-workers=4    <-- 并行复制的线程数(SQL Thread),线上物理机设置为 16 或者 32 足够了,在线修改后,需要 stop slave; start slave 做一个启停以生效
slave_preserve_commit_order=1 <-- Slave 上 commit 的顺序保持一致,必须为1,否则可能会有GAP锁产生

如果是多线程复制,无论是5.6库级别的假多线程还是MariaDB或者5.7的真正的多线程复制,SQL线程只做coordinator,只负责把relay log中的binlog读出来然后交给worker线程,woker线程负责具体binlog event的执行。

MySQL 8.0 的并行复制更牛,即使主上面是单线程执行的,从上面也可以并行回放。其思想是,MySQL 日志记录的是行,包含了库、表、主键等信息,如果两个事务之间,修改的行是独立的,互不影响的话,就可以并行回放。

可以根据自己的业务情况来决定是否开启并行回放。一些繁忙的业务系统,主从延迟可能达到小时级,这时并行回放就可以很好的解决这个问题。

虽然 MySQL 的并行复制配置起来非常简单,但是其主从复制延时的问题困扰了 MySQL 十几年,直到 2015 年 MySQL 5.7 出来才很好的解决了该问题。

半同步复制

之前的复制,都是异步复制,Master 并不关心数据是否被 Slave 节点所获得 ,所以复制效率很高,但是主从切换后,数据有可能会丢失。相对于异步复制,半同步复制提高了数据的安全性,同时它也造成了一定程度的延迟,这个延迟最少是一个TCP/IP往返的时间。所以,半同步复制最好在低延时的网络中使用。从实际情况和 sysbench 测试结果来看,(无损)半同步复制的性能损失并没有大家想象的那么大,这个与网络与数据读写比例有关。

半同步架构介绍

semi_sync.JPG
lossless_sync.JPG

从 MySQL5.5 开始,MySQL推出了 semi-sync replication(半同步复制)。
从 MySQL5.7 开始,MySQL推出了 lossless semi-sync replication(无损复制)。

rpl_semi_sync_master_wait_point:AFTER_SYNC(默认值),5.7 之前没有无损半同步复制,5.7.2 开始增加了这个参数,当取值为 AFTER_COMMIT 就是普通的半同步复制。该参数的设计是为了兼容之前的版本,比如,现有生产系统是 5.5,备库是 5.7。

这两种都是半同步复制,区别在于等待的位置。主在提交一个事务时,会有写 prepare log,写 binlog,写 redolog 三个步骤。启用半同步复制后,主都会等待至少一个 Slave 节点的 IO 线程回复 ACK 后(表示从收到了 binlog ),才能继续下一个事物(可以通过 rpl_semi_sync_master_wait_for_slave_count 来定义需要等待回复的从的数量)。如果在一定时间内(Timeout)内没有收到 ACK,则自动切换为异步模式,当 Slave 又追上 Master 了(IO线程),则会自动切换回半同步复制。半同步复制与无损复制的区别在于,semi-sync 是在三个步骤完成后等 ACK,lossless semi-sync 是在前两个步骤完成后等 ACK。

例如:停掉从的 IO_THREAD 线程后,在主上执行 insert cyt values (1) 操作,此时,该会话会卡起待:
如果是无损半同步模式,则在主的另一个会话里是看不到插入的记录的;
如果是普通半同步模式,则在主的另一个会话里是能看到插入的记录的。

假设主从复制时产生异常(比如 Master 宕机了),Master的binlog还没有传递到Slave上,此时半同步复制与无损复制,在主从数据一致性上的表现是不一样的。

  • semi-sync replication 在 commit 完成后,才传输 binlog,意味着在 Master 节点上,这个刚刚提交的事务对数据库的修改, 对其他事务是可见的,假如此时 Master 宕机了,且发生主从切换,此时的 Slave 提升为 New Master,但是此时的 New Master 上是没有之前提交的事务的内容的,这样就产生了主从数据的不一致。对 App 而言,之前读取到的内容,现在读取不到了。
  • loss less semi-sync replication 在 write binlog 完成后,就传输 binlog,但还没有去写 commit log,意味着当前这个事务对数据库的修改,其他事务也是不可见的,假如此时 Master 宕机了,且发生了主从切换,此时的 Slave 提升为 New Master,由于 Master 上对该事务还没有提交,且此时的 New Master 上同样也没有该事务的内容,此时主从的数据是一致的,对于 APP 来说,发生主从切换后,APP 读取到的内容前后是一致的。(如果原来的Master又恢复了,需要让原来的Master不要提交宕机前的那个事务)

所以,当主机失败,且主从未切换,恢复后,两种复制模式下的主从数据都是最终一致的(配置是 crash_safe 的)。
若主机失败,做了主从切换,则两种方式的表现不一样:

  • semi-sync 可以减少数据丢失风险但不能完全避免数据丢失
  • lossless semi-sync 可保证数据完全不丢失

安装半同步插件

两种同步方式使用相同的插件。注意:这儿仅仅是安装了插件而已,还未启用。主从上都要安装。

安装方式一:手工安装

mysql> INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so';
mysql> INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so’;

安装方式二:写入配置文件

[mysqld]
plugin_dir=/usr/local/mysql/lib/plugin
plugin_load="rpl_semi_sync_master=semisync_master.so;rpl_semi_sync_slave=semisync_slave.so"

检查是否安装成功:

mysql> show plugins;
+----------------------------+----------+--------------------+--------------------+---------+
| Name                       | Status   | Type               | Library            | License |
+----------------------------+----------+--------------------+--------------------+---------+
| rpl_semi_sync_master       | ACTIVE   | REPLICATION        | semisync_master.so | GPL     |
| rpl_semi_sync_slave        | ACTIVE   | REPLICATION        | semisync_slave.so  | GPL     |
+----------------------------+----------+--------------------+--------------------+---------+

启用/关闭半同步

[mysqld]
loose_rpl_semi_sync_master_enabled=1 <-- 开启主的半同步复制
loose_rpl_semi_sync_slave_enabled=1  <-- 开启从的半同步复制
loose_rpl_semi_sync_master_timeout=1000  <-- 超时1秒,如果可靠性要求高,不想它切加异步,就把这个值调高
loose_rpl_semi_sync_master_wait_point=AFTER_SYNC <-- 5.7 可以设置为 AFTER_SYNC 来使用无损复制,AFTER_COMMIT 使用半同步
loose_rpl_semi_sync_master_wait_for_slave_count=1 <-- 5.7 可以配置至少收到多少个slave发回的 ACK

使用 loose_ 前缀表示如果没有加载 semi_sync 的插件,则忽略该参数。

两种复制方式的性能

在 IO Bound 场景下:

  • 异步复制性能很好,但是随着并发数的增长,性能有所下降
  • 无损复制随着并发数的增长,性能几乎是线性增长的,在高并发下,性能会优于异步复制
  • 半同步复制性能较低

无损复制性能优于半同步复制的原因:

  1. 就等待ACK回包问题上,其实两种复制的开销是一样的,没有区别,都是网络的等待开销。
  2. 无损复制由于在 write binlog 后,需要等待ACK,后续的事物无法提交,这样就堆积了很多需要落盘的事物(半同步复制由于已经提交了事物,没有堆积事物的效果),通过组提交机制,一次 fsync 的事物变多了(半同步复制也有组提交,只是一次fsync 的事物数没那么多),相当于提高了 I/O 性能 。
    所以线程(事物)越多,效果越明显,以至于有超过异步复制的效果。(无损复制的组提交比原版组提交的高3~4倍)

通过GTID复制

GTID 是后面一切高可用技术的基础,一定要打开。

GTID(Global Transaction Identifier,全局事物ID)是MySQL 5.6的新特性,GTID 由 Server_UUID + Transaction_ID 组成,作用是替代 Filename + Position。每个实例的 Server_UUID 是不一样的(show variables like '%uuid%')。

在没有 GTID 时,一主多从的架构中,当 Master 宕机后,某个 Slave 被提升为 New Master,如果需要继续维持复制关系,就需要把另外的 Slave 的 CHANGE MASTER 指向 New Master,新 Master 的 Filename + Position 是比较难以确定的(主从的文件名并没有对应关系)。如果使用了 GTID,就可以通过 GTID 来确定位置(全局唯一)。

那 GTID 是如何快速定位的呢?

因为 binlog 在 rotate(rotate events)的时候,是知道当前最大的 GTID 的,可以将该值写入到下一个新的 binlog 的开头,即 binlog 中记录的 Previous_gtids:

mysql> show binlog events in 'bin.000021'\G

当从需要跟主同步时,就可以使用从当前的 GTID 值,跟 binlog 头部的 Previous_gtids 作比较:如果我要的下一个 GTID 比 Previous_gtids 值大,就扫描当前文件,反之则扫描之前的文件,依次类推。

启用 GTID

GTID 的配置很简单,只需要写上如下的4条配置即可:

[mysqld]
log_bin = bin.log
gtid_mode = ON
log_slave_updates = 1
enforce_gtid_consistency = 1

1.MySQL 5.6 必须开启参数 log_slave_updates (5.6版本的限制),5.7 版本开始可以不开启
2.MySQL 5.6 升级到gtid模式需要停机重启,5.7.6 版本开始可以在线升级成gtid模式

开启 GTID 后的注意事项

1.启用 GTID 后,数据库之前的备份就不能用了,需要重新对数据库做备份。
2.使用 mysqldump 备份单个数据库时会有 Warning,大致意思为你只备份了部分数据库,但是启用GTID后包含了所有的事物。(可以忽略该警告)
3.启用 GTID 后,某些不安全的语句会被禁用,比如 CTAS,因为这其实是多个事务了,GTID没法对应。
4.开启 GTID 后,不能在一个事物中使用创建临时表的语句,需要参数 autocommit=1 才可以。

不要设置的两个参数

下面这两个参数不要去设置,设置了反而性能差

mysql> show variables like "%binlog_group%";
+-----------------------------------------+-------+
| Variable_name                           | Value |
+-----------------------------------------+-------+
| binlog_group_commit_sync_delay          | 0     |
| binlog_group_commit_sync_no_delay_count | 0     |  <-- 等待一组里面有多少事务我才提交
+-----------------------------------------+-------+
2 rows in set (0.03 sec)

mysql> show variables like "%binlog_max%";
+-----------------------------+-------+
| Variable_name               | Value |
+-----------------------------+-------+
| binlog_max_flush_queue_time | 0     |  <-- 等待多少时间后才进行组提交
+-----------------------------+-------+
1 row in set (0.03 sec)

slave_rows_search_algorithms

参数 slave_rows_search_algorithms 的默认值为 "TABLE_SCAN,INDEX_SCAN",不要去更改,这个参数表明了怎样来提高复制的性能。

之前强调过的每张表一定要有主键的要求,除了是符合范式的要求,也可以提高主从复制的性能。因为Slave进行回放的时候,是根据索引进行回放的,过程为:先找主键 --> 没有主键则找唯一索引 --> 没有唯一索引则找普通索引 --> 没有普通索引则全表扫描。

index used/option valueINDEX_SCAN,HASH_SCAN Or INDEX_SCAN,TABLE_SCAN,HASH_SCANINDEX_SCAN,TABLE_SCANTABLE_SCAN,HASH_SCAN
Primary key or unique keyIndex scanindex scanindex hash
(other)keyIndex hashindex scanindex hash
No indexTable hashTable scanTable hash

例如:delete from cyt where id in (1, 3);
针对上述语句,假设 cyt 不存在任何主键或索引,则在 Master 上操作的时候只要扫描一遍该表即可,但是复制是基于行的(ROW格式),在 Slave 上就要扫描两次,一次扫描是为了 id=1 的行,一次扫描 是为了 id=3 的行。即全表扫描时:Master 扫描一次,复制时,Slave上扫 N 次。

新增的 Hash Scan 方式,会先增加一个哈希表,这样就只扫描一次了,但是创建哈希表的代价很大。所以默认没有启用的。

所以强烈建议:每张表上都要有一个主键。

-- By 许望(RHCA、OCM、VCP)
最后修改:2025 年 07 月 20 日 04 : 47 PM
如果觉得我的文章对你有用,请随意赞赏