可扩展性设计之 MySQL Replication(上)

326 阅读12分钟

前言

MySQL ReplicationMySQL 非常有特色的一个功能,他能够将一个 MySQL ServerInstance 中的数据完整的复制到另外一个 MySQL ServerInstance 中。虽然复制过程并不是实时而是异步进行的,但是由于其高效的性能设计,延时非常之少。MySQLReplication 功能在实际应用场景中被非常广泛的用于保证系统数据的安全性和系统可扩展设计中。

这篇文章将专门针对如何利用 MySQLReplication 功能来提高系统的扩展性进行详细的介绍。

一、Replication 对可扩展性设计的意义

在互联网应用系统中,扩展最为方便的可能要数最基本的 Web 应用服务了。因为 Web 应用服务大部分情况下都是无状态的,也很少需要保存太多的数据,当然 Session 这类信息比较例外。所以,对于基本的 Web 应用服务器很容易通过简单的添加服务器并复制应用程序来做到 Scale Out

而数据库由于其特殊的性质,就不是那么容易做到方便的 Scale Out。当然,各个数据库厂商也一直在努力希望能够做到自己的数据库软件能够像常规的应用服务器一样做到方便的 Scale Out,也确实做出了一些功能,能够基本实现像 Web 应用服务器一样的 Scalability,如很多数据库所支持的逻辑复制功能。

MySQL 数据库也为此做出了非常大的努力,MySQL Replication 功能主要就是基于这一目的所产生的。通过 MySQLReplication 功能,我们可以非常方便的将一个数据库中的数据复制到很多台 MySQL 主机上面,组成一个 MySQL 集群,然后通过这个 MySQL 集群来对外提供服务。这样,每台 MySQL 主机所需要承担的负载就会大大降低,整个 MySQL 集群的处理能力也很容易得到提升。

为什么通过 MySQL 的 Replication 可以做到 Scale Out 呢?

主要是因为通过 MySQLReplication,可以将一台 MySQL 中的数据完整的同时复制到多台主机上面的 MySQL 数据库中,并且正常情况下这种复制的延时并不是很长。当我们各台服务器上面都有同样的数据之后,应用访问就不再只能到一台数据库主机上面读取数据了,而是访问整个 MySQL 集群中的任何一台主机上面的数据库都可以得到相同的数据。此外还有一个非常重要的因素就是 MySQL 的复制非常容易实施,也非常容易维护。这一点对于实施一个简单的分布式数据库集群是非常重要的,毕竟一个系统实施之后的工作主要就是维护了,一个维护复杂的系统肯定不是一个受欢迎的系统。

二、Replication 机制的实现原理

要想用好一个系统,理解其实现原理是非常重要的事情,只有理解了其实现原理,我们才能够扬长避短,合理的利用,才能够搭建出最适合我们自己应用环境的系统,才能够在系统实施之后更好的维护他。

下面我们分析一下 MySQL Replication 的实现原理。

①Replication 线程

MysqlReplication 是一个异步的复制过程,从一个 Mysql instace(我们称之为 Master)复制到另一个 Mysql instance(我们称之 Slave)。在 MasterSlave 之间的实现整个复制过程主要由三个线程来完成,其中两个线程(Sql 线程和 IO 线程)在 Slave 端 ,另外一个线程(IO 线程)在 Master 端。

要实现 MySQLReplication ,首先必须打开 Master 端的 Binary Logmysql-bin.xxxxxx)功能,否则无法实现。因为整个复制过程实际上就是 SlaveMaster 端获取该日志然后再在自己身上完全顺序的执行日志中所记录的各种操作。打开 MySQLBinary Log 可以通过在启动 MySQL Server 的过程中使用“—log-bin” 参数选项,或者在 my.cnf 配置文件中的 mysqld 参数组([mysqld]标识后的参数部分)增加 “log-bin” 参数项。

