7.4.6 案例分享六:生产环境下的MySQL数据库主从同步

MySQL的主从Replication同步(又叫主从复制)是一个很成熟的架构(如图7-9所示),笔者负责的许多电商平台的线上环境采用的都是这种方案,它的优点为:

·在业务繁忙的阶段,可以在从服务器上执行查询工作(即我们常说的读写分离),降低主服务器的压力。

广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元

·在从服务器上进行备份,避免备份期间影响主服务器服务。

·当主服务器出现问题时,可以迅速切换到从服务器,这样就不会影响线上环境了。

·数据分布。由于MySQL复制并不需要很大的带宽,所以可以在不同的数据中心实现跨机房数据的复制(阿里巴巴这种级别的公司业务另当别论)。比如笔者目前的数据中心的adserver业务数据库,就是一主四从。

主从复制是MySQL数据库提供的一种高可用、高性能的解决方案,其实现并不复杂,它不是完全实时的,而是异步实时的,如果网络延迟比较严重,则要考虑将其延迟时间作为Nagios报警的选项参数,其具体工作步骤为:

1)主服务器把数据更新记录到二进制日志中。

2)从服务器把主服务器的二进制日志复制到自己的中继日志中,这个由从服务器的I/O线程负责。

3)从服务器执行中继日志,把其更新应用到自己的数据库上,这个由从服务器的SQL线程负责。

阅读 ‧ 电子书库

图7-9 MySQL数据库主从同步的工作流程

鉴于生产环境下对MySQL有更严谨的要求,推荐采用源码编译的方法安装MySQL数据库。以下内容笔者曾在个人博客中阐述过,并且收到了许多热心网友的中肯意见,笔者据此进行了5次修改。如果大家在工作中有相应的需求,可以参考下。

编译安装MySQL数据库之前建议在服务器上安装基础库文件,命令如下:

yum -y install gcc gcc-c++ autoconf libjpeg libjpeg-devel libpng libpng-devel freetype freetype-devel libxml2 libxml2-devel zlib zlib-devel glibc glibc-devel glib2 glib2-devel bzip2 bzip2-devel ncurses ncurses-devel curl curl-devel e2fsprogs e2fsprogs-devel krb5 krb5-devel libidn libidn-devel openssl openssl-devel cmake

由于服务器采用的是最小化安装,因此建议也安装一下开发工具和开发库,以防止源码编译安装MySQL时报错,另外,MySQL从5.5版本开始,通过./configure进行编译配置的方式已经被取消,取而代之的是cmake工具,因此,我们首先要在系统中源码编译安装cmake工具。

MySQL数据库涉及的文件及对应的目录如下。

MySQL的安装位置:/usr/local/mysql

MySQL的配置文件:/etc/my.cnf

MySQL数据库的位置:/data/mysql/

下面介绍一下工作环境。

主数据库IP:192.168.1.205

从数据库IP:192.168.1.204

系统:CentOS 6.4 x86-64

内核版本:2.6.32-358.el6.x86_64

MySQL版本:5.5.40

服务器硬件环境:DELL PowerEdge R410或PowerEdge R710(6块300G SAS300G做成RAID 10)

