主备备的两个备机转为双Master出现诡异的slave lag问题

Preview:

DESCRIPTION

Mysql replication方面的

Citation preview

主备备的两个备机转为双 Master 出现诡异的 slave lag 问题

有 三 台 MySQL 服 务 器 , a,b 和 c , 复 制 关 系 为 a -> b -> c 。 a,b,c 的

server_id 分别为 1,2,3

因为需要切换为 a b <-> c,也就是说,a 单独出来,b 和 c 作为双 master 结

构时。

这种切换会经常出现在需要搭建备机把数据备份出来,然后把 a 独立出来的

case 中。

昨天,我就做了这样的切换,结果发现出现莫名奇妙的 slave lag。

Seconds_Behind_Master 一下子为 0,一下子变成几千秒。

使 用 mysqlbinlog 查 看 ,binlog 日 志 里 面 也 有 很 多 时 间 在 几 小 时 以 前 的

event 数据。

为了验证复制是否正常,我特别测试了一下,在 b 建一个表,并插入时间数据,

到 c 上一看,表已经复制过来了,时间数据也是正确。

询问了一下同事,他说应该是 MySQL 的 bug,在这种切换的情况下很容易触

发这个 bug,可以采用 stop slave;change master; start slave;的方法来修

复。但是实际的数据其实完全没有影响,复制还是正常的。

于是我按照这个办法:

stop slave io_thread;

stop slave;

show slave status\G

(这里先停 io_thread 是为了 SQL thread 和 IO thread 都执行到了同一个位置,

change master 的时候没有风险)

stop slave;change master to … ; start slave;

(change master 到 show slave status 的 Master_Log_File: 和

Exec_Master_Log_Pos:位置,也就是说,其实根本没有改变复制的位置)

结果 slave lag 依然故我。这个问题就比较郁闷了。时间已经过了午夜,脑袋

也转不动了,想过不管它了,反正复制没有问题。但是问题没有解决总觉得什

么东西卡在喉咙一样。各种资料,各种变量都参考了一遍,最后,基本不太意

识的输入:

show master logs;

show binlog events in ‘mysql-bin.000680′ from 34385301;

想看看最新产生的 event,结果就发现不对的地方了。

这个最新产生的 event 有很多,并且 server_id 是 1,1 是 a 的 server_id 啊,

应用访问的是 b 啊,怎么会在 b 上面产生 a 的 server_id 列,MySQL 哪里出

问题了?

仔细一想,明白了,事情是这样的:

a -> b -> c,a 的 event1(server_id 为 1)复制到 b,也会复制到 c,这个是

正常的。

然后搭建 c -> b 的复制关系时,b 需要断开 a 的连接,切换主库到 c,在

change master 的位置在 event1 出现之前,那么 event1 肯定会被重新复制

到 b 去,event1 的 server_id 是 1,那么 b 判断,这个 event1 不是我提交的,

需要在本地执行,并且把它记录到了自己的 binlog 中;

由于 b 和 c 是双 master 结构,event1 又复制到了 c,c 同样判断它不是我提

交的,那么我需要在本地执行,并且记录到本地 binlog 中。

这样 event1 就在 b 和 c 之间循环往复,时间保持不变,MySQL 的 slave lag

也就一下子是 0,一下子是几千秒了。

这里,还需要说明一点,在环型复制里面,event 之所以能够在环内只循环一

次,而不是重复做,是因为提交的那个节点会发现这个 event 的 server_id 是

自己的 server_id,也就是说是自己提交的。那么,它就不会把这个 event 再

应用一次,自然也不会记录到 binlog。这个循环就结束了。除非你闲着没事做,

设置了 replicate-same-server-id 参数。

那么解决问题怎么办列,很简单,把没有应用访问的 c 的 server_id设置成 a

的 server_id:

set global server_id=1;

看看时间差不多了,server_id 为 1 的 event 都被干掉以后:

set global server_id=3;

然后再设置回来。

还好,MySQL 5.0 和 5.1 的 server_id 都是动态的。

may your success.

research report for MySQL multi-master tool

把老的多主 master向同一个 slave 复制的文档找出来了。这个是研究性质的,

不会牵涉到太多技术细节,所以不担心会有泄漏以前公司技术的嫌疑。之前公

司的这个软件已经完成了,包括事件解析,应用以及冲突处理。我自己想做一

个开源的,multi_master 的版本,但是架构基本上会非常不同。希望有时间

能够真正的做出来阿,这个功能相信对我们还是非常有用的。

Multi-Master

测试报告

目 录

目 录 2

1 报告信息 3

2 概述 4

2.1 目的 4

2.2 相关信息 4

2.3 MySQL replication 数据复制格式 5

3 测试过程 9

3.1 测试进度安排 9

3.2 测试过程描述 9

4 测试结论 12

4.1 测试最终结论 12

5 附录 13

5.1 附录 1 MySQL 5.1.20 Beta包含的事件类型 13

5.2 附录 2 MySQL 5.1.20 Beta 各事件的附加事件头长度 14

5.3 附录 3 MySQL 5.1.20 Beta 中各列在内部存储时可能的各种数据类型 16

5.4 附录 4 MySQL 5.1.20 Beta 中各 ROW_EVENT 的 m_flags包含的标志

位 17

概述

目的

测 试是否可以模拟 MySQL replication 的功能构建一个小工具。该工具模拟

replication端的两个线程:I/O线程和 SQL线程。I/O线程用于从master端

(主服务器)注册自己和申请获得 master 的 binlog信息,把获得的 binlog

信息进行解析并保存在本地 relay-log 中。SQL线程负责读 取解析 relay-log

并在本地 MySQL 服务器上执行这些语句。从而达到从master端复制数据并

在 slave端(从服务器)执行,以及时同步 slave 的目的。用这个工具,我们

可以避免MySQL 只能从单个 master 中同步数据的限制,实现从多个 master

中复制数据,同步 slave 的功 能。

相关信息

项目来源

在我们GSB 的数据库复制同步中,杭州 的 GSB 数据库需要从不同的 master

端获得数据,并同步到自己的数据库中。而 MySQL 本身不提供multi-master

的功能,只能从单个 master 中复制和同步数据,并且在将来一段时间也不准

备增加这个功能。所以我们打算先对 MySQL 的源代码进行理解和分析,并尝

试着模拟MySQL replication 的功能,研究multi-master工具的可行性。

测试环境

在 这 次 测 试 中 我 们 主 要 用 到 了 两 台 机 器 , 192.168.1.112 和

192.168.1.113,它们都是虚拟机,安转的操作系统为 Linux 2.6.16。其中

112 机 器 中安装的 MySQL server 版本 为 : 5.0.27-standard-log , 作 为

master。113 机器安装的 MySQL server版本为:5.0.24-max-log,作为

slave。由于采用了增加 server_id 列标识提交语句的 site 的策略,我们将测

试环境 中的两个 MySQL版本升级为 5.1.20 Beta。

其他相关信息

MySQL 的 数据复制功能主要涉及到三个线程(master端一个,slave端两

个)。当用户在 slave端提交 start slave;slave端将首先创建 I/O线程,I/O

线程会 连 接 到 master 并请求 master 将其 binlog 中 的语句发送回来 。

Master 接收 到请求后将创建一个线程(Binlog Dump)用于发送 binlog信

息给 slave,用户可以通过 Show processlist 在 master端看到此线程。I/O

线程读取主服务器 Binlog Dump线程发送的内容并将该数据拷贝到从服务器

数据目录中的本地文件中,即中继日志(relay-log)。第 3 个线程是 SQL线

程,是 slave端创建的用于读取中继日志并执行日志中语句的线程。

如果 有 多 个从服 务 器 , 主 服 务 器将为每个当前 连 接 的从服 务 器创建 一 个

(Binlog Dump)线程;每个从服务器都有自己的 I/O 和 SQL线程。

MySQL 复制基于服务器在二进制日志中对所有数据库的更改(更新、删除等等)。

因此,要进行复制,必须在主服务器上启用二进制日志。

从MySQL 5.1 开始,MySQL replication 可以支持两种复制类型,第一种是

原有的 statement based replication,也就是将在 master端提交的查询语

句及相关环境一起记录到 binlog 中,slave模拟该环境并提交语句执行。第二

种是 row based replication,即将master端提交的查询语句改变的所有行

数据记录到 binlog 中,slave端获得这些数据直接提交给MySQL server,修

改对应数据文件。这两种复制类型可以单独运行或者混合起来。MySQL默认

的复制类型就是混合类型的复制,它根据查询类型,表的类型等相 关因素确定

该表的复制类型。

在每一个数据库的表中,我们增加了一列 server_id 用于模拟线程变量 thd-

>server_id。但是由于 server_id存在于表中并且将随表数据一同复制,在复

