2021-02-20

1、复制概述

1.1、介绍

可以通过为服务器配置一个或多个备库的方式来进行数据同步,复制功能不仅有利于构建高性能的应用,同时也是高可用性、可扩展性、灾难恢复、备份以及数据仓库等工作的基础

复制解决的基本问题是让一台服务器的数据与其他服务器保持同步。一台主库的数据可以同步到多台备库上,备库本身也可以被配置成另外一台服务器的主库。主库和备库之间可以有多种不同的组合方式。

MySQL复制大部分是向后兼容的,新版本的服务器可以作为老版本服务器的备库,但反过来,将老版本作为新版本服务器的备库通常是不可行的,因为它可能无法解析新版本所采用的新的特性或语法,另外所使用的二进制文件的格式也可能不相同。

复制通常不会增加主库的开销,主要是启用二进制日志带来的开销,但出于备份或及时从崩溃中恢复的目的,这点开销也是必要的。除此之外,每个备库也会对主库增加一些负载(例如网络I/O开销),另外锁竞争也可能阻碍事务的提交。

1.2、复制的类型

MySQL支持两种复制方法:基于行的复制基于语句的复制。这两种方式都是通过在主库上记录二进制日志、在备库重放日志的方式来实现异步的数据复制。这意味着,在同一时间点备库上的数据可能与主库存在不一致,并且无法保证主备之间的延迟。一些大的语句可能导致备库产生几秒、几分钟甚至几个小时的延迟。

(1)基于语句的复制(默认):
在主服务器上执行的语句,从服务器执行同样的语句。

(2)基于行的复制:
把改变的内容复制到从服务器。

(3)混合类型的复制:
一旦发现基于语句无法精确复制时,就会采取基于行的复制。

1.3、复制解决的问题

  • 数据分布:可以随意地停止或开始复制,并在不同的地理位置来分布数据备份,例如不同的数据中心。
  • 负载均衡:通过MySQL复制可以将读操作分布到多个服务器上,实现对读密集型应用的优化,并且实现很方便,通过简单的代码修改就能实现基本的负载均衡。对于小规模的应用,可以简单地对机器名做硬编码或使用DNS轮询(将一个机器名指向多个IP地址)。
  • 备份:对于备份来说,复制是一项很有意义的技术补充,但复制既不是备份也不能够取代备份。
  • 高可用性和故障切换:复制能够帮助应用程序避免MySQL单点失败,一个包含复制的设计良好的故障切换系统能够显著地缩短宕机时间。
  • MYSQL升级测试:使用一个更高版本的MySQL作为备库,保证在升级全部实例前,查询能够在备库按照预期执行。

1.4、复制如何工作

MySQL复制数据的三个步骤:

  • 在主库上把数据更改记录到二进制日志中(二进制日志事件);
  • 备库将主库上的日志复制到自己的中继日志中;
  • 备库读取中继日志中的事件,将其重放到备库数据之上。
image

整体复制过程:

  1. 在主库上记录二进制日志。在每次准备提交事务完成 数据更新前,主库将数据更新的事件记录到二进制日志中。Mysql 会按事务提交的顺序而非每条语句的执行顺序来记录二进制日志。在记录二进制日志后,主库会告诉存储引擎可以提交事务了。
  2. 备库将主库的二进制日志复制到其本地的中继日志中。首先,备库会启动一个工作线程,称为 I/O 线程,I/O 线程跟主库建立一个普通的客户端连接,然后在主库上启动一个特殊的二进制转储(binlog dump)线程,这个二进制转储线程会读取主库二进制日志中的事件。它不会对时间进行轮询。如果该线程“追赶”上了主库,它将进入睡眠状态,直到主库发送信号量通知它有新的事件产生才会被唤醒,备库 I/O 线程会将接收到的事件记录到中继日志中。
  3. 备库启动 SQL 线程,执行最后一步。该线程从中继日志中读取事件并在备库执行,从而实现备库数据的更新。当 SQL 线程追赶上 I/O 线程时,中继日志通常已经在系统缓存中,所以中继日志的开销很低。SQL 线程执行的事件也可以通过配置项来决定是否写入自身的二进制日志中,这对于备库再配置备库的常见非常有用。

这种复制架构实现了获取事件重放事件的解耦,允许这两个过程异步进行。也就是说 I/O 线程能够独立于 SQL 线程之前工作。但是,这种架构也限制了复制的过程,其中最重要的一点是,在主库上并发运行的查询在备库上只能串行化执行,因为只有一个 SQL 线程来重放中继日志中的事件。