首先,在MySQL官方网站(http://downloads.mysql.com/archives/community/ )上下载MySQL 5.5.40的源码包,并分别在两台机器上进行安装。

生成运行MySQL服务的用户及用户组的命令如下:

groupadd mysql

useradd -g mysql mysql

mkdir -p /data/mysql

chown -R mysql:mysql /data/mysql

源码编译安装MySQL 5.5.40的命令如下:

cd /usr/local/src/

tar xvf mysql-5.5.40.tar.gz

cd mysql-5.5.40

cmake -DCMAKE_INSTALL_PREFIX=/usr/local/mysql -DMYSQL_DATADIR=/data/mysql -DDEFAULT_CHARSET=utf8 -DDEFAULT_COLLATION=utf8_unicode_ci -DWITH_READLINE=1 -DWITH_SSL=system -DWITH_EMBEDDED_SERVER=1 -DENABLED_LOCAL_INFILE=1 -DDEFAULT_COLLATION=utf8_general_ci -DWITH_MYISAM_STORAGE_ENGINE=1 -DWITH_INNOBASE_STORAGE_ENGINE=1 -DWITH_DEBUG=0

make && make install && cd ../

然后,对MySQL进行权限配置,将MySQL的数据安装路径设为/data/mysql,并配置MySQL为服务启动状态,命令如下:

cd /usr/local/src/mysql-5.5.40

cp ./support-files/my-huge.cnf /etc/my.cnf

cp ./support-files/mysql.server /etc/init.d/mysqld

chmod +x /etc/init.d/mysqld

chown -R mysql:mysql /usr/local/mysql

这里暂时只用系统自带的“my-huge.cnf”作为MySQL数据库的配置文件,后期将根据MySQL的status运行状态来进行调优整理。

第三步是修改这两台服务器的MySQL服务器下的/etc/my.cnf的[mysqld]项,在其下面添加MySQL运行时的数据存放路径,命令如下所示:

datadir=/data/mysql

第四步是在这两台服务器上分别运行如下命令初始化数据库,在/data/mysql下生成MySQL的初始化文件和初始库等。

/usr/local/mysql/scripts/mysql_install_db --user=mysql --basedir=/usr/local/mysql --datadir=/data/mysql

此时就可以顺利地以服务的形式来启动MySQL服务了,命令如下:

service mysqld start

现在将其配置成为自启动状态,命令如下:

chkconfig mysqld on

配置完成后可以用如下命令来检查:

chkconfig --list mysqld

结果显示如下,表示上面的设定是成功的:

mysqld 0:off 1:off 2:on 3:on 4:on 5:on 6:off

最后,将MySQL的执行路径添加进PATH环境变量,在/etc/profile的最后一行添加如下内容:

export PATH=$PATH:/usr/local/mysql/bin

执行如下命令使更改立即生效:

source /etc/profile

下面详细介绍一下主从复制同步的过程。

1.设置主库

1)修改主库my.cnf,主要是设置各不一样的server-id,以及要同步的数据库名字。可以用vim编辑/etc/my.cnf文件,在[mysqld]段下面增加如下内容:

server-id = 1

log-bin= binlog

binlog_format=mixed

从库my.cnf文件跟主库不一样,具体改动如下:

server-id=2

log-bin= binlog

binlog_format=mixed

replicate_wild_do_table=adserver.%

replicate_wild_ignore_table=mysql.%

adserver为要同步的数据库的名字,mysql为不需要同步的数据库,这里为了避免发生跨库同步失败的问题,建议在从库里面这样配置。将从库所在服务器的server-id修改为非1的数字即可,不然会在同步的过程中发现错误,主库所在的机器的是必须要开启二进制日志的,其日志格式为mixed,而从库服务器则不必非要开启二进制日志。

2)分别重启主从库服务器让修改的配置生效,命令如下:

service mysqld restart

3)登录主库:

mysql -u root -p

此处不必输入密码即可进行,这是由于MySQL服务器是刚配置的,所以MySQL数据库的root密码暂时没有配置,为了安全起见,大家可以稍后再进行设置。

4)赋予从库权限账号,允许用户在主库上读取日志,命令如下:

mysql> grant replication slave on *.* to 'admin'@'192.168.11.27' identified by 'admin@101';

replication slave是一个基本的必须的权限,它直接授予从机服务器以该账户连接主机执行replication操作的权限,为了安全起见,建议只分配admin账户replication slave权限。

此操作完成以后,建议立即在从机上验证一下,如果成功显示MySQL登录界面则表示设置成功,命令如下:

mysql -u admin -p -h 192.168.1.204

Enter password:

输入admin密码以后应该有如下成功登录的显示结果。

Welcome to the MySQL monitor. Commands end with ; or \g.

Your MySQL connection id is 9

Server version: 5.5.40-log Source distribution

Copyright (c) 2000, 2014, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its

affiliates. Other names may be trademarks of their respective

owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql>

这里有个知识点需要说明一下,MySQL数据库的权限系统在实现上比较简单,相关权限信息主要存储在几个被称为grant tables的系统表中,即:mysql.user、msyql.db、mysql.host、mysql.table_priv和mysql.columm_priv。由于权限信息的数据量比较小,访问又非常频繁,所以MySQL在启动的时候,就会将所有的权限信息都加载到内存中,并保存在几个特定的结构里。这就使得每次在手动修改了相关权限表之后,都必须要执行flush privileges,通知MySQL重新加载MySQL的权限信息。当然,如果通过grant、revoke或drop user命令来修改相关权限,则不必再手动执行flush privileges命令了。

5)检查创建是否成功,命令显示结果如下(我们只需关注user名为admin的那行即可):

mysql> select user,host from mysql.user;

+-------+---------------+

| user | host |

+-------+---------------+

| root | 127.0.0.1 |

| admin | 192.168.1.205 |

| root | ::1 |

| | fabric |

| root | fabric |

| | localhost |

| root | localhost |

+-------+---------------+

