主备备的两个备机转为双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
测试报告
目 录
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值 |
图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操作的提交和复制过程。
- 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时停止复制,而避免数据复制的死循环。
- 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语句。针对这个问题,我们有两 种解决方案:
-
-
- 由于在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生成了若干行能够被匹配的 数据的话,这种方案是行不通的。
-
-
- 第二种解决方案相对复杂一些。它保证了 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 ;
- 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@local