Posts tagged ‘replication’

淘宝物流MySQL slave数据丢失详细原因

前两天,惊闻淘宝发生了一个非常狗血的事情,备库复制状态一切正常,但是备库的数据DDL可以复制过去,DML都丢失了。导致数据库数据不一致。这样的话,就算你有slave监控,也发现不了主备数据延迟和不一致的问题。

最后淘宝希羽定位到了问题,并提出了解决方案。参考http://hickey.in/?p=146。不过具体原因和为什么会发生这个问题并没有说的那么详细。沃趣科技特别针对这个问题做了深入详细的研究,整理出来,以飨观众。

首先我们需要跟大家解释一下MySQL复制的基本原理。

主库为每一个slave开启一个binlog dump线程,用于把本机记录下所有的变更,发送给备库;备库使用io thread线程接收数据存入relay log中;然后由sql thread线程从relay log中读出来应用到本地。这个是大家都熟知的。我们不详细介绍。但是slave是怎么注册上主库,主库是怎么通知各个binlog dump线程,binlog dump和io thread线程怎么通讯,io thread怎么重连,relay log怎么读取二进制数据翻译成对应的信息应用在slave上;我们就不得而知了。而问题就发生在最后“relay log怎么读取二进制数据翻译成对应的信息应用在slave上”

binlog是二进制数据,必须用mysqlbinlog工具才能打开。所以我们有必要先介绍一下binlog的格式。也就是insert,update,delete等这些数据是怎么以二进制形式记录到binlog文件中去的。binlog文件是按照event来组织的。每个文件前4个字节是fe 62 69 6e,接下来就是各个event了。event有很多种类型。列出如下:

比如ROTATE_EVENT对应的记录了binlog切换到下一个binlog文件的信息,XID_EVENT记录了一个事务提交的相关信息。Binlog_format可以设置为STATEMENT和ROW的方式。当设置为STATEMENT情况下,DML会记录为原始的SQL,也就是记录在QUERY_EVENT中。而ROW会记录为TABLE_MAP_EVENT+ROW_LOG_EVENT(包括WRITE_ROWS_EVENT,UPDATE_ROWS_EVENT,DELETE_ROWS_EVENT)。使用mysqlbinlog可以看看他们的区别。

STATEMENT方式下,记录为QUERY_EVENT如下图:

statement_event

 

 

 

 

 

ROW方式下,update一条记录如下:

row_simple

 

 

 

 

这样的话我们就无法看到它到底update了什么数据,使用mysqlbinlog -vvv可以让它更详细的翻译给我看:

row_vvv

 

 

 

 

 

 

 

淘宝采用的是ROW方式,有两个好处:第一:更容易解析,DRC或者mysql transfer等淘宝系数据库迁移工具可以精确的解析出数据,进行同步;第二:可以有效避免rand(),uuid()等由于主备环境不一致而导致的问题。

这里还有一个地方需要解释一下,为什么一个update在ROW模式下需要分解成两个event:一个Table_map,一个Update_rows。我们想象一下,一个update如果更新了10000条数据,那么对应的表结构信息是否需要记录10000次列,其实是对同一个表的操作,所以这里binlog只是记录了一个Table_map用于记录表结构相关信息,而后面的Update_rows记录了更新数据的行信息。他们之间是通过table_id来联系的。

淘宝的问题也就出现在这里,这两个事件是通过table_id来联系的,table_id是ulong类型的。刚好这个联系在淘宝的这个环境下就断了。具体的细节要牵涉到部分源码。对源码不感兴趣的同学可以直接跳过这一段:

首先,我们了解一下记录表定义信息的数据结构。它对应的class是Table_map_log_event(对应源码sql/log_event.cc),它保存了本次DML操作对应的:table_id,数据库名,表名,字段数,字段类型等。对应的这些信息都是保存在一个table_mapping的hash数据结构中(sql/rpl_tblmap.cc中)。hash的key就是table_id,hash的值就是TABLE*的数据结构(包含了表的各种信息,包括数据库名,表名,字段数,字段类型等),通过set_table()方法来hash,通过get_table()方法来根据table_id获得对应的表信息。这里table_id是ulong型的。

然后我们了解一下存储变更的具体数据的数据结构。update行的Update_row event对应的class是Update_rows_log_event(对应源码sql/log_event.cc),基类是Rows_log_event(之类分别有:Write_rows_log_event,Update_rows_log_event,Delete_rows_log_event与insert, update, delete一一对应)。一个Row_log_event对应一行数据的变更(插入,更新,删除),它记录的信息包括table_id,哪些字段为空的bitmap,各个字段的具体数据等。这里table_id是ulong型的。

所有的binlog event有一个公共的父类Log_event(对应源码sql/log_event.h),每一个log_event都是通过do_apply_event()方法来将event应用到本地数据库去。