7 rows in set (0.01 sec)

6)锁主库表,命令如下(建议不要退出此终端,以免锁表失败):

mysql> flush tables with read lock;

7)显示主库信息。

这里要记录一下File和Position,因为在设置从库时将会用到它们,命令如下:

mysql> show master status;

+------------------+----------+--------------+------------------+

| File | Position | Binlog_Do_DB | Binlog_Ignore_DB |

+------------------+----------+--------------+------------------+

| mysql-bin.000004 | 106 | mydata | |

+------------------+----------+--------------+------------------+

1 row in set (0.00 sec)

8)Slave端机器获取Master端MySQL数据库的“快照”有两种方法。第一种是锁表后直接用tar将原Master打包给Slave机器,这种情况适用于网站初始化,而且数据库比较单一的工作场景。比如说笔者的数据库已经在线运行了一段时间,由于MySQL中有默认设置,因此没有按表空间分离数据,所有的表数据都被放到ibdata1文件中了,其中包括数据字典(也就是InnoDB表的元数据)、变更缓冲区、双写缓冲区、撤销日志。ibdata1文件的大小会持续增长,就算删除了表的数据,ibdata1文件的体积也不会减小。所以如果是采用tar方案的话,首先需要对ibdata1文件进行“瘦身”操作。

如果机器上还存在着不同的数据库,那么就不适合用tar打包的方法了,比如笔者的订单系统的数据库上面的生产数据库数据差不多有9.8GB了,321张表,但有的数据库不需要同步,所以可以用别的方法来进行(比如mysqldump,它可以单独对某个特定的数据库进行逻辑备份,下面会重点讲解这种方法)。

如果只需单独备份主机上的adserver数据库,可以用如下的方式:

mysqldump --master-data -u root -p adserver > adserver.sql

另外,稍微解释一下--master-data参数的作用,mysqldump程序的开发者给程序设计这项参数是为了帮助获取对应的Log Position,在添加了这个参数选项以后,mysqldump会在dump文件中产生一条CHANGE MASTER TO命令,命令中记录了dump时刻所对应的Log Position的详细信息。

2.设置从库

1)在主库上将adserver.sql传输给从机,推荐用rsync命令,如果是生产环境下的数据库,那么rsync尤其适合在服务器之间传输上百GB的生产数据库数据,命令如下:

rsync -vzrtopg adserver.sql root@192.168.1.205:/root/

2)登录从库,建立adserver数据库,命令如下:

mysql>create database adserver;

然后,退出mysql命令行,导入adserver.sql数据,命令如下:

mysql -u root -p adserver < /root/adserver.sql

3)解锁主库表,命令如下所示:

mysql> unlock tables;

4)在从库上设置同步。

设置连接MASTER“MASTER_LOG_FILE”为主库的File,“MASTER_LOG_POS”为主库的Position,命令如下所示:

mysql> slave stop;

mysql> change master to master_host='192.168.1.204',master_user='admin', master_password='admin@101',

master_log_file='mysql-bin.000004', master_log_pos=7323251;

mysql> slave start;

5)查看从库的status状态,命令如下所示:

mysql>show slave status\G;

结果如下所示:

*************************** 1. row ***************************

Slave_IO_State: Waiting for master to send event

Master_Host: 192.168.1.204

Master_User: admin

Master_Port: 3306

Connect_Retry: 60

Master_Log_File: mysql-bin.000004

Read_Master_Log_Pos: 7323251

Relay_Log_File: localhost-relay-bin.000002

Relay_Log_Pos: 253

Relay_Master_Log_File: mysql-bin.000004

Slave_IO_Running: Yes

Slave_SQL_Running: Yes

Replicate_Do_DB:

Replicate_Ignore_DB:

Replicate_Do_Table:

Replicate_Ignore_Table:

Replicate_Wild_Do_Table: adserver.%

Replicate_Wild_Ignore_Table: mysql.%

Last_Errno: 0

Last_Error:

Skip_Counter: 0

Exec_Master_Log_Pos: 7323251

Relay_Log_Space: 413

Until_Condition: None

Until_Log_File:

Until_Log_Pos: 0

Master_SSL_Allowed: No

Master_SSL_CA_File:

Master_SSL_CA_Path:

Master_SSL_Cert:

Master_SSL_Cipher:

Master_SSL_Key:

Seconds_Behind_Master: 0

Master_SSL_Verify_Server_Cert: No

Last_IO_Errno: 0

Last_IO_Error:

Last_SQL_Errno: 0

Last_SQL_Error:

Replicate_Ignore_Server_Ids:

Master_Server_Id: 1