MySQL 复制的基本过程如下:

  1. Slave 上面的 IO 线程连接上 Master,并请求从指定日志文件的指定位置(或者从最开始的日志)之后的日志内容;
  2. Master 接收到来自 SlaveIO 线程的请求后,通过负责复制的 IO 线程根据请求信息读取指定日志指定位置之后的日志信息,返回给 Slave 端的 IO 线程。返回信息中除了日志所包含的信息之外,还包括本次返回的信息在 Master 端的 Binary Log 文件的名称以及在 Binary Log 中的位置;
  3. SlaveIO 线程接收到信息后,将接收到的日志内容依次写入到 Slave 端的 Relay Log 文件(mysql-relay-bin.xxxxxx)的最末端,并将读取到的 Master 端的 bin-log 的文件名和位置记录到 master-info 文件中,以便在下一次读取的时候能够清楚的高速 Master“我需要从某个 bin-log 的哪个位置开始往后的日志内容,请发给我”
  4. SlaveSQL 线程检测到 Relay Log 中新增加了内容后,会马上解析该 Log 文件中的内容成为在 Master 端真实执行时候的那些可执行的 Query 语句,并在自身执行这些 Query。这样,实际上就是在 Master 端和 Slave 端执行了同样的 Query,所以两端的数据是完全一样的。

实际上,在老版本中,MySQL 的复制实现在 Slave 端并不是由 SQL 线程和 IO 线程这两个线程共同协作而完成的,而是由单独的一个线程来完成所有的工作。但是 MySQL 的工程师们很快发现,这样做存在很大的风险和性能问题,主要如下:

首先,如果通过一个单一的线程来独立实现这个工作的话,就使复制 Master 端的, Binary Log 日志,以及解析这些日志,然后再在自身执行的这个过程成为一个串行的过程,性能自然会受到较大的限制,这种架构下的 Replication 的延迟自然就比较长了。

其次,Slave 端的这个复制线程从 Master 端获取 Binary Log 过来之后,需要接着解析这些内容,还原成 Master 端所执行的原始 Query,然后在自身执行。在这个过程中, Master 端很可能又已经产生了大量的变化并生成了大量的 Binary Log 信息。如果在这个阶段 Master 端的存储系统出现了无法修复的故障,那么在这个阶段所产生的所有变更都将永远的丢失,无法再找回来。这种潜在风险在 Slave 端压力比较大的时候尤其突出,因为如果 Slave 压力比较大,解析日志以及应用这些日志所花费的时间自然就会更长一些,可能丢失的数据也就会更多。

所以,在后期的改造中,新版本的 MySQL 为了尽量减小这个风险,并提高复制的性能, 将 Slave 端的复制改为两个线程来完成,也就是前面所提到的 SQL 线程和 IO 线程。最早提出这个改进方案的是 Yahoo! 的一位工程师“Jeremy Zawodny”。通过这样的改造,这样既在很大程度上解决了性能问题,缩短了异步的延时时间,同时也减少了潜在的数据丢失量 。

当然,即使是换成了现在这样两个线程来协作处理之后,同样也还是存在 Slave 数据延时以及数据丢失的可能性的,毕竟这个复制是异步的。只要数据的更改不是在一个事务中 ,这些问题都是存在的。

如果要完全避免这些问题,就只能用 MySQLCluster 来解决了。不过 MySQLCluster 知道笔者写这部分内容的时候,仍然还是一个内存数据库的解决方案,也就是需要将所有数据包括索引全部都 Load 到内存中,这样就对内存的要求就非常大的大,对于一般的大众化应用来说可实施性并不是太大。当然,在之前与 MySQLCTO David 交流的时候得知,MySQL 现在正在不断改进其 Cluster 的实现,其中非常大的一个改动就是允许数据不用全部 Load 到内存中,而仅仅只是索引全部 Load 到内存中,我想信在完成该项改造之后的 MySQL Cluster 将会更加受人欢迎,可实施性也会更大。

②复制实现级别

MySQL 的复制可以是基于一条语句(Statement Level),也可以是基于一条记录(Row level),可以在 MySQL 的配置参数中设定这个复制级别,不同复制级别的设置会影响到 Master 端的 Binary Log 记录成不同的形式。

  1. Row LevelBinary Log 中会记录成每一行数据被修改的形式,然后在 Slave 端再对相同的数据进行修改。