制中不能随意改变,所以这个方法存在一定的限 制,后面我们将详细描述。由

于我们使用 server_id 列判断表中某一行(row)数据操作的最先发起 site,

所 以 我们必须保 证 复 制 的 列 中包括这 一 列 , 这 样 我们必须限制 MySQL

master 和 slave 端 replication 的类型 为 row based ,即 row-based

replication。注意:row-based replication 是 MySQL 5.1版本才被引入的。

下面我们所涉及的复制除了特殊指明外都是指 row based replication。

在 MySQL 5.1.20 Beta版本中,在 master端如果一条语句改变了一个表的

多条数据,master将首先在 binlog 中记录下一个 Tablemap 事件用于将表的

相 关信息以及表的 id信息对应记录下来。而针对每一行需要改变的数据,

master端单独记录一个 writerow(updaterow,deleterow)事件,而且

每一行数据都是二进制的 MySQL 内部格式的数据。其中 writerow 对应的查

询语句是 : insert 和 replace ; updaterow 对 应 的 是 update 语句;

deleterow 对应的是 delete语句。而 writerow 事件中除了环境数据以外还包

含了对应的行数据为 record[0]。record[0]中包含有将要插入 MySQL 表中的

一行数据。 Update语句包含两个行数据,record[0]表示将要更新存储到表

中的行数据,而 record[1]中包含了 update将要替换的已存在于表中 的行数

据。Delete 的 record[0]中包含的是将要删除的行数据。由于 binlog 中的数

据都是二进制的 MySQL格式的数据,而我们也没有找到 直接将这些数据插入

MySQL server 的接口,所以在对这些数据操作之前,我们首先必须将这些数

据转换回可读数据。然后就可以直接向MySQL server 提交对应的查询语句。

每种不同的类型在 MySQL 中保存的格式都不一样,MySQL 5.1.20 Beta 的数

据类型见附录 3。

MySQL replication 数据复制格式

这 里我们基于 MySQL 5.1.20 Beta描述MySQL两个 slave端的 thread 发

送和接收数据的格式。某些字段所占的字节数跟MySQL 的版本有关,这里我

们所描述的为 binlog版本为 4,MySQL server版本为 5.1.20 Beta 下的数

据格式。

MySQL I/O thread 数据格式

向主服务器注册自己

向 主服务器注册自己并不是一个必须的操作,如果没有注册同样可以向主服务

器请求数 据 。如果 需 要向主 服 务 器注册, 那 么 可 以调用 mysql.h 中 的

simple_command(mysql, command, arg, length, skip_check)函数,在

arg 参 数 中 依序填入 下述的 各 个字段, 并指定 其 中 的 参 数 command 为

COM_REGISTER_SLAVE 以注册自 己。

名称 字节数 含义

server_id 4本 MySQL instance 的 server_id值

strlen(report_h

ost)

1 or 2标识接下来的 report_host 的长度,如果

长度<251占 1 个字节,否则占两个字节

report_host Strlen(report_h

ost)向主服务器注册的 MySQL instance标识

strlen(report_u

ser)

1 or 2标识接下来的 report_user 的长度,如果

长度<251占 1 个字节,否则占 2 个字节

report_user Strlen(report_u

ser)向主服务器注册的用户名

strlen(report_p

assword)

1 or 2标识接下来的 report_password 的长度,

如果长度<251占 1 个字节,否则占 2 个

字节

report_passwor

d

Strlen(report_p

assword)向主服务器注册的密码

report_port 2向主服务器注册的端口

rpl_recovery_r

ank

4复制的恢复等级

master_id 4填入 0,主服务器将自行填入 master_id

register_master

图 1、主服务器注册示意图

向主服务器请求数据

从 服务器向主服务器发送了请求数据的命令以后主服务器将根据要求将对应

binlog文件的指定位置开始的事件记录发送给从服务器。向主服务器请求数据,

可 以 调 用 mysql.h 中 的 simple_command(mysql, command, arg,

length, skip_check)函数,在 arg 参数中依序填入下述的各个字段,并指定

其中的参数 command 为 COM_BINLOG_DUMP。

名称 字节数 含义

master_log_pos 4请求主 服 务 器 发送的 事件记 录 在

binlog文件中的偏移量

binlog_flags 2暂时填 0,做扩展用

server_id 4本 MySQL instance 的 server_id

logname Strlen(logname)请求主服务器发送的 binlog文件的

文件名

如 果 没 有 指 定 MySQL 使 用 methods , 那 么 我 们 应 该 调 用 函 数

sql_common.h 头 文 件 中 的 cli_advanced_command ( ) 代 替

simple_command()。

向 主服务器请求了数据以后,从服务器就可以通过 cli_safe_read(mysql);获

得主服务器发送过来的数据,每次获得一个事件记录的数据。 cli_safe_read

的返回值标示了从主服务器发送过来的数据的数据字节数。而发送过来的数据

保存在 mysql->net->read_pos 数组中。I/O thread模块可以利用 MySQL

的 io_cache将对应事件记录存储到 relay-log文件中。

MySQL binlog 文件初始化

由于 MySQL binlog 的特殊性,以及为了 mysqlbinlog工具能够识别我们

relay-log文件,在新建一个 relay-log文件时必须写入一定的初始化数据。这

些初始化数据依序包括如下字段:

名称 字节数 含义

BINLOG_MAGIC

( 即 ” \xfe\x62\

BIN_LOG_HEADER_SI

ZE(4)Binlog文件的标识值

x69\x6e”)

MySQL SQL thread 数据格式

只 要循环的调用 cli_safe_read函数,从服务器可以不断得到从主服务器发送

过来的事件记录。接下来我们介绍一下相关的一些事件记录格式。在提交了

COM_BINLOG_DUMP命令后,主服务器首先给从服务器发送的两个事件依序

分 别 为 ROTATE_EVENT 和 FORMAT_DESCRIPTION_EVENT 事 件 。

ROTATE_EVENT 事件用来标示接下来主服务器将从哪一个 binlog文件的哪个

位置开始 发送事件记录。而 FORMAT_DESCRIPTION_EVENT 事件用来记录

本 MySQL instance 的 server_id值,binlog版本号,MySQL server 的版

本,本 relay-log创建的时间以及各个不同事件的事件头所占的字节数等信息。

我 们 关 心 的 其 他 的 事 件 记 录 的 格 式 包 括

WRITE_ROWS_EVEN

T,UPDATE_ROWS_EVENT,DELETE_ROWS_EVENT等。

事件头字段描述

各个事件都包括一个事件头,事件头的字段格式如下:

名称 字 节

含义

When 4事件的创建时间。

Type 1事件的类型(见附录 1)。

server_id 4事件发生时所在 MySQL 的 server_id值。

data_writ

ten

4该事件一共占用的字节数,包括事件头的字节数。

log_pos 4下一事件在 binlog文件中将要开始的位置,即本事件的结束

位置

Flags 2事件的其他标志位。

ROTATE_EVENT 事件字段描述

由于各个事件的事件头基本一致,这里我们就不重复介绍事件头的各字段了,

后面的各个事件我们也都将忽略对事件头字段的描述。

ROTATE_EVENT 事件的附加事件头字段主要包括:

名称 字 节 含义

pos 8主服务器将要发送的事件记录在 binlog文件中的偏移量。一

般为从服务器提交的 COM_BINLOG_DUMP请求中的偏移量

值。

ROTATE_EVENT 事件的其他信息字段主要包括:

名称 字节数 含义

new_log_

ident

strlen(new_lo

g_ident)主服务器将要发送的事件记录的 binlog文件名。一

般为从服务器提交的 COM_BINLOG_DUMP请求中

的 binlog文件名。

FORMAT_DESCRIPTION_EVENT 事件字段描述

FORMAT_DESCRIPTION_EVENT 事件的附加事件头的字段如下:

名称 字节数 含义

binlog_ver

sion

2Binlog文件的版本号,这里一般为最新的

版本号 4

server_ver

sion

ST_SERVER_VER_LE

N(50)MySQL 的版本号。例如:” 5.1.20-

beta-log”

Created 4事件创建时间,这里一般和事件头中的

when 一致

event_hea

der_len

1一般事件的事件头长度,一般设置为:

LOG_EVENT_HEADER_LEN(19)

post_head

er_len

ENUM_END_EVENT-

1(26)不同事件类型的附加事件头的长度,见附

录 2。

TABLE_MAP_EVENT 事件字段描述

TABLE_MAP_EVENT 事件的附加事件头的字段如下:

名称 字节数 含义

m_table_id6(5.1.4 前的版本中为 4) 表的 id标识符

m_flags 2表的各种标志位,见附录 4

TABLE_MAP_EVENT 事件的其他信息字段主要包括:

名称 字节数 含义