1 row in set (0.00 sec)

ERROR:

No query specified

注意上面显示结果中两部分的显示内容,“Slave_IO_Running:Yes”表示网络正常,“Slave_SQL_Running:Yes”表示结构正常,根据MySQL主从同步的原理,这两个部分必须都为YES(正常)才表示同步是成功的,此外,还要注意Seconds_Behind_Master这个选项,它表示主从同步延迟时间,在一些对数据即时性要求很高的生产场景下,这个选项也应该引起我们足够的重视。

6)进行一些测试工作。

在主库的adserver数据上建立名为yuhongchun的表,命令如下:

mysql> CREATE TABLE `yuhongchun` (

`id` INT(5 ) UNSIGNED NOT NULL AUTO_INCREMENT ,

`username` VARCHAR(20) NOT NULL ,

`password` CHAR(32) NOT NULL ,

`time` DATETIME NOT NULL ,

`number` FLOAT(10) NOT NULL ,

`content` TEXT NOT NULL ,

PRIMARY KEY (`id`)

) ENGINE = MYISAM ;

在从机中马上就可以看到,在mydata数据库下产生了名为yuhongchun的表,只不过表的记录目前为空,另外,观察下从机的中继日志,可以用如下命令:

mysqlbinlog localhost-relay-bin.000002

结果显示如下所示:

/*!50530 SET @@SESSION.PSEUDO_SLAVE_MODE=1*/;

/*!40019 SET @@session.max_insert_delayed_threads=0*/;

/*!50003 SET @OLD_COMPLETION_TYPE=@@COMPLETION_TYPE,COMPLETION_TYPE=0*/;

DELIMITER /*!*/;

# at 4

#150707 4:16:16 server id 2 end_log_pos 107 Start: binlog v 4, server v 5.5.40-log created 150707 4:16:16

BINLOG '

0IqbVQ8CAAAAZwAAAGsAAAAAAAQANS41LjQwLWxvZwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

AAAAAAAAAAAAAAAAAAAAAAAAEzgNAAgAEgAEBAQEEgAAVAAEGggAAAAICAgCAA==

'/*!*/;

# at 107

#691231 19:00:00 server id 1 end_log_pos 0 Rotate to mysql-bin.000004 pos: 7323251

# at 150

#150707 3:11:08 server id 1 end_log_pos 0 Start: binlog v 4, server v 5.5.40-log created 150707 3:11:08

BINLOG '

jHubVQ8BAAAAZwAAAAAAAAAAAAQANS41LjQwLWxvZwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

AAAAAAAAAAAAAAAAAAAAAAAAEzgNAAgAEgAEBAQEEgAAVAAEGggAAAAICAgCAA==

'/*!*/;

# at 253

#150707 4:20:17 server id 1 end_log_pos 7323578 Query thread_id=12 exec_time=0 error_code=0

use `adserver`/*!*/;

SET TIMESTAMP=1436257217/*!*/;

SET @@session.pseudo_thread_id=12/*!*/;

SET @@session.foreign_key_checks=1, @@session.sql_auto_is_null=0, @@session.unique_checks=1, @@session.autocommit=1/*!*/;

SET @@session.sql_mode=0/*!*/;

SET @@session.auto_increment_increment=1, @@session.auto_increment_offset=1/*!*/;

/*!\C utf8 *//*!*/;

SET @@session.character_set_client=33,@@session.collation_connection=33,@@session.collation_server=33/*!*/;

SET @@session.lc_time_names=0/*!*/;

SET @@session.collation_database=DEFAULT/*!*/;

CREATE TABLE `yuhongchun` (

`id` INT(5 ) UNSIGNED NOT NULL AUTO_INCREMENT ,

`username` VARCHAR(20) NOT NULL ,

`password` CHAR(32) NOT NULL ,

`time` DATETIME NOT NULL ,

`number` FLOAT(10) NOT NULL ,

`content` TEXT NOT NULL ,

PRIMARY KEY (`id`)

) ENGINE = MYISAM

/*!*/;

DELIMITER ;

# End of log file

ROLLBACK /* added by mysqlbinlog */;

/*!50003 SET COMPLETION_TYPE=@OLD_COMPLETION_TYPE*/;

/*!50530 SET @@SESSION.PSEUDO_SLAVE_MODE=0*/;

这表明主从同步复制是成功的。

3.MySQL主从同步常见的问题