另外,我们必须要介绍一下RPL_TABLE_LIST结构(对应源码sql/rpl_utility.h),它的父类TABLE_LIST(对应源码:sql/table.h)中定义table_id为:

  uint          table_id; /* table id (from binlog) for opened table */   //这里就是最终需要bug fix的地方

这里定义为uint和其他地方定义为ulong不一样!

这里就是最终需要bug fix的地方。问题的原因我们还要继续看。这个RPL_TABLE_LIST是包含在Relay_log_info结构(对应源码sql/rpl_mi.h)中的,它记录了这次变更需要lock的多个表信息。

 

前面提过,每一个event都有一个do_apply_event()方法用于将event应用到本地数据库中去。int Table_map_log_event::do_apply_event(Relay_log_info const *rli)方法(对应源码sql/log_event.cc)中就将ulong型的m_table_id赋值给uint型的table_list->table_id,而table_list作为tables_to_lock存入了公共变量rli中。

table_list->table_id= DBUG_EVALUATE_IF(“inject_tblmap_same_id_maps_diff_table”, 0, m_table_id);

/*
We record in the slave’s information that the table should be
locked by linking the table into the list of tables to lock.
*/
table_list->next_global= table_list->next_local= rli->tables_to_lock;
const_cast<Relay_log_info*>(rli)->tables_to_lock= table_list;
const_cast<Relay_log_info*>(rli)->tables_to_lock_count++;
/* ‘memory’ is freed in clear_tables_to_lock */

Row_log_event类的int Rows_log_event::do_apply_event(Relay_log_info const *rli)方法(对应源码sql/log_event.cc)中:

TABLE_LIST *ptr= rli->tables_to_lock;
for (uint i=0 ; ptr && (i < rli->tables_to_lock_count); ptr= ptr->next_global, i++)
const_cast<Relay_log_info*>(rli)->m_table_map.set_table(ptr->table_id, ptr->table);

利用记录在rli(Relay_log_info结构)中的tables_to_lock获得table_list(RPL_TABLE_LIST结构),而这个结构里面的是已经被截断的uint型的table_id。

当需要对具体的表进行row变更的时候在同样的int Rows_log_event::do_apply_event(Relay_log_info const *rli)方法(对应源码sql/log_event.cc)中:

TABLE*
table=
m_table= const_cast<Relay_log_info*>(rli)->m_table_map.get_table(m_table_id);

DBUG_PRINT(“debug”, (“m_table: 0x%lx, m_table_id: %lu”, (ulong) m_table, m_table_id));

通过ulong型的值去获得hash表中获得表结构信息就无法获取了。也就是说,之前用uint型的ptr->table_id构建出来的key,value的hash对,用ulong型的m_table_id是无法查询到的。

图示如下:

tableid_bug

 

 

 

 

 

 

 

 

 

 

为了举例简单,假设uint 4 bit ulong 8bit,ulong11111111被存到了uint型的数据中(假设被截断为00001111)并存到hash表中去了,那么对应的ulong型的key去查数据时,插到的表定义肯定就是NULL了。这样的话,表结构找不到,备机也就无法同步主库的任何DML数据,也就是淘宝物流库备机与主机不一致的原因了。

 

那么是什么原因导致的这个问题列。淘宝是因为它的table cache设置过小,table_definition_cache为256,table_open_cache为512,而该实例上由于分库分表,表一共有4301个,table cache严重不足。这里很多人对table_id有误解,认为table_id是跟表一起走的,是固定的。其实table_id是表载入table cache时临时分配的,一个不断增长的变量。当table cache不足,flush table又非常多的时候,这个table_id增长的速率非常快,达到uint的上限时,2的32次方以后,就触发了这个bug,导致主备不一致。广大的MySQL使用者都留意一下,你的table_id是不是也非常大了,超过2的32次方,那么你的备机就重搭吧。

解决方案:

1、自己打patch,重编译并替换线上MySQL

2、增加table cache 大小。

3、重启主库让table_id归零。

 

参考:

http://hickey.in/?p=146

http://hatemysql.com/2011/12/14/mysql-show-slave-status/

http://dev.mysql.com/doc/internals/en/binary-log.html

主备备的两个备机转为双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_host) 1 or 2 标识接下来的report_host的长度,如果长度<251占1个字节,否则占两个字节
report_host Strlen(report_host) 向主服务器注册的MySQL instance标识
strlen(report_user) 1 or 2 标识接下来的report_user的长度,如果长度<251占1个字节,否则占2个字节
report_user Strlen(report_user) 向主服务器注册的用户名
strlen(report_password) 1 or 2 标识接下来的report_password的长度,如果长度<251占1个字节,否则占2个字节
report_password Strlen(report_password) 向主服务器注册的密码
report_port 2 向主服务器注册的端口
rpl_recovery_rank 4 复制的恢复等级
master_id 4 填入0,主服务器将自行填入master_id值
register_master

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\x69\x6e”) BIN_LOG_HEADER_SIZE(4) Binlog文件的标识值

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_EVENT,UPDATE_ROWS_EVENT,DELETE_ROWS_EVENT等。