m_dbl

en

1数据库名的长度

m_db

nam

m_dblen+1数据库名,以’\0’结尾

m_tbll

en

1表名的长度

m_tbl

nam

m_tbllen+1表名,以’\0’结尾

m_col

cnt

net_field_le

ngth()表的字段个数,所占字节数根据第一个字节的大小由

net_field_length函数确定

m_col

type

m_colcnt表的各个字段的字段类型,参见附录 3。

WRITE_ROWS_EVENT 事件字段描述

WRITE_ROWS_EVENT 事件的附加事件头的字段如下:

名称 字节数 含义

m_table_id6(5.1.4 前的版本中为 4) 表的 id标识符

m_flags 2表的各种标志位,见附录 4

WRITE_ROWS_EVENT 事件的其他信息字段主要包括:

名称 字节数 含义

m_width net_field_lengt

h()表的各列的位图长度,所占字节数根据第一个字节

的大小由 net_field_length函数确定

m_cols.b

itmap

(m_width +

7) / 8表的各列的位图,每一位表示m_rows_buf 是否

包含表中一列的值,如果没有置位表示该列的值没

有包含在 m_rows_buf 中

m_rows_

buf剩余字节数(len-

已占字节数)

将要插入到表中的一行数据值。

UPDATE_ROWS_EVENT 事件字段描述

UPDATE_ROWS_EVENT 事件的附加事件头的字段如下:

名称 字节数 含义

m_table_id6(5.1.4 前的版本中为 4) 表的 id标识符

m_flags 2表的各种标志位,见附录 4

UPDATE_ROWS_EVENT 事件的其他信息字段主要包括:

名称 字节数 含义

m_width net_field_length()表的各列的位图长度,所占字节数根据第

一个字节的大小由 net_field_length函数

确定

m_cols.bit

map

(m_width + 7) / 8表中被匹配行数据的各列的位图,每一位

表示 m_rows_buf 是 否包含表 中 该 列 的

值。

m_cols_ai.

bitmap

(m_width + 7) / 8表中将要更新的行数据的各列的位图,每

一位表示m_rows_buf 是否包含表中一列

的值。

m_rows_bu

f剩余字节 数 (len-已占

字节数)

表中被匹配的那一行数据的值以及将要更

新的一行数据值。

DELETE_ROWS_EVENT 事件字段描述

DELETE_ROWS_EVENT 事件的附加事件头的字段如下:

名称 字节数 含义

m_table_id6(5.1.4 前的版本中为 4) 表的 id标识符

m_flags 2表的各种标志位

DELETE _ROWS_EVENT 事件的其他信息字段主要包括:

名称 字节数 含义

m_width net_field_lengt

h()表的各列的位图长度,所占字节数根据第一个字节

的大小由 net_field_length函数确定

m_cols.b

itmap

(m_width +

7) / 8表的各列的位图,每一位表示m_rows_buf 是否

包含表中一列的值。

m_rows_

buf剩余字节数(len-

已占字节数)

表中将要删除的一行数据值。

XID_EVENT 事件字段描述

XID_EVENT 一般出现在一个事务操作(transaction)之后或者其他语句提交之

后。它的主要作用是提交事务操作和把事件刷新至 binlog文件中。

XID_EVENT 事件的信息字段包括:

名称 字节数 含义

xid sizeof(xid) 8commit标识符

测试过程

测试进度安排

2007-07-26 —— 2007-08-03

理解和分析MySQL slave端 I/O线程以及 SQL线程的实现细节。

2007-08-04 —— 2007-08-08

模拟实现 MySQL replication 的 I/O线程,实现向master请求 binlog,并记

录读到的信息到一个本地日志文件 relay-log 中的功能。使用 mysqlbinlog 应

该能查看该日志。

2007-08-09 —— 2007-08-15

模拟实现 MySQL replication 的 SQL线程,实现读取并解析 relay-log文件,

设置 slave端的执行环境以提交查询。

2007-09-03 —— 2007-09-07

熟悉并了解 MySQL 5.1.20 Beta 中 row based replication配置以及实现。

2007-09-10 —— 2007-09-21

模拟实 现 MySQL 5.1.20 Beta 中 insert , update , delete 等语句的 在

slave段的解析并生成对应的语句。

2007-09-24 —— 2007-09-30

阅读和了解 replication 冲突解决方案,分析 update(delete)更新 0 row

冲突和 update(delete)复制执行空语句之间的区别。

测试过程描述

通过对 MySQL源代码的阅读,我们了解并熟悉了 MySQL replication 的基本

原理和实现细节。

对 MySQL I/O线程的模拟相对比较简单,这个线程的向master注册自己及请

求发送 binlog信息都有相应的接口提供出来,并且对于从master端接收的信

息也只做了比较简单的分析就直接将该信息存入 relay-log 日志文件中。所以

我们模拟 I/O线程比较顺利。

对于 MySQL 的 SQL线程的模拟实现中,读取和解析 relay-log 日志文件虽然

比较繁琐,但是基本实现的困难不大。主要的困难出现在如何设置 slave端的

执行环境。

这 里设置 slave端的执行环境包括从master端复制过来的一些环境信息,比

如 : server_id , charset , timezone ,

auto_increment_increment,auto_increment_offset等等。其中最重要

的是 server_id,它表示 master 发送过来的 binlog信息中,将要执行的这一

条语句最开始是在哪一个 MySQL server 中提交执行的。如果配置了环形的

replication链,并且复制过来的 server_id与本机的 server_id相等的话,那

么说明 这条语句最开始就是在本机中执行的,它对应的语句应该被忽略。

如果从master 服务器复制过来的 server_id与本机的 server_id 不同,那么

就应该在本机执行这条语句。这里我们特别要注意的是:在记录本地 binlog 日

志文件时,server_id 应该保证为复制 过来的 server_id,而不是我们普通提

交查询时记录的本地的 server_id。不然在配置双向复制和环形复制链时将造

成数据复制的死循环,从而造 成数据紊乱。

我们仔细察看了 SQL线程的源代码,发现它在读取 relay-log 记录时会把复制

过来的 server_id信息保存在对应 的 SQL线程变量 thd->server_id 中,从而

在写本机 binlog 日志文件时记录的 server_id将会是从master端复制过来的

server_id信息。如果我们要模拟 SQL线程,我们需要在提交查询前修改连接

会话的相应变量。但由于 server_id 在 MySQL 中是一个全局变 量,而不是一

个 会话期变 量 , 所 以 我们不 能 在 连 接 中 修 改 这 个 变 量(不 然 ,从修 改 了

server_id 后到恢复本机的 server_id值之间的这一段时间 里,在 binlog 日志

文件中记录的用户提交的语句对应的 server_id值将保持为修改后的值,而不

是本机 server_id值)。在写 binlog 日志时,写入的 server_id值依赖于连接

线程中的 server_id值,而 MySQL server 也没有提供任何修改连接线程中对

应的 server_id 变量值的接口。这样,我们无法模拟 SQL线程来执行复制过来

的语句。

为 了能够标识数据最新的插入和更新 site,我们在每一个数据库表上都增加了

一列 server_id。在对表执行 insert操作时,server_id将 自动赋值为该 site

的 server_id值。也就是说,在增加 server_id 列时设置它的 default值为该

MySQL 的 server_id值。 而为了保证在本机执行的 update操作对 server_id

有同样的影响,我们可以借助 MySQL 的 trigger功能,使得在本机上执行的

update操作修改行数据值中 server_id 的值为本机的 server_id值。另外,

我们还要注意的是用户在提交查询时不应该自己操作 server_id值,而应该通

过我们设置的 MySQL 的已有机制进行操作,以防出现数据复制的死循环。下

面我们分别描述 insert,update,delete操作的提交和复制过程。

1. Insert 的提交和复制:

由 于 在 MySQL 的 row based replication 中 insert 和 replace 语句在

binlog 中都被记录为 write row 事件,所以我们把 replace语句的提交和复制

合并到 insert 的提交和复制中。Insert 的提交和复制相对来说比其他的的操作

简单。我们只 要设置 server_id 列的 default 值为对应 MySQL server 的

server_id值,当用户提交 insert 查询时,server_id将自动赋值。而 MySQL

将把 insert 插 入 的每一 行 数 据 自 动 地 对 应 一 个 write row 事件并 记 录 在

binlog 中。其他的 slave 可以通过我们的工具将 binlog文件复制到本地,然

后解析write row 以生成对应的 replace语句。通过 server_id 列的值我们可

以很容易的知道最开始提交 insert语句的 MySQL 的 server_id 值,从而在该

write row 事件复制到该 MySQL instance 时停止复制,而避免数据复制的死

循环。

1. Update 的提交和复制:

Update 的提交和复制比较复杂。在一个 MySQL 上提交的 update语句将被