熟悉MySQL主从复制架构的朋友应该很熟悉常见的错误,其实主要就是网络、MySQL访问权限、iptables和SELinux等的问题,我们平时注意检查这些问题,处理起来应该不是很困难,由于我们一般将MySQL主从服务器放在局域网(内网)环境内,在局域网环境内我们要记得关闭iptables和SELinux,注意Slave_IO_Running和Slave_SQL_Running的状态必须确保为YES才行,另外也要注意从机的Seconds_Behind_Master值。

这里跟大家分享下笔者在工作中主从同步时遇到的问题。

(1)表结构不同导致的主从同步失败的问题,从机报错信息如下所示:

120307 14:44:50 [ERROR] Slave: Error 'Cannot add or update a child row: a foreign key constraint fails (`offer99/fulfillment`, CONSTRAINT `fulfillment_ibfk_1` FOREIGN KEY (`offer_id`) REFERENCES `offer` (`offer_id`))' on query. Default database: 'offer99'. Query: 'INSERT INTO fulfillment(account_id,product_id,f_record_ts,fulfilled_ts,transaction_id,statuscode,gross_rev_in,gross_rev_out,merch_rev,net_rev,vc_points,vc_exchange_rate,offer_id,tab_name,advertising) VALUES('9496896','613','2012-03-07 09:33:46','2012-03-09:30:02','10139126','1','20.00','12.00','12.00','8','120000','10000.00','663','-1','1')', Error_code: 1452

笔者起初以为是外键约束的问题,在这上面浪费了不少时间;在这里跟大家提个醒,这个时候千万不要盲目地删除外键,这会导致在以后的MySQL主从同步维护工作中后患无穷,是个治标不治本的方法。这个时候更应该静下心来处理这个问题,我仔细地检查了两边数据库的fulfillment和offer表,发现两边的offer表数据不一致,特别是从数据库,差了3条数据,那么我们如何从几万条数据中找到究竟是差了哪3条数据呢?

这里可以用select into outfile的方法导出两边offer表的数据,将其保存为CSV格式的文件,然后用Linux下的diff命令进行比较,这样很快就能找出差了的数据的offer_id值,然后以SQL格式的形式导出数据并导进从机(load命令也是可以的),然后在从机上重新执行slave start命令进行同步,这样就可以排除故障了。

SELECT * FROM offer WHERE offer_id IN (658,663,694)

(2)表结构不同导致的从机更新失败

根据MySQL官方文档的说明,如果遇到错误的SQL执行语句的时候,故障的表象是从机不会去同步主库,所以要手工地让这个语句不被执行,跳过N个事件步骤后直接处理下一个事件,而这个跳过去的事件对数据的完整性是没有什么影响的。一般设置“SET GLOBAL sql_slave_skip_counter=1”就可以跳过去了,如果跳不过去,就要具体判断得跳多少步才能正确了。

这里再提供一个具体案例以供大家参考,我们本来就在从机上安装了bugfree数据库,结果笔者不小心在主数据库上也安装了bugfree数据库,这时候从机上不能再创建bugfree数据库了,直接导致Replication同步失败。这个时候可以用“set global sql_slave_skip_counter=N”忽略这个操作(N值取决于在主机上建立bugfree库和表的值),从而让数据库主从同步正常。

(3)主机硬件故障,如何切换主从服务器(一主多从)

某天早上笔者一来就发现公司的网站打不开了,初步断定是主数据库出了问题,发现SSH都登录不上去了,这时候紧急联系机房人员让他们帮忙重启,发现居然连重启都重启不了,他们断定是MySQL服务器硬件出了问题。这时候需要紧急将从机提升为主服务器,我们应该如何操作呢?可以按照如下的步骤进行操作:

1)用stop slave IO_THREAD命令在从机上停掉IO_Thread进程,确保从机上再没有同步的SQL语句,即出现“Has read all relay log”语句字样。

2)在从数据库上执行stop slave命令停止从机服务,然后执行reset master命令将其设置成主数据库。

3)在起始的从机上将原有的主机IP地址更换为此机器(现在已变为Master主数据库)的IP地址。

4)删除新的主数据库服务器的master.info和relay-log.info文件,防止它下次重启时还是按照从机来启动。

MySQL主从Replication复制非常快,在保证网络的前提下,小数据的改变几乎感觉不到延迟(但还是属于异步同步),通常在Master端改动以后,Slave端也会立即改动,这种模式非常适合那种对延时性要求很低的工作环境,比如线上的BBS论坛;如果是电子商务网站,由于对数据的即时性要求很高,建议不要用读写分离的方案,即:所有的读写操作均在主机上实现,从机只是作为主机的备份。其他非电子商务性质的网站,可以考虑MySQL一主多从,读写分离的方案。