前言
MySQL Replication
是 MySQL
非常有特色的一个功能,他能够将一个 MySQL Server
的 Instance
中的数据完整的复制到另外一个 MySQL Server
的 Instance
中。虽然复制过程并不是实时而是异步进行的,但是由于其高效的性能设计,延时非常之少。MySQL
的 Replication
功能在实际应用场景中被非常广泛的用于保证系统数据的安全性和系统可扩展设计中。
这篇文章将专门针对如何利用 MySQL
的 Replication
功能来提高系统的扩展性进行详细的介绍。
一、Replication 对可扩展性设计的意义
在互联网应用系统中,扩展最为方便的可能要数最基本的 Web 应用服务了。因为 Web 应用服务大部分情况下都是无状态的,也很少需要保存太多的数据,当然 Session
这类信息比较例外。所以,对于基本的 Web 应用服务器很容易通过简单的添加服务器并复制应用程序来做到 Scale Out
。
而数据库由于其特殊的性质,就不是那么容易做到方便的 Scale Out
。当然,各个数据库厂商也一直在努力希望能够做到自己的数据库软件能够像常规的应用服务器一样做到方便的 Scale Out
,也确实做出了一些功能,能够基本实现像 Web 应用服务器一样的 Scalability
,如很多数据库所支持的逻辑复制功能。
MySQL
数据库也为此做出了非常大的努力,MySQL Replication
功能主要就是基于这一目的所产生的。通过 MySQL
的 Replication
功能,我们可以非常方便的将一个数据库中的数据复制到很多台 MySQL
主机上面,组成一个 MySQL
集群,然后通过这个 MySQL
集群来对外提供服务。这样,每台 MySQL
主机所需要承担的负载就会大大降低,整个 MySQL
集群的处理能力也很容易得到提升。
为什么通过 MySQL 的 Replication 可以做到 Scale Out 呢?
主要是因为通过 MySQL
的 Replication
,可以将一台 MySQL
中的数据完整的同时复制到多台主机上面的 MySQL
数据库中,并且正常情况下这种复制的延时并不是很长。当我们各台服务器上面都有同样的数据之后,应用访问就不再只能到一台数据库主机上面读取数据了,而是访问整个 MySQL
集群中的任何一台主机上面的数据库都可以得到相同的数据。此外还有一个非常重要的因素就是 MySQL
的复制非常容易实施,也非常容易维护。这一点对于实施一个简单的分布式数据库集群是非常重要的,毕竟一个系统实施之后的工作主要就是维护了,一个维护复杂的系统肯定不是一个受欢迎的系统。
二、Replication 机制的实现原理
要想用好一个系统,理解其实现原理是非常重要的事情,只有理解了其实现原理,我们才能够扬长避短,合理的利用,才能够搭建出最适合我们自己应用环境的系统,才能够在系统实施之后更好的维护他。
下面我们分析一下 MySQL Replication
的实现原理。
①Replication 线程
Mysql
的 Replication
是一个异步的复制过程,从一个 Mysql instace
(我们称之为 Master
)复制到另一个 Mysql instance
(我们称之 Slave
)。在 Master
与 Slave
之间的实现整个复制过程主要由三个线程来完成,其中两个线程(Sql
线程和 IO
线程)在 Slave
端 ,另外一个线程(IO
线程)在 Master
端。
要实现 MySQL
的 Replication
,首先必须打开 Master
端的 Binary Log
(mysql-bin.xxxxxx
)功能,否则无法实现。因为整个复制过程实际上就是 Slave
从 Master
端获取该日志然后再在自己身上完全顺序的执行日志中所记录的各种操作。打开 MySQL
的 Binary Log
可以通过在启动 MySQL Server
的过程中使用“—log-bin
” 参数选项,或者在 my.cnf
配置文件中的 mysqld
参数组([mysqld]
标识后的参数部分)增加 “log-bin
” 参数项。
MySQL
复制的基本过程如下:
Slave
上面的IO
线程连接上Master
,并请求从指定日志文件的指定位置(或者从最开始的日志)之后的日志内容;Master
接收到来自Slave
的IO
线程的请求后,通过负责复制的IO
线程根据请求信息读取指定日志指定位置之后的日志信息,返回给Slave
端的IO
线程。返回信息中除了日志所包含的信息之外,还包括本次返回的信息在Master
端的Binary Log
文件的名称以及在Binary Log
中的位置;Slave
的IO
线程接收到信息后,将接收到的日志内容依次写入到Slave
端的Relay Log
文件(mysql-relay-bin.xxxxxx
)的最末端,并将读取到的Master
端的bin-log
的文件名和位置记录到master-info
文件中,以便在下一次读取的时候能够清楚的高速Master
“我需要从某个bin-log
的哪个位置开始往后的日志内容,请发给我”Slave
的SQL
线程检测到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
数据延时以及数据丢失的可能性的,毕竟这个复制是异步的。只要数据的更改不是在一个事务中 ,这些问题都是存在的。
如果要完全避免这些问题,就只能用 MySQL
的 Cluster
来解决了。不过 MySQL
的 Cluster
知道笔者写这部分内容的时候,仍然还是一个内存数据库的解决方案,也就是需要将所有数据包括索引全部都 Load
到内存中,这样就对内存的要求就非常大的大,对于一般的大众化应用来说可实施性并不是太大。当然,在之前与 MySQL
的 CTO David
交流的时候得知,MySQL
现在正在不断改进其 Cluster
的实现,其中非常大的一个改动就是允许数据不用全部 Load
到内存中,而仅仅只是索引全部 Load
到内存中,我想信在完成该项改造之后的 MySQL Cluster
将会更加受人欢迎,可实施性也会更大。
②复制实现级别
MySQL
的复制可以是基于一条语句(Statement Level
),也可以是基于一条记录(Row level
),可以在 MySQL
的配置参数中设定这个复制级别,不同复制级别的设置会影响到 Master
端的 Binary Log
记录成不同的形式。
- Row Level:
Binary 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
变更语句的处理方式是重建整个表的所有数据,也就是说表中的每一条记录都需要变动,那么该表的每一条记录都会被记录到日志中。
- Statement Level:每一条会修改数据的
Query
都会记录到Master
的BinaryLog
中。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()
函数,可能会使 slave
和 master
上得到不一致的 id
等等。由于 row level
是基于每一行来记录的变化,所以不会出现类似的问题。
从官方文档中看到,之前的 MySQL
一直都只有基于 Statement
的复制模式,直到 5.1.5 版本的 MySQL
才开始支持 Row Level
的复制。从 5.0 开始,MySQL
的复制已经解决了大量老版本中出现的无法正确复制的问题。但是由于存储过程的出现,给 MySQL
的复制又带来了更大的新挑战。另外,看到官方文档说,从 5.1.8 版本开始,MySQL
提供了除 Statement Level
和 Row Level
之外的第三种复制模式:Mixed Level
,实际上就是前两种模式的结合。在 Mixed
模式下,MySQL
会根据执行的每一条具体的 Query
语句来区分对待记录的日志形式,也就是在 Statement
和 Row
之间选择一种。新版本中的 Statment level
还是和以前一样,仅仅记录执行的语句。而新版本的 Mysql
中对 Row Level
模式也被做了优化,并不是所有的修改都会以 Row Level
来记录,像遇到表结构变更的时候就会以 statement
模式来记录,如果 Query
语句确实就是 UPDATE
或者 DELETE
等修改数据的语句,那么还是会记录所有行的变更。
这篇文章还没有结束!剩下的内容(Replication 常用架构、Replication 搭建实现和总结)将在下一篇文章中更新!
请关注我的个人专栏 “Mysql性能优化” ,持续关注有关mysql性能优化的文章!