优点:在 Row Level 模式下,Binary Log 中可以不记录执行的 sql 语句的上下文相关的信息,仅仅只需要记录那一条记录被修改了,修改成什么样了。所以 Row Level 的日志内容会非常清楚的记录下每一行数据修改的细节,非常容易理解。而且不会出现某些特定情况下的存储过程,或 function,以及 trigger 的调用和触发无法被正确复制的问题。

缺点Row Level 下,所有的执行的语句当记录到 Binary Log 中的时候,都将以每行记录的修改来记录,这样可能会产生大量的日志内容,比如有这样一条 update 语句:UPDATE group_message SET group_id = 1 where group_id = 2,执行之后,日志中记录的不是这条 update 语句所对应的事件(MySQL 以事件的形式来记录 Binary Log 日志),而是这条语句所更新的每一条记录的变化情况,这样就记录成很多条记录被更新的很多个事件。自然 , Binary Log 日志的量就会很大。尤其是当执行 ALTER TABLE 之类的语句的时候,产生的日志量是惊人的。因为 MySQL 对于 ALTER TABLE 之类的 DDL 变更语句的处理方式是重建整个表的所有数据,也就是说表中的每一条记录都需要变动,那么该表的每一条记录都会被记录到日志中。

  1. Statement Level:每一条会修改数据的 Query 都会记录到 MasterBinaryLog 中。Slave 在复制的时候 SQL 线程会解析成和原来 Master 端执行过的相同的 Query 来再次执行。

优点Statement Level 下的优点首先就是解决了 Row Level 下的缺点,不需要记录每一行数据的变化,减少 Binary Log 日志量,节约了 IO 成本,提高了性能。因为他只需要记录在 Master 上所执行的语句的细节,以及执行语句时候的上下文的信息。

缺点:由于他是记录的执行语句,所以,为了让这些语句在 slave 端也能正确执行,那么他还必须记录每条语句在执行的时候的一些相关信息,也就是上下文信息,以保证所有语句在 slave 端杯执行的时候能够得到和在 master 端执行时候相同的结果。另外就是,由于 Mysql 现在发展比较快,很多的新功能不断的加入,使 mysql 得复制遇到了不小的挑战,自然复制的时候涉及到越复杂的内容,bug 也就越容易出现。在 statement level 下,目前已经发现的就有不少情况会造成 mysql 的复制出现问题,主要是修改数据的时候使用了某些特定的函数或者功能的时候会出现,比如:sleep()函数在有些版本中就不能真确复制,在存储过程中使用了 last_insert_id()函数,可能会使 slavemaster 上得到不一致的 id 等等。由于 row level 是基于每一行来记录的变化,所以不会出现类似的问题。

从官方文档中看到,之前的 MySQL 一直都只有基于 Statement 的复制模式,直到 5.1.5 版本的 MySQL 才开始支持 Row Level 的复制。从 5.0 开始,MySQL 的复制已经解决了大量老版本中出现的无法正确复制的问题。但是由于存储过程的出现,给 MySQL 的复制又带来了更大的新挑战。另外,看到官方文档说,从 5.1.8 版本开始,MySQL 提供了除 Statement LevelRow Level 之外的第三种复制模式:Mixed Level,实际上就是前两种模式的结合。在 Mixed 模式下,MySQL 会根据执行的每一条具体的 Query 语句来区分对待记录的日志形式,也就是在 StatementRow 之间选择一种。新版本中的 Statment level 还是和以前一样,仅仅记录执行的语句。而新版本的 Mysql 中对 Row Level 模式也被做了优化,并不是所有的修改都会以 Row Level 来记录,像遇到表结构变更的时候就会以 statement 模式来记录,如果 Query 语句确实就是 UPDATE 或者 DELETE 等修改数据的语句,那么还是会记录所有行的变更。

这篇文章还没有结束!剩下的内容(Replication 常用架构、Replication 搭建实现和总结)将在下一篇文章中更新!

请关注我的个人专栏 “Mysql性能优化” ,持续关注有关mysql性能优化的文章!