事件头字段描述

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

名称 字节数 含义
When 4 事件的创建时间。
Type 1 事件的类型(见附录1)。
server_id 4 事件发生时所在MySQL的server_id值。
data_written 4 该事件一共占用的字节数,包括事件头的字节数。
log_pos 4 下一事件在binlog文件中将要开始的位置,即本事件的结束位置
Flags 2 事件的其他标志位。

ROTATE_EVENT事件字段描述

由于各个事件的事件头基本一致,这里我们就不重复介绍事件头的各字段了,后面的各个事件我们也都将忽略对事件头字段的描述。

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

名称 字节数 含义
pos 8 主服务器将要发送的事件记录在binlog文件中的偏移量。一般为从服务器提交的COM_BINLOG_DUMP请求中的偏移量值。

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

名称 字节数 含义
new_log_ident strlen(new_log_ident) 主服务器将要发送的事件记录的binlog文件名。一般为从服务器提交的COM_BINLOG_DUMP请求中的binlog文件名。

FORMAT_DESCRIPTION_EVENT事件字段描述

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

名称 字节数 含义
binlog_version 2 Binlog文件的版本号,这里一般为最新的版本号4
server_version ST_SERVER_VER_LEN(50) MySQL的版本号。例如:” 5.1.20-beta-log”
Created 4 事件创建时间,这里一般和事件头中的when一致
event_header_len 1 一般事件的事件头长度,一般设置为:LOG_EVENT_HEADER_LEN(19)
post_header_len ENUM_END_EVENT-1(26) 不同事件类型的附加事件头的长度,见附录2。

TABLE_MAP_EVENT事件字段描述

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

名称 字节数 含义
m_table_id 6(5.1.4前的版本中为4) 表的id标识符
m_flags 2 表的各种标志位,见附录4

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

名称 字节数 含义
m_dblen 1 数据库名的长度
m_dbnam m_dblen+1 数据库名,以’\0’结尾
m_tbllen 1 表名的长度
m_tblnam m_tbllen+1 表名,以’\0’结尾
m_colcnt net_field_length() 表的字段个数,所占字节数根据第一个字节的大小由net_field_length函数确定
m_coltype m_colcnt 表的各个字段的字段类型,参见附录3。

WRITE_ROWS_EVENT事件字段描述

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

名称 字节数 含义
m_table_id 6(5.1.4前的版本中为4) 表的id标识符
m_flags 2 表的各种标志位,见附录4

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

名称 字节数 含义
m_width net_field_length() 表的各列的位图长度,所占字节数根据第一个字节的大小由net_field_length函数确定
m_cols.bitmap (m_width + 7) / 8 表的各列的位图,每一位表示m_rows_buf是否包含表中一列的值,如果没有置位表示该列的值没有包含在m_rows_buf中
m_rows_buf 剩余字节数(len-已占字节数) 将要插入到表中的一行数据值。

UPDATE_ROWS_EVENT事件字段描述

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

名称 字节数 含义
m_table_id 6(5.1.4前的版本中为4) 表的id标识符
m_flags 2 表的各种标志位,见附录4

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

名称 字节数 含义
m_width net_field_length() 表的各列的位图长度,所占字节数根据第一个字节的大小由net_field_length函数确定
m_cols.bitmap (m_width + 7) / 8 表中被匹配行数据的各列的位图,每一位表示m_rows_buf是否包含表中该列的值。
m_cols_ai.bitmap (m_width + 7) / 8 表中将要更新的行数据的各列的位图,每一位表示m_rows_buf是否包含表中一列的值。
m_rows_buf 剩余字节数(len-已占字节数) 表中被匹配的那一行数据的值以及将要更新的一行数据值。

DELETE_ROWS_EVENT事件字段描述

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

名称 字节数 含义
m_table_id 6(5.1.4前的版本中为4) 表的id标识符
m_flags 2 表的各种标志位

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

名称 字节数 含义
m_width net_field_length() 表的各列的位图长度,所占字节数根据第一个字节的大小由net_field_length函数确定
m_cols.bitmap (m_width + 7) / 8 表的各列的位图,每一位表示m_rows_buf是否包含表中一列的值。
m_rows_buf 剩余字节数(len-已占字节数) 表中将要删除的一行数据值。

XID_EVENT事件字段描述

XID_EVENT一般出现在一个事务操作(transaction)之后或者其他语句提交之后。它的主要作用是提交事务操作和把事件刷新至binlog文件中。

XID_EVENT事件的信息字段包括:

名称 字节数 含义
xid sizeof(xid) 8 commit标识符

测试过程

测试进度安排

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. 由于在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. 第二种解决方案相对复杂一些。它保证了 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.