Posts tagged ‘slave’

淘宝物流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

mysql server_id错误配置讨论

在ITPUB上看到一个面试题,感觉很有意思,不知道大家有没有遇到过这样的问题:

NewImage

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

两种情况,

第一种情况:两个slave有同样的server-id会有什么问题?

第二种情况:如果是级联复制,再级联复制的路径上有相同的server-id会有什么问题。

 

 

其实很简单,只要你搭建复制,做一些简单的操作,你就能发现问题。

在第一个场景下,你会发现两个slave在不断的重连master,日志里面也会有错误信息,说slave被断掉,尝试重连,并且也连上去了。然后又被断掉,又重连…循环往复。

第二个场景,最后一个slave不能得到master的变更,在master上做的任何操作都不会应用到最后一个slave上。

 

 

MySQL复制的原理这边就不详细描述。简单的说,就是如果开启了log-bin记录二进制日志,master会在自己的binlog中记录下变更发生的时间,query(如果是Row的话会转成具体的行变更),server-id等信息,然后当slave请求(请求的信息中包括连接master的ip,port等连接信息;以及需要从master的那个二进制文件的哪个位置开始,获得之后的所有master变更)发送到master之后,master启动一个binlog dump的线程用于发送变更信息给该slave。slave通过IO thread接受消息,记录到自己的relay log中,(如果发现server-id等于自己的,它就认为是在本地产生的,直接丢弃),然后SQL thread读取relay log应用到本地MySQL。我们理解了这个,其实上面的两个问题就能够迎刃而解了。

第二个问题更加直观,master和第三个slave的server-id是一样的,所以第三个slave发现master产生的binlog,误以为是它自己产生的,就会丢弃掉。这样的话,master产生的binlog在第三个slave就无法应用。

第一个问题比较复杂一点,牵涉到master怎么处理server-id相同的slave请求。这里冲突的焦点就是,master怎么区分各个slave的不同之处。在MySQL中,就是用server id来区分的。比如:左边的slave首先连上master,master会分配binlog dump线程并于该slave通讯,发送变更信息;这时,右边的slave连上来,server-id也是2,master以server-id来区别slave,所以它认为是同一个slave连过来,请求另外一个binlog和对应position处开始的数据,于是按照正常逻辑它清理了左边slave的binlog dump线程,并给右边的分配binlog dump线程。左边的slave发现复制断掉以后会自动重连,所以右边的又悲剧了…循环往复,两个slave一直这样相互冲突。源码参看:sql/sql_parse.cc dispatch_command()的COM_BINLOG_DUMP段,截取部分源码如下:

pos = uint4korr(packet);
flags = uint2korr(packet + 4);
thd->server_id=0; /* avoid suicide */
if ((slave_server_id= uint4korr(packet+6))) // mysqlbinlog.server_id==0
····kill_zombie_dump_threads(slave_server_id);
thd->server_id = slave_server_id;

 

原因分析完了,我们来说说具体的解决方案:那就是要尽量避免上面两种情况:master的各个slave必须有不同的id;级联复制各节点不能有相同的server-id。

不妨把条件升级一下:所有的MySQL server_id都不允许一样?

server-id是用4个字节的unsigned int来保存的,值的范围:0 .. 4294967295。有将近43亿的server-id可以用。足够我们使用了。

要保证唯一的话,手工分配是完全不靠谱的,自动分配的话,我们提供两种方法以供参考:

1、使用一个集中式的自增的公共服务。可以用oracle的sequece,mysql的自增值等。优点是比较简单,缺点就是需要一个额外的服务

2、使用IP+PORT来算出一个server-id,一个IP和PORT可以唯一定位一个MySQL实例,所以各个MySQL server-id不会有冲突。IP和PORT怎么跟server-id对应列。由于IP本身对应的就是一个4个字节,(比如:INET_ATON(‘209.207.224.40’)=209×2563 + 207×2562 + 224×256 + 40=3520061480),加上端口两个字节,所以做不到完全的一一对应的关系。但是IP地址中有很多保留的的地址(比如:127.0.0.0到127.255.255.255是保留地址),另外由于一个IP对应的实例毕竟有限(你不可能在一个IP上启动65535个MySQL实例吧?),所以针对你自己的业务场景做一套适应的IP,PORT对应server-id的算式也是一个解决办法。比如:如果你高位两个字节都相同,就可以把端口的两个字节和IP后两个字节拼接成一个4字节的无符号整数作为server-id。

双slave的server_id相同的问题

前段时间新来了一个机器,想要替换一个应用的备机。
已有的架构是这样的:A和B两个MySQL相互作为备机,A的性能比较好,B的性能比较差。
现在买了一台新的机器,新的机器c性能比较好,准备用来替换B。
安装MySQL和配置等等都不细说,比较顺利的。

