《高性能MySQL第3版》读书笔记(三)

Published: 04 Feb 2019 Category: mysql

十、 复制

MySQL内建的复制功能是构建基于MySQL的大规模、高性能应用的基础,这类应用使用所谓的“水平扩展”的架构。

MySQL支持两种复制方式:基于行的复制和基于语句的复制。

这两种方式都是通过在主库上记录二进制日志(2)、在备库重放日志的方式来实现异步的数据复制。这意味着,在同一时间点备库上的数据可能与主库存在不一致,并且无法保证主备之间的延迟。

正因为复制提供了如此重要和复杂的功能,服务器本身不提供所有其他你需要的功能,例如,配置、监控、管理和优化。第三方工具可以很好地帮助你。笔者认为最值得关注的工具是是Percona Toolkit和Percona XtraBackup,它们能够很好地改进你对复制的使用。

10.1 复制比较常见的用途

  • 数据分布(物理容灾备份):MySQL复制通常不会对带宽造成很大的压力。
  • 负载均衡:可以将读操作分布到多个服务器上,实现对读密集型应用的优化。
  • 备份:对于备份来说,复制是一项很有意义的技术补充,但复制既不是备份也不能够取代备份。
  • 高可用性和故障切换:避免MySQL单点失败,一个包含复制的设计良好的故障切换系统能够显著地缩短宕机时间
  • MySQL升级测试:使用一个更高版本的MySQL作为备库,保证在升级全部实例前,查询能够在备库按照预期执行。

10.2 复制如何工作

总的来说,复制有三个步骤:

  1. 在主库上把数据更改记录到二进制日志(Binary Log)中(这些记录被称为二进制日志事件)。
  2. 备库将主库上的日志复制到自己的中继日志(Relay Log)中。
  3. 备库读取中继日志中的事件,将其重放到备库数据之上。

10.3 复制的原理

10.3.1 基于语句的复制

基于语句的复制模式下,主库会记录那些造成数据更改的查询,当备库读取并重放这些事件时,实际上只是把主库上执行过的SQL再执行一遍。

这种方式最明显的好处是实现相当简单。另一个好处是二进制日志里的事件更加紧凑,所以相对而言,基于语句的模式不会使用太多带宽。

但事实上基于语句的方式可能并不如其看起来那么便利。因为主库上的数据更新除了执行的语句外,可能还依赖于其他因素。例如,同一条SQL在主库和备库上执行的时间可能稍微或很不相同,因此在传输的二进制日志中,除了查询语句,还包括了一些元数据信息,如当前的时间戳。

另外一个问题是更新必须是串行的。这需要更多的锁;另外不是所有的存储引擎都支持这种复制模式。

10.3.2 基于行的复制

这种方式会将实际数据记录在二进制日志中,跟其他数据库的实现比较相像。

最大的好处是可以正确地复制每一行(所有场景都能处理,且减少锁的使用)。由于无须重放更新主库数据的查询,使用基于行的复制模式能够更高效地复制数据。

但在另一方面,下面这条语句使用基于语句的复制方式代价会小很多:

mysql> UPDATE enormous_table SET col1 = 0;

由于这条语句做了全表更新,使用基于行的复制开销会很大,因为每一行的数据都会被记录到二进制日志中,这使得二进制日志事件非常庞大。并且会给主库上记录日志和复制增加额外的负载,更慢的日志记录则会降低并发度。

另外,由于语句并没有在日志里记录,因此无法判断执行了哪些SQL。

基于行的日志无法处理诸如在备库修改表的schema这样的情况,而基于语句的日志可以。

由于没有哪种模式对所有情况都是完美的,MySQL能够在这两种复制模式间动态切换。默认情况下使用的是基于语句的复制方式,但如果发现语句无法被正确地复制,就切换到基于行的复制模式。

10.4 复制拓扑

无论哪种拓扑,记住下面的基本原则:

  • 一个MySQL备库实例只能有一个主库。
  • 每个备库必须有一个唯一的服务器ID。
  • 一个主库可以有多个备库(或者相应的,一个备库可以有多个兄弟备库)。
  • 如果打开了log_slave_updates选项,一个备库可以把其主库上的数据变化传播到其他备库。