不过值得高兴的是,5.7 版本已经支持从库的并行复制了。基于二进制日志的并行复制,是在日志内容中新增了 last_committed 和 sequence_number,分别 表示事务提交的时间和上次事务提交的编号。如果事务具有相同的时间,表示这些事务是在一组内,可以进行并行回放。

2、复制的原理

2.1、基于语句的复制

在 Mysql 5.0 及之前的版本中只支持基于语句的复制(也称为逻辑复制)。基于语句的复制模式,主库会记录那些造成数据更改的 SQL 语句,当备库读取并重放这些事件时,实际上只是把主库执行过的 SQL 再执行一遍。这种方式既有优点,也有缺点。

优点

  1. 实现简单。理论上来说,只要简单地记录和执行 SQL 语句,就能够让主备保持同步。
  2. 二进制日志不会对带宽产生较大影响。二进制日志里的事件更加紧凑,占用带宽较小。

但事实上,基于语句的方式可能并不如其看起来那么便利

缺点 :

  1. 主库上的数据除了执行的语句外,可能还依赖其他因素。当主库使用 CURRENT_USER() 函数的语句,存储过程和触发器在使用基于语句的复制模式时就可能会出现问题。

2.2、基于行的复制

Mysql 5.1 开始支持基于行的复制。这种方式会将实际数据记录在二进制日志中。同样的,它也有其自身的优缺点。

优点:可以更加准确的复制数据

缺点:则是可能造成较大的开销。比如一个工资表中有一万个用户,我们把每个用户的工资+1000,那么基于行的复制则要复制一万行的内容,由此造成的开销比较大,而基于语句的复制仅仅一条语句就可以了。

由于没有哪种模式是对所有情况都是完美的,Mysql 就使复制模式可以动态切换。默认情况下使用的是基于语句的复制方式,但如果发现语句无法被正确地复制,就切换到基于行的复制模式。还可以根据需要来设置会话级别的变量 binlog_format,控制二进制日志格式。

2.3 、复制文件解读

复制过程中会使用到一些文件。前面已经介绍了二进制日志文件中继日志文件,除此之外,还有其他的文件会被用到。

  1. mysql-bin.index:当在服务器上开启二进制日志时,同时会生成一个和二进制日志同名,但以 .index 作为后缀的文件,该文件用于记录磁盘上的二进制日志文件。这里的 index 并不是表的索引,而是说这个文件的每一行包含了二进制文件的文件名。Mysql 依赖这个文件识别二进制日志文件。
  2. mysql-relay-bin-index:中继日志的索引文件,和 mysql-bin.index 的作用类似。
  3. master.info:保存备库连接主库所需要的信息文件。格式为纯文本(每行一个值),不同的 Mysql 版本,记录的信息也可能不太一样。此文件不能删除,否则备库再重启后不能连接主库。这个文件以文本的方式记录了复制用户的密码,所以要注意此文件的权限控制。
  4. relay-log.info:记录当前备库复制的二进制日志和中继日志位置文件。

使用这些文件来记录 Mysql 复制和日志状态是一种非常粗糙的方式。更不幸的是,它们不是同步写的。如果服务器断电并且文件数据没有被刷新到磁盘,在重启服务器后,文件中记录的数据可能是错误。不过好在这些问题以及在 5.5 版本里做了改进。

2.4 、发送复制事件到其它备库

log_slave_update 选项可以让备库编程其它服务器的主库。在设置该选项后,Mysql 会将其执行过的事件记录到它自己的二进制日志中。这样它的备库就可以从其日志中检索并执行事件。下图阐述了这一过程:

image

在这种场景下,主库将数据更新事件写入二进制日志,第一个备库提取并执行这个事件。这个时候一个事件的生命周期应该已经结束了。但由于设置了 log_slave_updates,备库会将这个事件写到它自己的二进制日志中。这样第二个备库就可以从第一个备库中,将事件提取到它的中继日志中并执行。

这意味着作为源服务器的主库可以将其数据变化传递给没有与其直接相连的备库上。默认情况下,这个选项是被打开的,这样在连接到备库时就不需要重启服务器。

2.5、复制过滤

复制过滤可以让你只复制服务器中的一部分数据,有两种复制过滤:

  1. 在master上过滤二进制日志中的事件
  2. 在slave上过滤中继日志中的事件。