对应为一个 update row 事件记录在 binlog 文件中,slave端复制并解析

update row 事件生成对应的 update语句。这里我们举一个数据复制的例子:

如果某一条数据首先在 MySQL instance 1(M1)上插入,那么该数据的

server_id 列值为 1,该数据复制到 MySQL instance 2(M2)数据将保持不

变。但是,如果此时在 M2 上提交的 update 语句更新了该行数据,那么

server_id值仍然保持为 1。如果该 update row 事件重新复制回M1,那么我

们的 复 制工具发 现 该 行 数 据 的 server_id 值与本 MySQL instance 的

server_id值一样,认为该 update语句最开始就是在本机提交执行的,它将

忽略该 update语句。针对这个问题,我们有两 种解决方案:

1.

1.

1. 由于在 MySQL server 中,update 要更新的那一行数据匹配不

成功那么对于 MySQL 数据库的将不会有任何修改,并且这一条

update语句也将不会记录到 binlog文件中。我们可以利用这一

点允许 multi-master 工具在解析 update row 事件时忽略对

server_id 的检查,允许从其他 MySQL instance 复制的在本

MySQL instance 提交的 update语句继续执行,实际上该语句

将由于匹配不到对应的数据而执行空操作。这里执行的空操作实

际上和我们后面要讨论的一种冲 突类型(slave端提交的查询不

能更新数据冲突)是一样的。

这里我们仍然用上面的例子进行说明:

首 先在 M1 上提交的 insert语句被复制到 M2 并插入对应的数据,而当用户更

新这一行数据后,update row 事件复制回M1 时,由于 multi-master工具

不再检查 server_id值,那么同样的 update语句将在 M1 执行。M2 重新复制

得到 update row 事件并生成对应 update语句,但是由于该 update语句是

最先在 M2 提交成功的,那么正常情况下该 update语句不能匹配到要修改的

那一行 数据,从而执行一条空语句,不会出现数据复制的死循环。当然,如果

在复制回M2 数据前有其他的语句 insert或者 update 生成了若干行能够被匹

配的 数据的话,这种方案是行不通的。

1.

1.

1. 第二种解决方案相对复杂一些。它保证了 update语句更新行数

据的 server_id值为本 MySQL server 的 server_id值。我们利

用了 MySQL 提供的 trigger,在 update语句提交执行之前改变

行数据 NEW 的 server_id 值为本 MySQL server 的 server_id

值。(在 MySQL 的 trigger 中,OLD 行数据表示数据库中已有

的将要被 update匹配的数据,而 NEW 行数据 表示 update语

句将要更新 为 其值的 行 数 据)。 但 是 , 由 于 我们的 multi-

master工具将生成的语句直接提交到本 MySQL instance 中执

行,那么如果我们的 trigger 不能识别工具提交的语句和用户提

交 的语句, 而 把 NEW 行 数 据 的 server_id 值全部改 成 本

instance 的值,复制的死循环一定会出现。例如:在上面的例子

中,M1 的 server_id 为 1,在 M2 上 update 的数据复制到 M1

上提交执 行,如果 trigger 不能识别出它是从multi-master工

具生成的语句,而是把它的 NEW 行数据修改为 1,那么记录在

M1 的 binlog 中的数 据将不能标识最开始提交语句的 MySQL

instance 位置,从而不能中断数据复制的循环。至少有两种方法

可以区分普通的用户提交语句和 multi-master工具提交的语句。

1,专门指定 一个用户用于 multi-master工具提交查询语句,

以区别 于 其 他 用户提 交 的语句。 在 trigger 中 我们可 以 用

user()函数获得用户名来确定查 询语句提交的来源。2,通过

multi-master 修改相应语句而且使得它提交的所有语句与普通

用户提交的语句不同。下面我们详细阐述第二种策略。

我 们发现用户提交的所有 update语句都有一个共同的特点:OLD 行数据和

NEW 行数据的 server_id 值相等。如果我们在用工具生成 OLD 行数据和

NEW 行 数 据 的 server_id 值相等的 update 语句时 ,将 NEW 行 数 据 的

server_id值改变(例如改为 0)以区别于用户提交的语句。这样 trigger 就能

根据是否用户提交的语句而进行相应的操作了。一般来说,我们的 trigger 可

以写成如下的形式:

delimiter //

create trigger update_set_srvid_test_test001 before update on

test001 for each row

BEGIN

IF NEW.server_id=OLD.server_id THEN

SET NEW.server_id=1;

ELSEIF NEW.server_id=0 THEN

SET NEW.server_id=OLD.server_id;

END IF;

END;//

delimiter ;

1. Delete 的提交和复制:

Delete 的 提 交 和 复 制 和 update 的 基 本类似。 在 一 个 MySQL 上 提 交 的

delete语句将被对应为一个 delete row 事件记录在 binlog文件中,slave端

复制文件并解析 delete row 事件生成对应的 delete语句提交执行。为了解决

本 MySQL 上 insert 的数据在别的 MySQL instance 上 delete 的问题,我们

的第一个解决方案和 update 一样,也是忽略对 server_id值的检查。同样,

它存在着两个缺点:1. 它与冲突解决方案中的一种类型是一样的,我们不能区

别 这 样 的 一 个空操作 是 否 是 一 个冲突。 2. 如果 在 delete 语句复 制回本

instance 前,有其他的语句产生了该 delete语句能够匹配的行数据,那么这

一 行 数 据将被错误的删除掉。 同 时 , 因 为 delete 不包括 NEW 行 数 据 ,

update 的第二个解决方案对 delete语句不适用。

测试结论

测试最终结论

通过上面的测试和分析,我们发现可以通过增加 server_id 列和增加 trigger

等手段模拟 MySQL 的 replication功能,从而实现 MySQL 的多主复制工具

multi-master。

附录

附录 1 MySQL 5.1.20 Beta 包含的事件类型

下面列举了各种 MySQL 的事件类型(代码拷贝自 MySQL 5.1.20 的源代码):

enum Log_event_type

{

/*

Every time you update this enum (when you add a type), you have

to

fix Format_description_log_event::Format_description_log_event().

*/

UNKNOWN_EVENT= 0,

START_EVENT_V3= 1,

QUERY_EVENT= 2,

STOP_EVENT= 3,

ROTATE_EVENT= 4,

INTVAR_EVENT= 5,

LOAD_EVENT= 6,

SLAVE_EVENT= 7,

CREATE_FILE_EVENT= 8,

APPEND_BLOCK_EVENT= 9,

EXEC_LOAD_EVENT= 10,

DELETE_FILE_EVENT= 11,

/*

NEW_LOAD_EVENT is like LOAD_EVENT except that it has a longer

sql_ex, allowing multibyte TERMINATED BY etc; both types share the

same class (Load_log_event)

*/

NEW_LOAD_EVENT= 12,

RAND_EVENT= 13,

USER_VAR_EVENT= 14,

FORMAT_DESCRIPTION_EVENT= 15,

XID_EVENT= 16,

BEGIN_LOAD_QUERY_EVENT= 17,

EXECUTE_LOAD_QUERY_EVENT= 18,

TABLE_MAP_EVENT = 19,

/*

These event numbers were used for 5.1.0 to 5.1.15 and are

therefore obsolete.

*/

PRE_GA_WRITE_ROWS_EVENT = 20,

PRE_GA_UPDATE_ROWS_EVENT = 21,

PRE_GA_DELETE_ROWS_EVENT = 22,

/*

These event numbers are used from 5.1.16 and forward

*/

WRITE_ROWS_EVENT = 23,

UPDATE_ROWS_EVENT = 24,

DELETE_ROWS_EVENT = 25,

/*

Something out of the ordinary happened on the master

*/

INCIDENT_EVENT= 26,

/*

Add new events here – right above this comment!

Existing events (except ENUM_END_EVENT) should never change

their numbers

*/

ENUM_END_EVENT /* end marker */

};

附录 2 MySQL 5.1.20 Beta 各事件的附加事件头长度

下面列举了 MySQL 5.1.20 Beta 各事件的附加事件头长度(拷贝自 MySQL

源代码):

/* event-specific post-header sizes */

// where 3.23, 4.x and 5.0 agree

#define QUERY_HEADER_MINIMAL_LEN (4 + 4 + 1 + 2)

// where 5.0 differs: 2 for len of N-bytes vars.

#define QUERY_HEADER_LEN (QUERY_HEADER_MINIMAL_LEN + 2)

#define LOAD_HEADER_LEN (4 + 4 + 4 + 1 +1 + 4)

#define START_V3_HEADER_LEN (2 + ST_SERVER_VER_LEN + 4)

#define ROTATE_HEADER_LEN 8 // this is FROZEN (the Rotate post-

header is frozen)

