作者:卢飞
来源:DoDBA(mysqlcode)
0、导读
本文几乎涵盖了MySQL Replication(主从复制)的大部分知识点,包括Replication原理、binlog format、复制中如何保证数据一致性、组提交、复制优化、半同步复制、多源复制。
目前很多公司中的生产环境中都使用了MySQL Replication ,也叫 MySQL 复制,搭建配置方便等很多特性让 MySQL Replication 的应用很广泛,我们曾经使用过一主拖20多个从库来分担业务压力。关于 MySQL Replication 的文章网络上也有很多,但大多数都是讲如何搭建MySQL Replication,并没有说清楚如何才能搭建出高可靠的MySQL Replication。这篇文章也对半同步复制,无损复制,多源复制做了讲解。
复制有哪些用途
读写分离
灾备
高可用
线下统计
备份
复制是如何工作的
从上图中可以看到,复制的主要步骤:
master 将改变记录到二进制日志 binary log 中
slave 上的IO线程将主库上的日志复制到自己的 relay log 中
slave 上SQL线程回放中继日志的内容,使 slave 上的数据与 master 达到一致
binlog 二进制日志文件,用于记录 MySQL 的数据变更。
relay-log 中继日志文件,slave 的I/O线程读取 master 的 binlog,记录到 relay-log 中,然后 SQL 线程会读取 relay-log 日志的内容并应用到 slave 服务器。
binlog 记录的格式
STATEMENT(记录操作的SQL语句)
优点 减少了 binlog 日志量,节约IO,提高性能,易于理解
缺点 不是所有的DML语句都能被复制,有些函数UUID() 、FOUND_ROWS()、USER() 也无法被复制
ROW(记录操作的每一行数据的变化信息,RC 隔离级别,必须是 row 格式)
优点 任何情况都可以被复制,ROW 模式是最安全可靠的
缺点 产生大量的日志,特别是 copy data 的 DDL 会让日志暴涨
建议表一定要有主键
MIXED (混合模式)
先使用 STATEMENT 模式记录 binlog ,对于 STATEMENT 模式无法复制的操作使用 ROW 模式保存 binlog,MySQL 会根据执行的 SQL 语句选择日志记录方式。Bug 较多,不建议使用。
binlog Events
我们都知道 binlog 日志用于记录所有对 MySQL 的操作的变更,而这每一个变更都会对应的事件,也就是 Event,index文件记录了所有的 binlog 位置,每个 binlog 会有 header event,rotate 三个 event,binlog 的结构如下。
常见的Event如下:
Format_desc:全新的binlog日志文件
Rotate :日志分割
Table_map:表,列等元数据
Query:查询
Write_rows: 插入
Update_rows:更新
Delete_rows:删除
在了解了以上基础的内容后,我们可以带着以下的三个问题去学习复制到底是怎样工作的。
事务是如何提交的?事务提交先写 binlog 还是 redo log?
为什么 MySQL 有 binlog,还有redo log?
如何保证这两部分的日志做到一致性?
务是如何提交的?事务提交先写 binlog 还是 redo log?
以上的图片中可以看到,事务的提交主要分三个主要步骤:
InnoDB 层写 prepare log,此时SQL已经成功执行,并生成 xid 信息及 redo 和 undo 的内存日志,写入 redo log file
MySQL Server 层写 binlog (write --》 fsync)
InnoDB 层写 commit log, InnoDB存储引擎内提交,使 undo 和 redo 永久写入磁盘
为什么 MySQL 有 binlog,还有 redo log?
这个是因为 MySQL 体系结构的原因,MySQL 是多存储引擎的,不管使用那种存储引擎,都会有 binlog,而不一定有 redo log,简单的说,binlog 是 MySQL Server 层的,redo log 是 InnoDB 层的。
如何保证这两部分的日志做到一致性?
事务提交的过程上面已经说到,需要写 redo log 还要写 binlog,那么如何保证数据的一致性呢,如果不能保证写这两个文件在同一事务中,那么就会造成数据不一致,这个不一致包括MySQL crash时和主从复制的数据不一致。
面试时经常会问的一个问题,影响MySQL写入性能、数据一致性的参数有哪些?无疑是 双一参数 innodb_flush_log_at_trx_commit 和sync_binlog, 这两个参数是控制 MySQL 磁盘写入策略以及数据安全性的关键参数,MySQL 为了保证 master 和 slave 的数据一致性,就必须保证 binlog 和 InnoDB redo 日志的一致性。
参数说明如下:
innodb_flush_log_at_trx_commit(redo)
0 log buffer 每秒一次地写入 log file 中,且进行 flush 操作
1 每次事务提交时都会把 log buffer 的数据写入 log file,并进行 flush 操作
2 每次事务提交时 MySQL 都会把 log buffer 的数据写入 log file,不进行 flush 操作
sync_binlog (binlog)
0 刷新binlog_cache中的信息到磁盘由os决定
N 每N次事务提交刷新binlog_cache中的信息到磁盘
那么如何保证 binlog 和 InnoDB redo 日志的一致性,MySQL 使用内部分布式事物来保证一致性,MySQL 在 prepare 阶段会生成 xid,xid 会写入prepare log中,也会写入到binlog中,当恢复时会对比此事务的xid在两个文件中是否都有,如果都存在该xid对应的事物会提交,反之会rollback此事务,下面是几种情况分析:
当事务在 prepare 阶段 crash,将该事务 rollback。
当事务在 binlog 阶段 crash,此时日志还没有成功写入到磁盘中,启动时会 rollback 此事务
当事务在 binlog 日志已经 fsync() 到磁盘后 crash,但是InnoDB没有来得及 commit,此时 MySQL 启动时会重新将该事务重做并 commit,使 InnoDB 存储引擎的 redo log 和 binlog 始终保持一致。
总结起来说就是如果一个事物在 prepare log 阶段中落盘成功,并在 MySQL Server 层中的 binlog 也写入成功,那这个事务必定commit成功。
组提交
现在回头看事务是如何提交的那张图,会发现innodb层每个事务是并行的,但是在写binlog时,MySQL Server层就变成了串行,这是因为每次提交都会去申请prepare_commit_mutex这把锁造成的,在MySQL 5.6版本中,提供了Binary Log Group Commit 也就是组提交,Group Commit分为了三个阶段。
Flush stage:Leader线程遍历FLUSH_STAGE链表,写入binary log缓存
Sync stage :将binlog缓存sync到磁盘,当sync_binlog=1时所有该队列事务的二进制日志缓存永久写入磁盘
Commit stage: leader根据顺序让InnoDB存储引擎完成Commit
下面是我们测试组提交的一张图,可以看到组提交的TPS高不少。
1062、1053复制错误
可能DBA在使用MySQL Replication的过程中,在slave 宕机或异常时,会遇到1062等错误,大家都是使用以下方式解决。
SET GLOBAL SQL_SLAVE_SKIP_COUNTER = 1
GTID 通过空事务方式
但为什么数据会冲突呢?我们分解下复制的步骤,这里有张图很好,图片来自于网络。
我们可以看到这里有保存Replication matadata的两个文件,
relay-info.log保存了SQL线程回放到的Relay_log_name和Relay_log_pos,以及对应的Master的Master_log_name和Master_log_pos。
master-info.log保存了连接master的用户,密码,端口,Master_log_name 和 Master_log_pos等信息。
如下图,如果SQL线程正在回放,回放完后,还没来的及写到Replication matadata的文件中,就宕机了,此时重启slave后,就会出现1062错误。
在MySQL 5.6中,提供了SQL/IO thread crash-safe特性。通过将relay_log_info_repository=TABLE,relay-info将信息写入到mysql.slave_relay_log_info这张表中,不但可以保证一致性(写文件变成同一事物的原子操作),还提高了写入性能。
如上图。IO thread同理,稍有不同的是 relay-log-recover 设置为1后,slave 的 IO thread 读取 events 时,会根据从 SQL thread 回放到的位置重新读取。
复制最优的参数配置
上面讲了这么多,其实就是得出 MySQL Replication 的最可靠的参数配置。
master:
binlog_format = ROW
transaction-isolation = READ-COMMITTE
Dexpire_logs_days = 30
server-id = 327
sync_binlog = 1
innodb_flush_log_at_trx_commit = 1
innodb_support_xa = 1
slave:
log_slave_updates=1
server-id = 328
relay_log_recover = 1
relay_log_info_repository = TABLE
master_info_repository = TABLE
read_only = 1
如何提高复制效率?
MySQL 5.6提供了并行复制,但是这种并行只是基于database的。如果是基于单database的依然无法做到真正的并行回放,这个阶段很多DBA将数据库进行垂直拆分,将一个database拆分成几个database,通过设置slave_parallel_workers=n,可以进行database级别的并行复制,但对于热点业务复制延迟依然无法解决。
MySQL 5.6版本中还引入了GTID,不但降低了主从failover时,寻找filename,position的难度,更是加入到了组提交中,这也造就了MySQL 5.7版本中的Multi-Threaded Slave的出现。如下图,一组中的事务,可以并行回放。
在下图的测试中,MySQL 5.7的多线程复制极大的提升了延迟效率,在30个线程并发操作的时候还能保证平均延迟5.9秒左右,而单线程复制的延迟率基本一直在上升。
Multi-Threaded Slave 相关参数
slave-parallel-type= DATABASE /LOGICAL_CLOCK
-- DATABASE -- 基于库级别的并行复制 与5.6相同
-- LOGICAL_CLOCK -- 逻辑时钟,主上怎么并行执行,从上怎么回放。
slave-parallel-workers=16
-- 并行复制的线程数
slave_preserve_commit_order=1
--commit的顺序保持一致
半同步
我们都知道,默认的 MySQL Replication 复制为异步模式,异步也就说明会有丢失数据的可能性,MySQL在5.5版本中提供了 semi-sync replication,也就是半同步,但半同步只能说减少数据丢失的风险,所以在 MySQL 5.7版本中,MySQL 提供了 lossless semi-sync replication,也就是无损复制,可最低限度的减少数据丢失(无损复制会和半同步一样在出问题时会切换为异步复制)。
在半同步中,至少有一个Slave节点收到binlog后再返回,不能完全避免数据丢失,超时后,切回异步复制。在事物提交的过程中,在InnoDB层的 commit log 阶段后,Master 节点需要收到至少一个Slave节点回复的ACK后,才能继续下一个事物。
无损复制
在无损复制中,master把binlog发送给slave,只有在slave把binlog写到本地的relay-log里,master才会将事务提交到存储引擎层,然后把请求返回给客户端,客户端才可以看见刚才提交的事务。在一个事物提交的过程中,在MySQL Server 层的 binlog阶段后,Master节点需要收到至少一个Slave节点回复的ACK后,才能继续下一个事物。
半同步复制与无损复制的对比
ACK的时间点不同
半同步复制在InnoDB层的Commit Log后,等待ACK。
无损复制在MySQL Server层的Write binlog后,等待ACK。
主从数据一致性
半同步复制意味着在Master节点上,这个刚刚提交的事物对数据库的修改,对其他事物是可见的。
无损复制在write binlog完成后,就传输binlog,但还没有去写commit log,意味着当前这个事物对数据库的修改,其他事物也是不可见的。
半同步相关参数
rpl_semi_sync_master_enabled=1
rpl_semi_sync_slave_enabled=1
rpl_semi_sync_master_timeout=1000
rpl_semi_sync_master_wait_for_slave_count=1
rpl_semi_sync_master_wait_point=AFTER_SYNC
rpl_semi_sync_master_wait_for_slave_count=1
半同步相关事件统计
Rpl_semi_sync_master_tx_avg_wait_time
--开启Semi_sync,平均需要额外等待的时间
Rpl_semi_sync_master_net_avg_wait_time
--事务进入等待队列后,到网络平均等待时间Semi-sync的网络消耗有多大。
Rpl_semi_sync_master_status
-- 则表示当前Semi-sync是否正常工作
Rpl_semi_sync_master_no_times
--可以知道一段时间内,Semi-sync是否有超时失败过,记录了失败次数。
multi-source
然而在MySQL 5.7版本中,提供了多源复制,多源复制的出现对于分库分表的业务提供了极大的便利,目前我们已经部署了多套多源复制供统计使用。
如上图,多源复制采用多通道的模式,和普通的复制相比,就是使用FOR CHANNEL进行了分离。
CHANGE MASTER TO .... FOR CHANNEL ‘m1';
CHANGE MASTER TO .... FOR CHANNEL ‘m2';
上面我们也说到,为了提高复制效率,很多DBA会根据业务进行DB拆分,但拆分后又面临一个新的问题,就是join,join绝对是关系型数据库中最常用一个特性,然而在分布式的环境中,join是最难解决的一个问题,使用多源复制就能很好的解决这个问题。
如果数据库,表名一致如何使用多源复制?,其实只要解决了数据冲突的问题就可以使用。
如上图的分库分表架构,可以使用以下参数实现奇偶插入的方式去解决。
auto_increment_offset=1…n
auto_increment_increment=n
但这种方式需要提前考虑扩展性。