image

4、复制拓扑

4.1、单一master和多slave

一个master和一个slave组成复制系统是最简单的情况。Slave之间并不相互通信,只能与master进行通信。

在实际应用场景中,MySQL复制90%以上都是一个Master复制到一个或者多个Slave的架构模式,主要用于读压力比较大的应用的数据库端廉价扩展解决方案。因为只要Master和Slave的压力不是太大(尤其是Slave端压力)的话,异步复制的延时一般都很少很少。尤其是自从Slave端的复制方式改成两个线程处理之后,更是减小了Slave端的延时问题。

[图片上传失败...(image-2ca0e2-1613832505375)]

如果写操作较少,而读操作很时,可以采取这种结构。你可以将读操作分布到其它的slave,从而减小master的压力。但是,当slave增加到一定数量时,slave对master的负载以及网络带宽都会成为一个严重的问题。

这种结构虽然简单,但是,它却非常灵活,足够满足大多数应用需求。一些建议:
(1) 不同的slave扮演不同的作用(例如使用不同的索引,或者不同的存储引擎);
(2) 用一个slave作为备用master,只进行复制;
(3) 用一个远程的slave,用于灾难恢复;

4.2、主动模式的Master-Master

Master-Master复制的两台服务器,既是master,又是另一台服务器的slave。如图:

image

主动的Master-Master复制有一些特殊的用处。例如,地理上分布的两个部分都需要自己的可写的数据副本。这种结构最大的问题就是更新冲突。假设一个表只有一行(一列)的数据,其值为1,如果两个服务器分别同时执行如下语句:
在第一个服务器上执行:

mysql> UPDATE tbl SET col=col + 1;

在第二个服务器上执行:

mysql> UPDATE tbl SET col=col * 2;

那么结果是多少呢?一台服务器是4,另一个服务器是3,但是,这并不会产生错误。
实际上,MySQL并不支持其它一些DBMS支持的多主服务器复制(Multimaster Replication),这是MySQL的复制功能很大的一个限制(多主服务器的难点在于解决更新冲突),但是,如果你实在有这种需求,你可以采用MySQL Cluster,以及将Cluster和Replication结合起来,可以建立强大的高性能的数据库平台。但是,可以通过其它一些方式来模拟这种多主服务器的复制。

4.3、主动-被动模式的Master-Master

这是master-master结构变化而来的,它避免了M-M的缺点,实际上,这是一种具有容错和高可用性的系统。它的不同点在于同一时刻其中一个服务只能进行只读操作。如图:

image

4.4、带从服务器的Master-Master结构

这种结构的优点就是提供了冗余。在地理上分布的复制结构,它不存在单一节点故障问题,而且还可以将读密集型的请求放到slave上。

image

5、复制和容量规划

5.1、为什么复制无法扩展写操作

 糟糕的服务容量比例的根本原因是不能像分发读操作那样把写操作等同地分发到更多服务器上。换句话说,**复制只能扩展读操作,无法扩展写操作**。

 你可能想知道到底有没有办法使用复制来增加写入能力。答案是否定的,**根本不行**。对数据进行分区是唯-可以扩展写入的方法。一些读者可能会想到使用主-主拓扑结构(参阅前面介绍的“主动-主动模式下的主-主复制”)并为两个服务器执行写操作。这种配置比主备结构能支持稍微多一点的写入,因为可以在两台服务器之间共享串行化带来的开销。如果每台服务器上执行50%的写入,那复制的执行量也只有50%需要串行化。理论上讲,这比在一台机器上(主库)对100%的写入并发执行,而在另外一一台机器(备库)上对100%的写入做串行化要更优。

 这可能看起来很吸引人,然而这种配置还比不上单台服务器能支持的写入。一个有50%的写入被串行化的服务器性能比一台全部写入都并行化的服务器性能要低。这是这种策略不能扩展写入的原因。它只能在两台服务器间共享串行化写入的缺点。所以“链中最弱的一环”并不是那么弱,它只提供了比主动-被动复制稍微好点的性能,但是增加了很大的风险,通常不能带来任何好处。

6、复制管理和维护

6.1、监控复制