#define CREATE_FILE_HEADER_LEN 4

#define APPEND_BLOCK_HEADER_LEN 4

#define EXEC_LOAD_HEADER_LEN 4

#define DELETE_FILE_HEADER_LEN 4

#define FORMAT_DESCRIPTION_HEADER_LEN

(START_V3_HEADER_LEN+1+LOG_EVENT_TYPES)

#define ROWS_HEADER_LEN 8

#define TABLE_MAP_HEADER_LEN 8

#define EXECUTE_LOAD_QUERY_EXTRA_HEADER_LEN (4 + 4 + 4 +

1)

#define EXECUTE_LOAD_QUERY_HEADER_LEN (QUERY_HEADER_LEN

+ EXECUTE_LOAD_QUERY_EXTRA_HEADER_LEN)

#define INCIDENT_HEADER_LEN 2

post_header_len[START_EVENT_V3-1]= START_V3_HEADER_LEN;

post_header_len[QUERY_EVENT-1]= QUERY_HEADER_LEN;

post_header_len[ROTATE_EVENT-1]= ROTATE_HEADER_LEN;

post_header_len[LOAD_EVENT-1]= LOAD_HEADER_LEN;

post_header_len[CREATE_FILE_EVENT-1]=

CREATE_FILE_HEADER_LEN;

post_header_len[APPEND_BLOCK_EVENT-1]=

APPEND_BLOCK_HEADER_LEN;

post_header_len[EXEC_LOAD_EVENT-1]= EXEC_LOAD_HEADER_LEN;

post_header_len[DELETE_FILE_EVENT-1]=

DELETE_FILE_HEADER_LEN;

post_header_len[NEW_LOAD_EVENT-1]=

post_header_len[LOAD_EVENT-1];

post_header_len[FORMAT_DESCRIPTION_EVENT-1]=

FORMAT_DESCRIPTION_HEADER_LEN;

post_header_len[TABLE_MAP_EVENT-1]= TABLE_MAP_HEADER_LEN;

post_header_len[WRITE_ROWS_EVENT-1]= ROWS_HEADER_LEN;

post_header_len[UPDATE_ROWS_EVENT-1]= ROWS_HEADER_LEN;

post_header_len[DELETE_ROWS_EVENT-1]= ROWS_HEADER_LEN;

/*

We here have the possibility to simulate a master of before we

changed

the table map id to be stored in 6 bytes: when it was stored in 4

bytes (=> post_header_len was 6). This is used to test backward

compatibility.

This code can be removed after a few months (today is Dec 21st

2005),

when we know that the 4-byte masters are not deployed anymore

(check

with Tomas Ulin first!), and the accompanying test (rpl_row_4_bytes)

too.

*/

DBUG_EXECUTE_IF(“old_row_based_repl_4_byte_map_id_master”,

post_header_len[TABLE_MAP_EVENT-1]=

post_header_len[WRITE_ROWS_EVENT-1]=

post_header_len[UPDATE_ROWS_EVENT-1]=

post_header_len[DELETE_ROWS_EVENT-1]= 6;);

post_header_len[BEGIN_LOAD_QUERY_EVENT-1]=

post_header_len[APPEND_BLOCK_EVENT-1];

post_header_len[EXECUTE_LOAD_QUERY_EVENT-1]=

EXECUTE_LOAD_QUERY_HEADER_LEN;

post_header_len[INCIDENT_EVENT-1]= INCIDENT_HEADER_LEN;

附录 3 MySQL 5.1.20 Beta 中各列在内部存储时可能的各种数据类型

enum enum_field_types

{

MYSQL_TYPE_DECIMAL = 0,

MYSQL_TYPE_TINY = 1,

MYSQL_TYPE_SHORT = 2,

MYSQL_TYPE_LONG = 3,

MYSQL_TYPE_FLOAT = 4,

MYSQL_TYPE_DOUBLE = 5,

MYSQL_TYPE_NULL = 6,

MYSQL_TYPE_TIMESTAMP = 7, // 4 from_unixtime(0x)

MYSQL_TYPE_LONGLONG = 8,

MYSQL_TYPE_INT24 = 9, //field_medium

MYSQL_TYPE_DATE = 10,

MYSQL_TYPE_TIME = 11,

MYSQL_TYPE_DATETIME = 12,

MYSQL_TYPE_YEAR = 13,

MYSQL_TYPE_NEWDATE = 14,

MYSQL_TYPE_VARCHAR = 15, //field_varstring

MYSQL_TYPE_BIT = 16,

MYSQL_TYPE_NEWDECIMAL = 246,

MYSQL_TYPE_ENUM = 247,

MYSQL_TYPE_SET = 248,

MYSQL_TYPE_TINY_BLOB = 249,

MYSQL_TYPE_MEDIUM_BLOB = 250,

MYSQL_TYPE_LONG_BLOB = 251,

MYSQL_TYPE_BLOB = 252,

MYSQL_TYPE_VAR_STRING = 253,

MYSQL_TYPE_STRING = 254,

MYSQL_TYPE_GEOMETRY = 255,

};

附录 4 MySQL 5.1.20 Beta 中各 ROW_EVENT 的 m_flags 包含的标志

在 MySQL 5.1.20 Beta 中,各 ROW_EVENT 都含有 m_flags标志位集合。

可能的标志如下:

名称 值 含义

STMT_END_F 1语句执行结束标志

NO_FOREIGN_KEY_CHECKS_F 2不进行外键约束检查的标志

RELAXED_UNIQUE_CHECKS_F 4不进行唯一键约束检查的标志

相关代码如下:

/*

These definitions allow you to combine the flags into an

appropriate flag set using the normal bitwise operators. The

implicit conversion from an enum-constant to an integer is

accepted by the compiler, which is then used to set the real set

of flags.

*/

enum enum_flag

{

/* Last event of a statement */

STMT_END_F = (1U << 0),

/* Value of the OPTION_NO_FOREIGN_KEY_CHECKS flag in thd-

>options */

NO_FOREIGN_KEY_CHECKS_F = (1U << 1),

/* Value of the OPTION_RELAXED_UNIQUE_CHECKS flag in thd-

>options */

RELAXED_UNIQUE_CHECKS_F = (1U << 2)

};

typedef uint16 flag_set;

/* Special constants representing sets of flags */

enum

{

RLE_NO_FLAGS = 0U

};

MySQL row 方式的复制 relay

上次老大问我 row 方式的 binlog 复制到备机可不可能记录为 statement。根

据我对复制的了解和对 row 方式的理解,我回答的是不可能。因为 MySQL 的

源码中,我记得 row 方式的处理是 table map,row_log_event 分开的,然

后 row_log_event 中 记 录 的 是 行 的 数 据 (包括 bitmap 对 应 对 应 的 列 在 该

row_log_event 有没有记录,整个 row_log_event 的长度,每个列的长度后

面跟上列的具体数据等),这些东西记录下载,MySQL 的开发者如果要把它还

原成 statement 方式,并且将相关的 auto_increment,var,rand等还原出来

难度还是非常大的,并且,row 方式实际上已经毁坏了 statement 的结构(比

如 :update tbl1 set c1=3 where id=3 记 录 为 row 的话, 正 常 情 况 下 ,

row_log_event 不会只记录了更新的这一列 c1,它会记录 id或者其他的列,

虽然 id或者其他的列值并没有更新。原因可能是为了正确的更新对应的行。),

如果想完全还原成和主机上提交的 statement 一模一样基本是不可能的。另外,

我也没有看到 MySQL源码中的相关代码有将 row 转换成 statement 的相关

痕迹,所以判断 row 方式在备机中 log_slave_updates 会也被记录为 row 方

式。

但是,口说无凭,实践致胜,我在主备机环境下测试了上面的这个情况。确实

如上所述,row 方式的 binlog,备机利用 log_slave_updates 记录到本机

binlog 也是 row 方式。即使你设置备机的 binlog_format 为 statement。下

面是我的测试描述和结果。

1、主机上设置 binlog_format 为 row.

root@localhost : alitest 10:01:09> set binlog_format=row;

Query OK, 0 rows affected (0.00 sec)

root@localhost : alitest 10:01:25> show variables like ‘bin%’;

+——————-+———+

| Variable_name | Value |

+——————-+———+

| binlog_cache_size | 2097152 |

| binlog_format | ROW |

+——————-+———+

2 rows in set (0.00 sec)

2、备机上设置 binlog_format 为 statement

root@localhost : (none) 10:06:44> set global

binlog_format=’statement’;

Query OK, 0 rows affected (0.00 sec)

root@localhost : (none) 10:07:04> set binlog_format=’statement’;

Query OK, 0 rows affected (0.00 sec)

root@localhost : (none) 10:07:11> show variables like ‘bin%’;

+——————-+———–+