10.4.1 一主库多备库

在有少量写和大量读时,这种配置是非常有用的。

它的一些用途:

  • 为不同的角色使用不同的备库(例如添加不同的索引或使用不同的存储引擎)。
  • 把一台备库当作待用的主库,除了复制没有其他数据传输。
  • 将一台备库放到远程数据中心,用作灾难恢复。
  • 延迟一个或多个备库,以备灾难恢复。
  • 使用其中一个备库,作为备份、培训、开发或者测试使用服务器。

10.4.2 主动-主动模式下的主-主复制

主-主复制(也叫双主复制或双向复制)包含两台服务器,每一个都被配置成对方的主库和备库,换句话说,它们是一对主库。

这种配置最大的问题是如何解决冲突,两个可写的互主服务器导致的问题非常多。

  • 在两台机器上根据不同的顺序更新,可能会导致数据不同步。
  • 当正常的复制发生错误停止了,但应用仍在同时向两台服务器写入数据,这时候会发生什么呢?你不能简单地把数据从一台服务器复制到另外一台,因为这两台机器上需要复制的数据都可能发生了变化。解决这个问题将会非常困难。

10.4.3 主动-被动模式下的主-主复制

这是前面描述的主-主结构的变体,它能够避免我们之前讨论的问题。这也是构建容错性和高可用性系统的非常强大的方式,主要区别在于其中的一台服务器是只读的被动服务器,

这种方式使得反复切换主动和被动服务器非常方便,因为服务器的配置是对称的。这使得故障转移和故障恢复很容易。它也可以让你在不关闭服务器的情况下执行维护、优化表、升级操作系统(或者应用程序、硬件等)或其他任务。

设置主动-被动的主-主拓扑结构在某种意义上类似于创建一个热备份,但是可以使用这个“备份”来提高性能,例如,用它来执行读操作、备份、“离线”维护以及升级等。真正的热备份做不了这些事情。然而,你不会获得比单台服务器更好的写性能(稍后会提到)。

10.4.4 拥有备库的主-主结构

和上述两种方式类似,不同的是,为每个主库增加了一个备库。

10.4.5 环形复制

双主结构实际上是环形结构的一种特例。

环形结构可以有三个或更多的主库。每个服务器都是在它之前的服务器的备库,是在它之后的服务器的主库。

环形结构没有双主结构的一些优点,例如对称配置和简单的故障转移,并且完全依赖于环上的每一个可用节点,这大大增加了整个系统失效的几率。如果从环中移除一个节点,这个节点发起的事件就会陷入无限循环:它们将永远绕着服务器链循环。因为唯一可以根据服务器ID将其过滤的服务器是创建这个事件的服务器。总地来说,环形结构非常脆弱,应该尽量避免。

10.4.6 主库、分发主库以及备库

之前提到当备库足够多时,会对主库造成很大的负载。每个备库会在主库上创建一个线程,并执行binlog dump命令。该命令会读取二进制日志文件中的数据并将其发送给备库。每个备库都会重复这样的工作,它们不会共享binlog dump的资源。

因此,如果需要多个备库,一个好办法是从主库移除负载并使用分发主库。分发主库事实上也是一个备库,它的唯一目的就是提取和提供主库的二进制日志。多个备库连接到分发主库,这使原来的主库摆脱了负担。为了避免在分发主库上做实际的查询,可以将它的表修改为blackhole存储引擎。

10.4.7 树或金字塔形

如果正在将主库复制到大量的备库中。不管是把数据分发到不同的地方,还是提供更高的读性能,使用金字塔结构都能够更好地管理。

这种设计的好处是减轻了主库的负担,就像前一节提到的分发主库一样。它的缺点是中间层出现的任何错误都会影响到多个服务器。如果每个备库和主库直接相连就不会存在这样的问题。同样,中间层次越多,处理故障会更困难、更复杂。

10.5 复制和容量规划

复制只能扩展读操作,无法扩展写操作。对数据进行分区是唯一可以扩展写入的方法。

10.6 复制管理和维护