mysql> show master status;
+------------------+-----------+--------------+------------------+
| File             | Position  | Binlog_Do_DB | Binlog_Ignore_DB |
+------------------+-----------+--------------+------------------+
| mysql-bin.000013 | 971159980 |              |                  |
+------------------+-----------+--------------+------------------+
1 row in set (0.00 sec)
mysql> show master logs;
+------------------+------------+
| Log_name         | File_size  |
+------------------+------------+
| mysql-bin.000011 | 1073949250 |
| mysql-bin.000012 | 1073751139 |
| mysql-bin.000013 |  971159980 |
+------------------+------------+
3 rows in set (0.00 sec)
mysql> show binlog events in 'mysql-bin.000013' from 971245404;
+------------------+-----------+-------------+-----------+-------------+-----------------------------------------+
| Log_name         | Pos       | Event_type  | Server_id | End_log_pos | Info                                    |
+------------------+-----------+-------------+-----------+-------------+-----------------------------------------+
| mysql-bin.000013 | 971245404 | Query       |         1 |   971245474 | BEGIN                                   |
| mysql-bin.000013 | 971245474 | Table_map   |         1 |   971245651 | table_id: 175 (attend.sys_user)         |
| mysql-bin.000013 | 971245651 | Update_rows |         1 |   971246116 | table_id: 175 flags: STMT_END_F         |
| mysql-bin.000013 | 971246116 | Table_map   |         1 |   971246185 | table_id: 176 (attend.sys_user_branch)  |
| mysql-bin.000013 | 971246185 | Write_rows  |         1 |   971246266 | table_id: 176 flags: STMT_END_F         |
| mysql-bin.000013 | 971246266 | Table_map   |         1 |   971246335 | table_id: 176 (attend.sys_user_branch)  |
| mysql-bin.000013 | 971246335 | Delete_rows |         1 |   971246416 | table_id: 176 flags: STMT_END_F         |
| mysql-bin.000013 | 971246416 | Xid         |         1 |   971246443 | COMMIT /* xid=3065249192 */             |           |
+------------------+-----------+-------------+-----------+-------------+-----------------------------------------+
8 rows in set (0.00 sec)

6.2、查看复制库延迟

mysql> show slave status\G

6.3、确认主备是否一致

mysql没有内建方法来确认主备是否一致。checksum table可以校验数据,但是复制正在进行时,这种方法不可行。

可以使用percona提供的pt-table-checksum来校验主备数据是否一致。(结合pt-table-sync使用,重新同步数据)

7、复制的问题和解决方案

7.1、数据损坏或丢失

  • 主库意外关闭

问题未发生,避免方案:设置主库的 sync_binlog 选项为 1。此选项表示 MySQL 是否控制 binlog 的刷新。当设置为 1 时,表示每次事务提交,MySQL 都会把 binlog 刷下去,是最安全,性能损耗也最大的设置
问题已发生,解决方案:指定备库从下一个二进制日志的开头重新读日志。但是一些日志事件将永久性丢失。可以使用 Percona Toolkit 中的 pt-table-checksum 工具来检查主备一致性,以便于修复。

  • 备库意外关闭

备库意外关闭重启时,会去读 master.info 文件以找到上次停止复制的位置。但是在意外关闭的情况下,这个文件存储的信息可能是错误的。此外,备库也可能会尝试重新执行一些二进制文件,这可能会导致唯一索引错误。我们可以通过 Percona Toolkit 中的 pt-slave-restart 工具,帮助备库重新执行日志文件。

如果使用的是 InnoDB 表,可以在重启后观察 MySQL 的错误日志。InnoDB 在恢复过程中会打印出恢复点的二进制日志坐标,可以使用这个值来决定备库指向主库的偏移量。

  • 主库二进制日志损坏

如果主库上的二进制日志损坏,除了忽略损坏的位置外,别无选择。在忽略存货位置后,我们可以通过 FLUSH LOGS 命令在主库开始一个新的日志文件,然后将备库指向该文件的开始位置。

  • 备库中继日志损坏

如果主库上的日志是完好的,有两种解决方案:
1) 手工处理。找到 master binlog 日志的 pos 点,然后重新同步。

2) 自动处理。mysql5.5 考虑到 slave 宕机中继日志损坏这一问题,只要在 slave 的的配置文件 my.cnf 里增加一个参数 relay_log_recovery=1 即可。

  • 二进制日志与 InnoDB 事务日志不同步

由于各种各样的原因,MySQL 的复制碰到服务器崩溃、断电、磁盘损坏、内存或网络错误时,很难恢复当时丢失的数据。几乎都需要从某个点开始重启复制。

7.2、未定义的服务器ID