但是安装好以后,配置复制的时候出现了问题,change master以后可以复制,但是数据库监控一直报错。
show slave status\G显示的Seconds_Behind_Master:的复制延迟一下子是0,一下子是90多w秒。
以前我们也遇到过类似的问题,具体原因不大记得了(唉,就是没有文档记录的错阿),最后好像是通过重做复制修复了。
尝试重新change master以后还是没有好转。查看错误日志显示如下:
100817 19:45:45 [Note] Slave I/O thread: Failed reading log event, reconnecting to retry, log ‘mysql-bin.000033’ at postion 271148235
100817 19:45:45 [Note] Slave: received end packet from server, apparent master shutdown:
100817 19:45:45 [Note] Slave I/O thread: Failed reading log event, reconnecting to retry, log ‘mysql-bin.000033’ at postion 271148235
100817 19:45:45 [Note] Slave: received end packet from server, apparent master shutdown:
并且不断的在刷,每秒钟有十来条记录。
通过对应的binlog file和binlog position去查看主机对应的记录都没有问题。并且主机到备机的复制只是时断时续,并没有一直停着不往前走。
实在没有办法,最后停掉了复制。这个事情因为并不是那么紧急,所以也暂时放在一边了。

偶尔的机会,我在备机B上也发现了同样的错误,也是Slave: received end packet from server, apparent master shutdown的错误。
并且change master以后,出现的频率变的非常高。
看了一下my.cnf配置文件,注意到B和C配置的server_id都是2,因为C当时是计划替换B的所以server_id也配置为一样了。
是不是这个问题引起的列?试试就知道了,把C的server_id配置成3,数据库B和数据库C检查复制状态,好了!

想起数据库复制的那一块代码。数据库连接master,请求复制开始的时候需要几个参数:
1、四个字节的数据文件开始位置
2、两个字节的binlog flag(目前还没有用上)
3、2个字节的binlog file名字字符串长度
4、4个字节的server_id
5、n个字节的binlog file名字字符串(在前面3里记录了它的长度)
这里向主机申请的时候,需要填写自己的server_id,来通知master连上来的是谁。
如果现在有两个slave连上来并都申明自己的server_id为2,MySQL的master就有点无所适从了。
初步怀疑是slave的IO线程连上以后,master过了一段时间(ms级别的)才发现,然后把新的连接断掉,所以slave io线程才会报警:
100817 19:45:45 [Note] Slave I/O thread: Failed reading log event, reconnecting to retry, log ‘mysql-bin.000033’ at postion 271148235
100817 19:45:45 [Note] Slave: received end packet from server, apparent master shutdown:
说收到主机发起的end packet(主机每次发送一个event过来,如果1、收到的包数据有误或者2、不能由接收到的包解出正确的event来,slave io判定复制有问题,
而其中end packet属于第一种情况
对应的代码为sql/slave.cc的read_event函数:
/* Check if eof packet */
if (len < 8 && mysql->net.read_pos[0] == 254)
{
sql_print_information(“Slave: received end packet from server, apparent ”
“master shutdown: %s”,
mysql_error(mysql));
DBUG_RETURN(packet_error);
}

由于MySQL的io有自动重连的机制,所以MySQL的io线程又连主机,并且还连上了。
master过了一段时间(ms级别的)又发现已经有了server_id=2的slave连着的,把它重新断掉了。
看了一下master的代码sql/sql_parse.cc的dispatch_command函数:
mysql_binlog_send(thd, thd->strdup(packet + 10), (my_off_t) pos, flags);
unregister_slave(thd,1,1);
发送binlog以后,它会做一个unregister_slave,它认为是同一个slave change master连接过来,“理所当然的”它就把前面的连接给干掉了。
周而复始,就产生了这样的错误。
解决的办法很简单,就是不要用同样的server_id的slave同时连一台master。server_id可以设置的很大,所以不要顾虑,赶紧重设吧。

不知道有没有人想过为什么server_id不能设置为0
在my.cnf里面是没有办法设置server_id=0的,不信的话,你试试,正常情况下,它不会让你start slave的。代码如下sql/slave.cc的start_slave_thread函数:
if (!server_id)
{
if (start_cond)
pthread_cond_broadcast(start_cond);
if (start_lock)
pthread_mutex_unlock(start_lock);
sql_print_error(“Server id not set, will not start slave”);
DBUG_RETURN(ER_BAD_SLAVE);
}
不过,我还真的没有测试过这种情况,呵呵。
当然,server_id设置为0的情况也有,但是不是你或者我,而是mysqlbinlog,它的server_id可以设置为0.
原因如下:如果mysqlbinlog的server_id为0的话,它就会把正常的slave给停掉了。万一它的server_id为5,它连上去以后就会把真正的server-id为5的slave断掉了。
server_id设置为0我印象中还有一个不同点,是关于MySQL master发送binlog是否会停止的,但是就是想不起来了,老了。代码也没有找到确凿的证据;先这样把

may your success!