要做的工作有:监控和管理复制拓扑

  • 监控备库落后主库的延迟,一般用心跳的办法。
  • 监控主备是否一致:Percona Toolkit里的pt-table-checksum能够解决这个问题。其主要特性是用于确认备库与主库的数据是否一致。工作方式是通过在主库上执行INSERT…SELECT查询。
    这些查询对数据进行校验并将结果插入到一个表中。这些语句通过复制传递到备库,并在备库执行一遍,然后可以比较主备上的结果是否一样。由于该方法是通过复制工作的,它能够给出一致的结果而无须同时把主备上的表都锁上。

10.6.1 从主库重新同步备库

传统的修复不一致的办法是关闭备库,然后重新从主库复制一份数据。当备库数据不一致的问题可能导致严重后果时,一旦发现就应该将备库停止并从生产环境移除,然后再从一个备份中克隆或恢复备库。

这种方法的缺点是不太方便,特别是数据量很大时。如果能够找出并修复不一致的数据,要比从其他服务器上重新克隆数据要有效得多。如果发现的不一致并不严重,就可以保持备库在线,并重新同步受影响的数据。

最简单的办法是使用mysqldump转储受影响的数据并重新导入。在整个过程中,如果数据没有发生变化,这种方法会很好。你可以在主库上简单地锁住表然后进行转储,再等待备库赶上主库,然后将数据导入到备库中。

虽然这种方法在许多场景下是可行的,但在一个繁忙的服务器上有可能行不通。另外一个缺点是在备库上通过非复制的方式改变数据。通过复制改变备库数据(通过在主库上执行更新)通常是一种安全的技术,因为它避免了竞争条件和其他意料外的事情。如果表很大或者网络带宽受限,转储和重载数据的代价依然很高。当在一个有一百万行的表上只有一千行不同的数据呢?转储和重载表的数据是非常浪费资源的。

pt-table-sync是Percona Toolkit中的另外一个工具,可以解决该问题。该工具能够高效地查找并解决表之间的不同。它同样通过复制工作,在主库上执行查询,在备库上重新同步,这样就没有竞争条件。它是结合pt-table-checksum生成的checksum表来工作的,所以只能操作那些已知不同步的表的数据块。但该工具不是在所有场景下都有效。为了正确地同步主库和备库,该工具要求复制是正常的,否则就无法工作。pt-table-sync设计得很高效,但当数据量非常大时效率还是会很低。

10.6.2 改变主库

如果这是计划内的操作,会比较容易。只需在备库简单地使用CHANGE MASTER TO命令,并指定合适的值。大多数值都是可选的。只需要指定需要改变的项即可。备库将抛弃之前的配置和中继日志并从新的主库开始复制。同样新的参数会被更新到master.info文件中,这样就算重启,备库配置信息也不会丢失。

整个过程中最难的是获取新主库上合适的二进制日志位置,这样备库才可以从和老主库相同的逻辑位置开始复制。

把备库提升为主库要更困难一点。有两种场景需要将备库替换为主库,一种是计划内的提升,一种是计划外的提升。

在计划内的提升,理论上是很简单的。简单来说,有以下步骤:

1. 停止向老的主库写入。
2. 让备库追赶上主库(通过`FLUSH TABLES WITH READ LOCK`,可选的,会简化下面的步骤)。
3. 将一台备库配置为新的主库。
4. 将备库和写操作指向新的主库,然后开启主库的写入

更深入一点,下面是大多数配置需要的步骤:

1. 停止当前主库上的所有写操作。如果可以,最好能将所有的客户端
程序关闭(除了复制连接)。为客户端程序建立一个“do not run”这
样的类似标记可能会有所帮助。如果正在使用虚拟IP地址,也可以
简单地关闭虚拟IP,然后断开所有的客户端连接以关闭其打开的事
务。
2. 通过FLUSH TABLES WITH READ LOCK在主库上停止所有活跃的写
入,这一步是可选的。也可以在主库上设置read_only选项。从这
一刻开始,应该禁止向即将被替换的主库做任何写入。因为一旦它
不是主库,写入就意味着数据丢失。注意,即使设置read_only也
不会阻止当前已存在的事务继续提交。为了更好地保证这一点,可
以“kill”所有打开的事务,这将会真正地结束所有写入。
3. 选择一个备库作为新的主库,并确保它已经完全跟上主库(例如,
让它执行完所有从主库获得的中继日志)。
4. 确保新主库和旧主库的数据是一致的。可选。
5. 在新主库上执行STOP SLAVE。
6. 在新主库上执行CHANGE MASTER TO MASTER_HOST='',然后再执
行RESET SLAVE,使其断开与老主库的连接,并丢弃master.info里记
录的信息(如果连接信息记录在my.cnf里,会无法正确工作,这也
是我们建议不要把复制连接信息写到配置文件里的原因之一)。
7. 执行SHOW MASTER STATUS记录新主库的二进制日志坐标。
8. 确保其他备库已经追赶上。
9. 关闭旧主库。
10. 在MySQL 5.1及以上版本中,如果需要,激活新主库上事件。
11. 将客户端连接到新主库。
12. 在每台备库上执行CHANGE MASTER TO语句,使用之前通过SHOW
MASTER STATUS获得的二进制日志坐标,来指向新的主库。

在计划外的提升,要考虑备库的选择和数据丢失的问题。以下是对主备拓扑结构中的备库进行提升的过程:

1. 确定哪台备库的数据最新。检查每台备库上SHOW SLAVE STATUS命
令的输出,选择其中Master_Log_File/read_Master_Log_Pos的值
最新的那个。
2. 让所有备库执行完所有其从崩溃前的旧主库那获得的中继日志。如
果在未完成前修改备库的主库,它会抛弃剩下的日志事件,从而无
法获知该备库在什么地方停止。
3. 执行前一小节的5~7步。
4. 比较每台备库和新主库上的
Master_Log_File/Read_Master_Log_Pos的值。
5. 执行前一小节的10~12步。

十一、 可扩展的MySQL

11.1 向上扩展

向上扩展(有时也称为垂直扩展)意味着购买更多性能强悍的硬件,对很多应用来说这是唯一需要做的事情。这种策略有很多好处。例如,单台服务器比多台服务器更加容易维护和开发,能显著节约开销。在单台服务器上备份和恢复应用同样很简单,因为无须关心一致性或者哪个数据集是权威的。当然,还有一些别的原因。从复杂性的成本来说,向上扩展比向外扩展更简单。

11.2 向外扩展

可以把向外扩展(有时也称为横向扩展或者水平扩展)策略划分为三个部分:复制、拆分,以及数据分片(sharding)。

最简单也最常见的向外扩展的方法是通过复制将数据分发到多个服务器上,然后将备库用于读查询。这种技术对于以读为主的应用很有效。它也有一些缺点,例如重复缓存。

另外一个比较常见的向外扩展方法是将工作负载分布到多个“节点”。

一种是按功能拆分,或者说按职责拆分,意味着不同的节点执行不同的任务。比如为OLTP和OLAP工作负载设计不同的服务器。

一种是数据分片,在目前用于扩展大型MySQL应用的方案中,数据分片是最通用且最成功的方法。它把数据分割成一小片,或者说一块,然后存储到不同的节点中。

比如分区键(partitioning key)。数据分片最大的挑战是查找和获取数据:如何查找数据取决于如何进行分片。分区键决定了每一行分配到哪一个分片中。

多个分区键的必要性。例如,需要将博客应用的数据按照用户ID和文章ID进行分片,因为这两者都是应用查询数据时使用比较普遍的方式。试想一下这种情形:频繁地读取某个用户的所有文章,以及某个文章的所有评论。如果按用户分片就无法找到某篇文章的所有评论,而按文章分片则无法找到某个用户的所有文章。如果希望这两个查询都落到同一个分片上,就需要从两个维度进行分片。

需要多个分区键并不意味着需要去设计两个完全冗余的数据存储。一般可以添加冗余来解决。

11.3 向内扩展

处理不断增长的数据和负载最简单的办法是对不再需要的数据进行归档和清理。这种操作可能会带来显著的成效,具体取决于工作负载和数据特性。这种做法并不用来代替其他策略,但可以作为争取时间的短期策略,也可以作为处理大数据量的长期计划之一。