如果没有再 my.cnf 里定义服务器 ID,虽然可以通过 CHANGE MASTER TO 来设置备库,但在启动复制时会遇到:

mysql> START SLAVE;
ERROR 1200 (HY000): The server us bit configured as slave; fix in config file or with CHANGE MASTER TO

这个报错可能会让人困惑。因为我们可能已经通过 CHANGE MASTER TO 设置了备库,并且通过 SHOW MASTER STATUS 也确认了,为什么还会有这样的报错呢?我们通过 SELECT @@server_id 可以获得一个值,要注意的是,这个值只是默认值,我们必须为备库显式地设置服务器 ID。也就是在 my.cnf 里显示的设置服务器 ID。

7.3、对未复制数据的依赖性

如果在主库上有备库上不存在的数据库或数据表,复制就很容易中断,反之亦然。
对于前者,假设在主库上有一个 single_master 表,备库没有。在主库上对此表进行操作后,备库在尝试回放这些操作时就会出现问题,导致复制中断。

对于后者,假设备库上有一个 single_slave 表,主库没有。在主库上执行创建 single_slave 表的语句时,备库在回放该建表语句时就会出现问题。

对于此问题,我们能做的就是做好预防:

  1. 主备切换时,尽量在切换后对比数据,查清楚是否有不一致的表或库。
  2. 一定不要在备库执行写操作。

7.4、丢失的临时表

临时表和基于语句的复制方式不相容。如果备库崩溃或者正常关闭,任何复制线程拥有的临时表都会丢失。重启备库后,所有依赖于该临时表的语句都会失败。

复制时出现找不到临时表的异常时,可以做:

  1. 直接跳过错误,或者手动地创建一个名字和结构相同的表来代替消失的的临时表。

临时表的特性:

  1. 只对创建临时表的连接可见。不会和其他拥有相同名字的临时表的连接起冲突;
  2. 随着连接关闭而消失,无须显式的移除它们。

7.5、InnoDB 加锁读导致主备数据不一致

使用共享锁,串行化更新,保证备库复制时数据一致。

某些情况下,加锁读可以防止混乱。假设有两张表:tab1 没有数据,tab2 只有一行数据,值为 99。此时,有两个事务更新数据。事务 1 将 tab2 的数据插入到 tab1,事务 2 更新 tab2。

image
  1. 事务 1 使用获取 tab2 数据时,加入共享锁,并插入 tab1;
  2. 同时,事务 2 更新 tab2 数据时,由于写操作的排它锁机制,无法获取 tab2 的锁,等待;
  3. 事务 1 插入数据后,删除共享锁,提交事务,写入 binlog(此时 tab1 和 tab2 的记录值 都是 99);
  4. 事务 2 获取到锁,更新数据,提交事务,写入 binlog(此时 tab1 的记录值为 99,tab2 的记录值为 100)。

上述过程中,第二步非常重要。事务 2 尝试去更新 tab2 表,这需要在更新的行上加排他锁(写锁)。排他锁与其他锁不相容,包括事务 1 在行记录上加的共享锁。因此事务 2 需要等待事务 1 完成。备库在根据 binlog 进行复制时,会按同样的顺序先执行事务 1,再执行事务 2。主备数据一致。

同样的过程,如果事务 1 在第一步时没有加共享锁,流程就变成:

image
  1. 事务 1 无锁读取 tab2 数据,并插入 tab1(此时 tab1 和 tab2 的记录值 都是 99);
  2. 同时,事务 2 更新 tab2 数据,先与事务 1 提交事务,写入 binlog(此时 tab1 的记录值为 99,tab2 的记录值为 100);
  3. 事务 1 提交事务,写入 binlog(此时记录值无变化);

要注意的是,上述过程中,事务 2 先提交,先写入 binlog。在备库复制时,同样先执行事务 2,将 tab2 的记录值更新为 100。然后执行事务 1,读取 tab2 数据,插入 tab1,所以最终的结果是,tab1 的记录值和 tab2 的记录值都是 100。很明显,数据和主库有差异。

7.6、复制延迟过大

产生延迟的两种方式

  1. 突然产生延迟,然后再跟上;
  2. 稳定的延迟增大

前者通常是由于一条执行时间过长的 SQL 导致,而后者即使在没有慢语句也会出现。

对于前者,我们可以通过备库上的慢查询日志来进行优化。在备库上开启 log_slow_slave_statement 选项,可以在慢查询日志中记录复制线程执行的语句。

你可能感兴趣的:(2021-02-20)