| Variable_name | Value |

+——————-+———–+

| binlog_cache_size | 2097152 |

| binlog_format | STATEMENT |

+——————-+———–+

2 rows in set (0.00 sec)

3、主机上创建测试表:

root@localhost : (none) 10:00:02> use alitest;

Database changed

root@localhost : alitest 10:00:50> create table test9 (c1 int

unsigned primary key, c2 varchar(24));

Query OK, 0 rows affected (0.20 sec)

4、备机上查看目前的日志位置:

root@localhost : (none) 10:04:49> show master logs;

+——————+———–+

| Log_name | File_size |

+——————+———–+

| mysql-bin.000001 | 125 |

| mysql-bin.000085 | 362605228 |

+——————+———–+

85 rows in set (0.00 sec)

4、主机上生成测试的 row 方式日志:

root@localhost : alitest 10:01:27> insert into test9 (c1,c2) values

(44, “343434″);

Query OK, 1 row affected (0.00 sec)

root@localhost : alitest 10:02:53> insert into test9 (c1,c2) values (3,

“343434″);

Query OK, 1 row affected (0.00 sec)

5、备机查看自己生成 binlog:

root@localhost : (none) 10:07:53> show binlog events in ‘mysql-

bin.000085′ from 362605228;

+——————+———–+————+———–+————-+

——————————–+

| Log_name | Pos | Event_type | Server_id | End_log_pos | Info |

+——————+———–+————+———–+————-+

——————————–+

| mysql-bin.000085 | 362605228 | Query | 1 | 362605287 | BEGIN |

| mysql-bin.000085 | 362605287 | Table_map | 1 | 362605337 |

table_id: 27 (alitest.test9) |

| mysql-bin.000085 | 362605337 | Write_rows | 1 | 362605378 |

table_id: 27 flags: STMT_END_F |

| mysql-bin.000085 | 362605378 | Xid | 1 | 362605405 | COMMIT /*

xid=23537907 */ |

| mysql-bin.000085 | 362605405 | Query | 1 | 362605464 | BEGIN |

| mysql-bin.000085 | 362605464 | Table_map | 1 | 362605514 |

table_id: 27 (alitest.test9) |

| mysql-bin.000085 | 362605514 | Write_rows | 1 | 362605555 |

table_id: 27 flags: STMT_END_F |

| mysql-bin.000085 | 362605555 | Xid | 1 | 362605582 | COMMIT /*

xid=23537917 */ |

+——————+———–+————+———–+————-+

——————————–+

8 rows in set (0.00 sec)

由上可以看到,备机虽然设置自己的 binlog_format 为 statement,binlog

日 志 中 记 录从主 机 过 来 的 binlog 仍然 为 row 。写到 这 里 , 我突然 想起,

binlog_format 是否只是对在本机提交的 sql才有效,于是我测试了以下两种

情况:

1、主机为 statement,备机为 statement。 结果备机记录为 statement.

2、主机为 statement,备机为 row。结果备机记录为 statement.

也就是说,备机记录的从主机复制过来的 binlog 不随自己的 binlog_format

方式改变,而是忠实的依照主机记录的方式来记录。

下面是简单的测试结果:

测 试 1 主 机 为 statement , 备 机 为 statement 。 结 果 备 机 记 录 为

statement.

1、主机上设置 binlog_format 为 row 并插入一行

root@localhost : alitest 10:42:41> set binlog_format=statement;

Query OK, 0 rows affected (0.00 sec)

2、备机上设置为 statement

root@localhost : (none) 10:06:44> set global

binlog_format=’statement’;

Query OK, 0 rows affected (0.00 sec)

root@localhost : (none) 10:07:04> set binlog_format=’statement’;

Query OK, 0 rows affected (0.00 sec)

root@localhost : (none) 10:07:11> show variables like ‘bin%’;

+——————-+———–+

| Variable_name | Value |

+——————-+———–+

| binlog_cache_size | 2097152 |

| binlog_format | STATEMENT |

+——————-+———–+

2 rows in set (0.00 sec)

3、主机上插入一行

root@localhost : alitest 10:42:54> insert into test9 (c1,c2) values (4,

“343434″);

Query OK, 1 row affected (0.00 sec)

4、备机上查看日志:

root@localhost : (none) 10:35:48> show binlog events in ‘mysql-

bin.000085′ from 362605228;

+——————+———–+————+———–+————-+

—————————————————————+

| Log_name | Pos | Event_type | Server_id | End_log_pos | Info |

+——————+———–+————+———–+————-+

—————————————————————+

| mysql-bin.000085 | 362605228 | Query | 1 | 362605287 | BEGIN |

| mysql-bin.000085 | 362605287 | Table_map | 1 | 362605337 |

table_id: 27 (alitest.test9) |

| mysql-bin.000085 | 362605337 | Write_rows | 1 | 362605378 |

table_id: 27 flags: STMT_END_F |

| mysql-bin.000085 | 362605378 | Xid | 1 | 362605405 | COMMIT /*

xid=23537907 */ |

| mysql-bin.000085 | 362605405 | Query | 1 | 362605464 | BEGIN |

| mysql-bin.000085 | 362605464 | Table_map | 1 | 362605514 |

table_id: 27 (alitest.test9) |

| mysql-bin.000085 | 362605514 | Write_rows | 1 | 362605555 |

table_id: 27 flags: STMT_END_F |

| mysql-bin.000085 | 362605555 | Xid | 1 | 362605582 | COMMIT /*

xid=23537917 */ |

| mysql-bin.000085 | 362605582 | Query | 1 | 362605641 | BEGIN |

| mysql-bin.000085 | 362605641 | Query | 1 | 362605753 | use

`alitest`; insert into test9 (c1,c2) values (4, “343434″) |

| mysql-bin.000085 | 362605753 | Xid | 1 | 362605780 | COMMIT /*

xid=23537922 */ |

+——————+———–+————+———–+————-+

—————————————————————+

11 rows in set (0.00 sec)

测试 2 主机为 statement,备机为 row。结果备机记录为 statement.

1、主机上设置 binlog_format 为 row 并插入一行

root@localhost : alitest 10:42:41> set binlog_format=statement;

Query OK, 0 rows affected (0.00 sec)

2、备机上设置为 statement

root@localhost : (none) 10:46:23> set binlog_format=’row’;

Query OK, 0 rows affected (0.00 sec)

root@localhost : (none) 10:46:28> set global binlog_format=’row’;

Query OK, 0 rows affected (0.00 sec)

root@localhost : (none) 10:46:37> show variables like ‘bin%’;

+——————-+———+

| Variable_name | Value |

+——————-+———+

| binlog_cache_size | 2097152 |

| binlog_format | ROW |

+——————-+———+

2 rows in set (0.00 sec)

3、主机上插入一行

root@localhost : alitest 10:43:02> insert into test9 (c1,c2) values (5,

“343434″);

Query OK, 1 row affected (0.00 sec)

4、备机上查看日志:

root@localhost : (none) 10:46:43> show binlog events in ‘mysql-

bin.000085′ from 362605228;

+——————+———–+————+———–+————-+

—————————————————————+

| Log_name | Pos | Event_type | Server_id | End_log_pos | Info |

+——————+———–+————+———–+————-+

—————————————————————+

| mysql-bin.000085 | 362605228 | Query | 1 | 362605287 | BEGIN |

| mysql-bin.000085 | 362605287 | Table_map | 1 | 362605337 |

table_id: 27 (alitest.test9) |

| mysql-bin.000085 | 362605337 | Write_rows | 1 | 362605378 |

table_id: 27 flags: STMT_END_F |

| mysql-bin.000085 | 362605378 | Xid | 1 | 362605405 | COMMIT /*

xid=23537907 */ |

| mysql-bin.000085 | 362605405 | Query | 1 | 362605464 | BEGIN |

| mysql-bin.000085 | 362605464 | Table_map | 1 | 362605514 |

table_id: 27 (alitest.test9) |

| mysql-bin.000085 | 362605514 | Write_rows | 1 | 362605555 |

table_id: 27 flags: STMT_END_F |

| mysql-bin.000085 | 362605555 | Xid | 1 | 362605582 | COMMIT /*

xid=23537917 */ |

| mysql-bin.000085 | 362605582 | Query | 1 | 362605641 | BEGIN |

| mysql-bin.000085 | 362605641 | Query | 1 | 362605753 | use

`alitest`; insert into test9 (c1,c2) values (4, “343434″) |

| mysql-bin.000085 | 362605753 | Xid | 1 | 362605780 | COMMIT /*

xid=23537922 */ |

| mysql-bin.000085 | 362605780 | Query | 1 | 362605839 | BEGIN |

| mysql-bin.000085 | 362605839 | Query | 1 | 362605951 | use

