1. 复制概述
MySQL 内置的复制功能是构建基于 MySQL 的大规模、高性能应用的基础,复制解决的基本问题是让一台服务器的数据与其他服务器保持同步。接下来,我们将从复制概述及原理、复制的配置、常见的问题及解决方法来学习 MySQL 的复制功能。
1.1 复制解决的问题
下面是复制常见的用途:
数据分布。Mysql 复制通常不会对带宽造成很大压力,但在 5.1 版本中引入的基于行的复制会比传统的基于语句的复制模式产生更大的带宽压力。你可以随意地停止或开始复制,并在不同的地理位置来分布数据备份,例如不同的数据中心。另外,即使在不稳定的网络环境下,远程复制也可以工作。但如果未来保存很低的复制延迟,最好有一个稳定、低延迟的连接。
负载均衡。通过 Mysql 复制,可以将读操作分布到多个服务器上,实现对读密集型应用的优化,并且很容易实现,通过简单的代码修改就能实现基本的负载均衡。对应小规模的应用,可以简单的使用 DNS 轮询(将一个机器名指向多个 IP 地址)。
备份。对于备份来说,负载是一项很有意义的技术补充。
高可用性和故障切换。负载能够帮助应用避免 Mysql 单点失败,一个使用复制的设计良好的系统能够显著的缩短宕机时间。
Mysql 升级测试。这是比较常见的做法,在更新 Mysql 版本前,先使用将要更新的版本作为备库,保证更新版本不会对系统造成影响。
1.2 复制是如何工作的?
在详细介绍如何设置复制之前,让我们先看看 Mysql 实际上是如何进行数据复制的。
总的来说,复制有三个步骤:
在主库上把数据更改写入到二进制日志(Binary Log)中(这些记录被称为二进制日志事件)。
备库将主库上的日志复制到自己的中继日志(Relay Log)中。
备库读取中继日志中的事件,将其更改同步到备库。
以上是复制的简单概述,下图描述了复制的细节:
整体复制过程:
在主库上记录二进制日志。在每次准备提交事务完成 数据更新前,主库将数据更新的事件记录到二进制日志中。Mysql 会按事务提交的顺序而非每条语句的执行顺序来记录二进制日志。在记录二进制日志后,主库会告诉存储引擎可以提交事务了。
备库将主库的二进制日志复制到其本地的中继日志中。首先,备库会启动一个工作线程,称为 I/O 线程,I/O 线程跟主库建立一个普通的客户端连接,然后在主库上启动一个特殊的二进制转储(binlog dump)线程,这个二进制转储线程会读取主库二进制日志中的事件。它不会对时间进行轮询。如果该线程“追赶”上了主库,它将进入睡眠状态,直到主库发送信号量通知它有新的事件产生才会被换新,备库 I/O 线程会将接收到的事件记录到中继日志中。
备库启动 SQL 线程,执行最后一步。该线程从中继日志中读取事件并在备库执行,从而实现备库数据的更新。当 SQL 线程追赶上 I/O 线程时,中继日志通常已经在系统缓存中,所以中继日志的开销很低。SQL 线程执行的事件也可以通过配置项来决定是否写入自身的二进制日志中,这对于备库再配置备库的常见非常有用。
这种复制架构实现了获取事件和重放事件的解耦,允许这两个过程异步进行。也就是说 I/O 线程能够独立于 SQL 线程之前工作。但是,这种架构也限制了复制的过程,其中最重要的一点是,在主库上并发运行的查询在备库上只能串行化执行,因为只有一个 SQL 线程来重放中继日志中的事件。
不过值得高兴的是,5.7 版本已经支持从库的并行复制了。基于二进制日志的并行复制,是在日志内容中新增了 last_committed 和 sequence_number,分别 表示事务提交的时间和上次事务提交的编号。如果事务具有相同的时间,表示这些事务是在一组内,可以进行并行回放。
2. 复制的原理
我们已经了解了复制的一些基本概念,接下来我们要更深入的了解复制,看看复制究竟是如何工作的,有哪些优缺点。
2.1 基于语句的复制
在 Mysql 5.0 及之前的版本中只支持基于语句的复制(也称为逻辑复制)。基于语句的复制模式,主库会记录那些造成数据更改的 SQL 语句,当备库读取并重放这些事件时,实际上只是把主库执行过的 SQL 再执行一遍。这种方式既有优点,也有缺点。
优点是:
实现简单。理论上来说,只要简单地记录和执行 SQL 语句,就能够让主备保持同步。
二进制日志不会对带宽产生较大影响。二进制日志里的事件更加紧凑,占用带宽较小。
但事实上,基于语句的方式可能并不如其看起来那么便利,其缺点是:
主库上的数据除了执行的语句外,可能还依赖其他因素。当主库使用 CURRENT_USER() 函数的语句,存储过程和触发器在使用基于语句的复制模式时就可能会出现问题。
2.2 基于行的复制
Mysql 5.1 开始支持基于行的复制。这种方式会将实际数据记录在二进制日志中。同样的,它也有其自身的优缺点。
它的优点是可以更加准确的复制数据,而缺点,则是可能造成较大的开销。比如一个工资表中有一万个用户,我们把每个用户的工资 +1000,那么基于行的复制则要复制一万行的内容,由此造成的开销比较大,而基于语句的复制仅仅一条语句就可以了。
由于没有哪种模式是对所有情况都是完美的,Mysql 就使复制模式可以动态切换。默认情况下使用的是基于语句的复制方式,但如果发现语句无法被正确地复制,就切换到基于行的复制模式。还可以根据需要来设置会话级别的变量 binlog_format,控制二进制日志格式。
2.3 复制文件解读
复制过程中会使用到一些文件。前面已经介绍了二进制日志文件和中继日志文件,除此之外,还有其他的文件会被用到。
mysql-bin.index:当在服务器上开启二进制日志时,同时会生成一个和二进制日志同名,但以 .index 作为后缀的文件,该文件用于记录磁盘上的二进制日志文件。这里的 index 并不是表的索引,而是说这个文件的每一行包含了二进制文件的文件名。Mysql 依赖这个文件识别二进制日志文件。
mysql-relay-bin-index:中继日志的索引文件,和 mysql-bin.index 的作用类似。
master.info:保存备库连接主库所需要的信息文件。格式为纯文本(每行一个值),不同的 Mysql 版本,记录的信息也可能不太。此文件不能删除,否则备库再重启后不能连接主库。这个文件以文本的方式记录了复制用户的密码,所以要注意此文件的权限控制。
relay-log.info:记录当前备库复制的二进制日志和中继日志位置文件。
使用这些文件来记录 Mysql 复制和日志状态是一种非常粗糙的方式。更不幸的是,它们不是同步写的。如果服务器断电并且文件数据没有被刷新到磁盘,在重启服务器后,文件中记录的数据可能是错误。不过好在这些问题以及在 5.5 版本里做了改进。
2.4 发送复制事件到其它备库
log_slave_update 选项可以让备库编程其它服务器的主库。在设置该选项后,Mysql 会将其执行过的事件记录到它自己的二进制日志中。这样它的备库就可以从其日志中检索并执行事件。下图阐述了这一过程:
在这种场景下,主库将数据更新事件写入二进制日志,第一个备库提取并执行这个事件。这个时候一个事件的生命周期应该已经结束了。但由于设置了 log_slave_updates,备库会将这个事件写到它自己的二进制日志中。这样第二个备库就可以从第一个备库中,将事件提取到它的中继日志中并执行。
这意味着作为源服务器的主库可以将其数据变化传递给没有与其直接相连的备库上。默认情况下,这个选项是被打开的,这样在连接到备库时就不需要重启服务器。
当第一个备库把自主库获得的事件写入到其它二进制日志中时,这个事件在备库二进制日志中的位置与其主库二进制日志中的位置几乎肯定是不相同的,可能在不同的日志文件或文件内不同的位置。这意味着你不能假定所有拥有同一逻辑复制点的服务器拥有相同的日志坐标。
小结
复制功能是 MySQL 高扩展性的基础,常见的读写分离就使用了复制。
复制使用了三个线程。master 的日志线程,将事件写入 binlog,slave 的 IO 线程获取 binlog,并将其写入 relaylog,SQL 线程重放 relaylog 日志。
复制有基于语句复制和基于行的复制。
参考:
高性能 MySQL – 第 3 版;