`alitest`; insert into test9 (c1,c2) values (5, “343434″) |

| mysql-bin.000085 | 362605951 | Xid | 1 | 362605978 | COMMIT /*

xid=23537929 */ |

+——————+———–+————+———–+————-+

—————————————————————+

14 rows in set (0.00 sec)

附上 binlog_format 变量的介绍

–binlog-format={ROW|STATEMENT|MIXED}

Version Introduced 5.1.5

Command-Line Format –binlog-format

Config-File Format binlog-format

Option Sets Variable Yes, binlog_format

Variable Name binlog_format

Variable Scope Both

Dynamic Variable Yes

Permitted Values (>= 5.1.5, <= 5.1.7)

Type enumeration

Default STATEMENT

Valid Values ROW, STATEMENT

Permitted Values (>= 5.1.8, <= 5.1.11)

Type enumeration

Default STATEMENT

Valid Values ROW, STATEMENT, MIXED

Permitted Values (>= 5.1.12, <= 5.1.28)

Type enumeration

Default MIXED

Valid Values ROW, STATEMENT, MIXED

Permitted Values (>= 5.1.29)

Type enumeration

Default STATEMENT

Valid Values ROW, STATEMENT, MIXED

Specify whether to use row-based, statement-based, or mixed

replication (statement-based was the default prior to MySQL 5.1.12;

in 5.1.12, the default was changed to mixed replication; in 5.1.29,

the default was changed back to statement-based). See Section

16.1.2, “Replication Formats”. This option was added in MySQL

5.1.5.

Important

Setting the binary logging format without enabling binary logging

prevents the MySQL server from starting. This is a known issue in

MySQL 5.1 which is fixed in MySQL 5.5. (Bug#42928)

MySQL Cluster. The default value for this option in all MySQL Cluster

NDB 6.1, 6.2, 6.3, and later 6.x releases is MIXED. See Section

17.6.2, “MySQL Cluster Replication: Assumptions and General

Requirements”, for more information.

may your success.

eshop MySQL 数据库主备机数据 xtrabackup 同步数据

今天需要再次用到 xtrabackup 来备份和恢复数据。找了半天终于把以前写的

一 个文档找到 了 。 保存在 blog 中 。 以防丢失。 xtrabackup 更换 为

xtrabackup-1.2-22.rhel5.x86_64.rpm 也已经测试通过

一.迁移目的

目前 ITBU eshop MySQL 数据库有 64 台数据库服务器。两两互备作为双

master 结构相互备份。但是由于应用方使用了 MySQL 的 uuid函数,导致两

两互备的 mysql 服务器之间的数据不一致。这样两个数据库切换将导致用户数

据的丢失。

4月底,应用方修改了 uuid 的问题,避免的新的数据不一致问题。接下来就只

有数据库中已有的数据不一致问题了。

MySQL 数据库,由于主备之间已经切换过多次,目前一部分数据已经无法找

回。跟应用沟通后,确定以目前主库的数据为准,将主库的数据同步到备库。

备库的数据备份到本机。保存一个月。

二.迁移要求

1、迁移过程中要求不影响应用,也就是说 eshop 数据库服务不能停止。

2、迁移完成后主备机数据保持一致。

三.迁移方案

这 里 由 于 管理维护的必要 , 我们假设主 备 机 我们都 是 以 root 登录 。

xtrabackup 在 MySQL 中的权限为:

root@127.0.0.1 : (none) 20:57:24> show grants for

‘xtrabackup’@'localhost’\G

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

Grants for xtrabackup@localhost: GRANT SELECT, RELOAD, LOCK

TABLES, REPLICATION CLIENT ON *.* TO ‘xtrabackup’@'localhost’

IDENTIFIED BY PASSWORD

‘*BF9F4C1B8BC37C75BBF482C8037EC944555D371A’

*************************** 2. row ***************************

Grants for xtrabackup@localhost: GRANT INSERT, CREATE, DROP ON

`mysql`.`ibbackup_binlog_marker` TO ‘xtrabackup’@'localhost’

2 rows in set (0.00 sec)

3.1.备份/恢复工具选择

由于迁移过程中不能够停止数据库服务,数据库备份必须选择热备份。两种选

择mysqldump 的逻辑热备以及 xtrabackup 的物理热备。为了恢复时间考虑,

决定采用 xtrabackup。

3.2.备机备份数据的选择

eshop网店目前有两个存储,他们目前都在老聚园路。一个存储暂时没有上电,

另外一个存储空间所剩不多。目前每台 eshop 备机平均为 130G 的数据,一共

32 台,需要 4T 多的存储空间。而如果把 eshop 的 32套备机数据放到存储上,

对存储空间和网络的消耗都是巨大的。

最终,我们选择在备机存储本机的老数据。而不做另外拷贝。

附件 eshop_backup_slave_data_20100422.sh 是备份备机数据的脚本

3.3.主备机数据拷贝方案选择

主机备份生成的数据大小约为 130G左右,正常的话我们可以通过 scp 来拷贝,

但是 scp拷贝要么需要手工输入密码,要么需要打通 ssh隧道。如果用脚本来

操作的话,只能打通主备机之间的 ssh隧道。另外 scp 不能够限速。数据拷贝

量大的话可能引起网络堵塞。最后,考虑采用 rsync拷贝数据,它不仅能够限

速。并且可以在主机上启动 rsync –daemon,不用打通隧道(需要注意在拷贝

完数据以后停止 rsync 后台进程)

附 件 eshop_master_rsync_daemon_local.sh 和

eshop_master_rsync_daemon_remote.sh 是用于在 eshop 的 master 机

器上添加 rsync daemon 的脚本。rsyncd.conf 是拷贝到主机的 rsync配置

文件。

3.4.主机数据库备份方案

eshop 主备机每天 5:30 的时候都会有一个计划任务判断当前主机是否是备机。

如果是备机则利用 xtrabackup(innobackupex-1.5.1版本为 0.7)生成物

理备份。这里我们只需要简单修改一下备份脚本就可以在主机上也生成备份(注

意主机备机完之后需要把该脚本修改回来)。直接利用

innobackupex-1.5.1 –slave-info –no-timestamp –user=”$

{XTRA_BACKUP_USER}” —password=”$

{XTRA_BACKUP_PASSWORD}” ${XTRA_BACKUP_DIR}

生成备份。这里需要注意 xtrabackup 用户的权限。

附件 eshop_master_backup_local.sh 是用于主机 xtrabackup 备份数据库

的脚本。backup_mysql.sh 是需要拷贝到主机上执行的脚本

3.5.备机数据恢复方案

主机数据拷贝到备机以后,由于是 xtrabackup 备份出来的,所以也要用它来

恢复。注意,在恢复备机数据库数据前需要将备机数据库 MySQL 先停掉。原

有的 MySQL 数据目录需要 mv 到另外的位置。并根据需要新建好数据库需要

的目录,为备份数据拷贝到对应目录准备好环境。

xtrabackup 需要先生成 logfile,然后将产生的相关数据拷贝到具体的数据文

件目录中。需要需要执行两次。

■第一次利用:

innobackupex-1.5.1 —apply-log

/home/mysql/fs3/master_bak_data/eshop_alsmy_eshop13b_2010_04

_27/

应用备份期间变化的数据和生成 ib_logfile。这里需要注意将该命令输出的结

果记下来。后面主备机双 maser 复制环境的搭建需要利用到这里的”Last

MySQL binlog file position”信息

■第二次利用:

innobackupex-1.5.1 —copy-back

/home/mysql/fs3/master_bak_data/eshop_alsmy_eshop13b_2010_04

_27/

将 生 成 好 的 数 据 拷 贝 到 /etc/my.cnf 指 定 的 datadir 和

innodb_data_home_dir等对应目录中。

3.6.备机数据库启动和清理

备机数据库数据恢复以后,需要将数据库的目录 owner 修改一下。chown -R

mysql:mysql ${MYSQL_DATA_DIR}。否则MySQL启动会出错。

启动 MySQL 数据库,正常的话启动是没有问题的。如果出现问题,需要查看

MySQL 的错误日志,并根据不同的错误类型处理。

启动成功以后,MySQL 数据库里面的数据都是主机的数据。我们需要根据不

同的情况对它进行修改。(这里最好 SET SQL_LOG_BIN=0避免把一些数据记

录 到 binlog 中 ,从而 复 制 到 主 机 去 了 。 )eshop 这边因 为 复 制帐号是

replicator@’${SLAVE}’基于备机 ip 地址的,所以需要修改为基于主机的复

制帐号。这样备机到主机的复制才能搭建起来。

root@127.0.0.1 : (none) 16:13:06> show grants for

replicator@’172.18.94.25′;

+

——————————————————————————————————

———————————————————————–+

| Grants for replicator@172.18.94.25 |

+

——————————————————————————————————

———————————————————————–+

| GRANT SELECT, RELOAD, SUPER, REPLICATION SLAVE,

REPLICATION CLIENT ON *.* TO ‘replicator’@’172.18.94.25′

IDENTIFIED BY PASSWORD

‘*3836CCEA58805DB4CE7093BF6170F7A6027CDD86′ |

+

——————————————————————————————————

———————————————————————–+

1 row in set (0.00 sec)

root@127.0.0.1 : (none) 16:14:20> GRANT SELECT, RELOAD,

SUPER, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO

‘replicator’@’172.18.94.26′ IDENTIFIED BY ‘xlia9810pal’;

Query OK, 0 rows affected (0.00 sec)

root@127.0.0.1 : (none) 21:21:02> drop user

replicator@’172.18.94.25′;

Query OK, 0 rows affected (0.03 sec)

root@127.0.0.1 : (none) 16:14:44> flush privileges;

Query OK, 0 rows affected (0.00 sec)

附件 eshop_slave_sync_master_data.sh 是用来恢复备机数据库和清理数据

的脚本。

3.7.主备机双 master 复制环境搭建

3.7.1.搭建备机到主机的复制

由于备机是从主机导入的数据,并且备机没有应用访问,binlog 没有变化。我

们首先搭建备机到主机的复制。首先确定备机的 binlog 位置。利用 show

master status 可以查到。

root@127.0.0.1 : (none) 21:37:38> show master status;

+——————+———–+————–+——————+

| File | Position | Binlog_Do_DB | Binlog_Ignore_DB |

+——————+———–+————–+——————+

| mysql-bin.000001 | 98 | | |

+——————+———–+————–+——————+

1 row in set (0.00 sec)

如无意外,都应该是第一个文件的最开始位置。

修改主机的 master 位置:

Stop slave;

CHANGE MASTER TO MASTER_HOST=’172.18.94.25′,

MASTER_USER=’replicator’, MASTER_PASSWORD=’xlia9810pal’,

MASTER_LOG_FILE=’mysql-bin.000001′, MASTER_LOG_POS=98;

Start slave;

检查是否复制搭建成功:

Show slave status\G

如果 Slave_IO_Running,Slave_SQL_Running 其中一个为 No。需要检查

MySQL 的错误日志。一般可能有两种错误:

■备机的授权有问题

■备机的 binlog 位置有问题

附件 eshop_master_change_master.sh 为配置备机到主机的复制环境的脚

本。

3.7.2.搭建主机到备机的复制

保证备机到主机的复制正确以后,我们就可以搭建主机到备机的复制了。

备机从主机的哪个位置开始复制很重要,这个位置是从 备机数据恢复方案 中得

到的。在 apply-log 的时候,有一行文字很重要:

InnoDB: Last MySQL binlog file position 0 651339984, file name

./mysql-bin.001604

这 里 记 录 了 主 机 的 复 制 开 始 文 件 和 开 始 位 置 ( 这 里 就 是 mysql-

bin.001604,651339984)。那么我们搭建主机到备机的复制就很简单了:

Stop slave;

CHANGE MASTER TO MASTER_HOST=’172.18.94.26′,

MASTER_USER=’replicator’, MASTER_PASSWORD=’xlia9810pal’,

MASTER_LOG_FILE=’mysql-bin.001604′,

MASTER_LOG_POS=651339984;

Start slave;

检查是否复制搭建成功:

Show slave status\G

如果 Slave_IO_Running,Slave_SQL_Running 其中一个为 No。需要检查

MySQL 的错误日志。

这里由于备份的时间已经经过了这么长的时间,主机到备机的复制肯定有延迟,

静静的等待主备机的复制就可以了。

附件 eshop_slave_change_master.sh 为配置主机到备机的复制环境的脚本。

四.附件介绍

附件中的各个脚本介绍如下:

■附件 eshop_backup_slave_data_20100422.sh 是备份备机数据的脚本

■附件 eshop_master_backup_local.sh 是用于主机 xtrabackup 备份数据

库的脚本。backup_mysql.sh 是需要拷贝到主机上执行的脚本

■ 附 件 eshop_master_rsync_daemon_local.sh 和

eshop_master_rsync_daemon_remote.sh 是用于在 eshop 的 master 机

器上添加 rsync daemon 的脚本。rsyncd.conf 是拷贝到主机的 rsync配置

文件。

■附件 eshop_slave_sync_master_data.sh 是用来恢复备机数据库和清理数

据的脚本。

■附件 eshop_master_change_master.sh 为配置备机到主机的复制环境的

脚本。

■附件 eshop_slave_change_master.sh 为配置主机到备机的复制环境的脚

本。

may you success.

MySQL 数据库机器搬迁 checklist

itbu 的数据库机器从老机房搬迁到新机房的项目已经接近尾声了,这次的搬迁

是我经历的一个比较大的项目,虽然整体的调控不是我主导的,但是 MySQL

数据库的搬迁都是我在主导进行,期间出现了一些问题,还好没有出现非常重

大的错误。从中相信搬迁的大部分同学都成长了很多。我自己感觉我学到了很

多,也成长了很多,虽然加班加点的,累的够呛,还是很值得的。为了避免以

后大家再走弯路,也避免自己忘记搬迁的重要事项,特别记录下来,做为一个

搬迁工程的 checklist。方便大家搬迁过程中检查,不要遗漏了一些东西。

1、确认方案。尽早和应用方沟通,确认采用平滑搬迁或者停机搬迁的方案。

平滑搬迁是指MySQL 数据库停止备机,然后搬迁过去,在新环境搭好以后,

配置 MySQL 的双 master 复制,调试通过,应用服务正常。然后直接刷

dns(或者其他方式)使应用的真实用户访问新的服务。

停机搬迁的方式是指,应用发布停机公告,在适当的时机,将应用服务器和数

据库服务器等搬迁到新机房,部署上线。

上面的两种方案,第一种风险较小,对用户影响也非常小。万一刷过去以后,

真实用户访问有问题,还可以切回来。第二种风险比较大,甚至可能出现有些

配置修改修改不及时,造成停机时间已经过了,应用无法访问的问题。

2、确认机架位置。正常的话,sa负责人会将搬迁的机器列表和机柜,机架位

置发给大家一份。以方便现场工程师以及对应人员确认搬迁的就是这批机器。

我们需要注意一下,并提醒现场工程师机柜和机架号。eshop搬迁的时候,我

们的一个现场工程师就看错了机柜号,拔掉了一台正在提供应用服务的机器,

还好立刻意识到了,大家一起快速修复了这个问题。

3、确认搬迁的机器 IP 和搬迁到新机房的机器 IP。老机房需要搬迁的机器 IP 在

上一步就需要确认好了,新机房的机器 IP 需要跟 sa 和网络部门的同事沟通,

我们提供具体的分配策略,网段划分,新老机器 IP 对应关系。

4、确认时间。及早跟各应用方沟通搬迁时间,如果有变化,及时相互通知。

5、搬迁前准备工作。eshop 这边搬迁前需要重新同步主机数据到备机。其他

应用需要注意主备机的复制延迟,尽量保证复制延迟较低。

6、通知所有相关人员。包括数据仓库,应用开发人员,网络,sa。告知他们

新的数据库 IP 地址和连接方式。另外还需要检查 MySQL 里面的所有账户,看

这些用户是否都通知到了。pm 就没有及时通知数据仓库人员,导致第二天他

们取不到数据的问题。

7、修 改搬迁以 后 的配置 。包括 MySQL 账户的 修 改 ,heartbeat 的 修 改

cobar 的修改,脚本的修改,脚本的配置文件修改,带外登录等。

a)MySQL帐号的修改。检查 MySQL 的账户,看是否还有依赖于老的 IP 地址

段的账户。比如:eshop 的复制帐号 replicator@172.18.%搬迁到新的机房

就肯定需要修改掉。

b)heartbeat 的 修 改 ; cobar 的 修 改 。 机 器搬迁到 新 的 机房, 需 要 修 改

heartbeat 和 cobar 的配置。这样才能让应用访问新的 VIP 地址。

c)脚本的修改。目前 MySQL 的管理有许多脚本,这些脚本有些是对 IP 有依赖

关系的,需要及时修改掉。当然,最好修改掉这样的脚本,去掉对 IP 的依赖。

d)脚本配置文件的修改。有些脚本通过读取配置文件来依赖 IP,同样需要修改

配置文件。如果可能的话,修改脚本,去掉对 IP 的依赖。

e)带外登录。带外登录地址随着机器 IP 的更换也会变化。搬迁过去以后,需要

再次验证一下带外登录的方式是否还正常。

该 checklist 可能还不是很完全,需要补充和优化。

may you success.

Recommended