日期:2014-05-16  浏览次数:20764 次

淘宝物流MySQL slave复制数据丢失问题的个人整理

         对于这几天微博上较火的关于淘宝物流MySQL slave复制数据丢失问题,我自己也比较关注,然后根据沃趣科技的一篇分析文章算是大概明白了其中的明细,现在再来根据我自己的理解理一下思路,顺便加深自己的理解。

        简单的说这个问题的来由是这样的:主库的一些DML语句在从库上没执行。那么遇到这样问题,我们一般都是从这几个方面找问题,

  • show slave status查看复制状态,SQL_THREAD,IO_THREAD是否正常/延迟多少
  • 上面没有问题,再看事件在主库是否记录了binlog,是否传到了从库(从relay log中查看)
  • 上面都没问题,那是否从库在这个事件的数据丢失之前就已经是M/S数据不一致了,导致DML没有有效的记录进行操作
        淘宝的这个案例就比较有意思了,上面的情况都不符合。后来他们定位到问题是由于binlog(RBR)里的table_map_event事件中的table_id在内部执行过程中传递值溢出导致的。这个table_map_event和table_id是怎么回事呢?先简单看个RBL的binlog event:

#121031 16:25:45 server id 1  end_log_pos 3466 Table_map: `test`.`tf` mapped to number 40
#121031 16:25:45 server id 1  end_log_pos 3504 Write_rows: table id 40 flags: STMT_END_F


BINLOG '
ieCQUBMBAAAAKgAAAIoNAAAAACgAAAAAAAEABHRlc3QAAnRmAAIDAwAD
ieCQUBcBAAAAJgAAALANAAAAACgAAAAAAAEAAv/8bwAAAN0AAAA=
'/*!*/;
### INSERT INTO test.tf
### SET
###   @1=111
###   @2=221
# at 3650
#121031 16:25:45 server id 1  end_log_pos 3531 Xid = 71
COMMIT/*!*/;

        红色标记部分显示了table_map_event和table_id,那么RBL为什么要这么做呢?在沃趣科技里面解释了这点(应该比较合理):一个DML可能影响很多行,而RBL是对每行进行记录,那么就用一个table_map来记录这个表的相关信息比较合理,而不是每行都记录。那table_id的作用是用来干什么的呢,简单的说它最主要的作用是用来将某个表的信息hash到cache中做hash key的,将表信息hash到cache中有是用来干什么呢?因为RBL的一个更新可能影响很多行,每行在真正应用到slave上之前必然要经过一些检查,而这些检查必然是与它将要操作的表的信息做对比验证,如果我们不将这个表的信息放到内存中,并且快速的定位到它,那么当影响的行很多时效率估计就会下降比较厉害。其实这一步本身想法没什么问题,只是MySQL当初实现时用到了这样一个table_id。
        接下来说引起这个案例的本质原因,binlog中table_id它是一个ulong类型(无符号长整形),在slave进行重做binlog events之前,它会先将这个ulong的table_id传给一个它内部维护的一个数据结构RPL_TABLE_LIST,这个里面有一个变量table_id用来存储binlog中的m_table_id(为了避免混淆,用m_table_id),好了问题出现:数据结构的变量table_id是一个uint(无符号整形),如果m_table_id超过uint的范围会发生截断。而MySQL内部在构造hash,从hash表中取值是这样的做法:set_table(table_id),get_table(m_table_id),在两个阶段用到的key因为发生了数据截断所以必然也就不能取到预期的值,因为也就发生了slave端relay log的DML语句没有被执行这种情况。
        那怎么可以避免这个问题呢?第一,不让m_table_id太大,实际上我相信对于绝大部分公司也不会太大,淘宝这个案例的背景是这个实例有4000+的表;第二,适当的增大table_open_cache这个参数,因为m_table_id并不是与每个表的元数据信息id(类似表空间的tablespace id),它是这个表被打开时内部分配的id,当表被关闭后再次打开,那么分配给他的这个id就会递增,table_open_cache正好是控制能最多cache被打开的表。所以适当增加这个参数可行。第三,将RPL_TABLE_LIST这个内部数据结构里面的table_id类型改为ulong(已经上报bug)。
        最后一个小问题,这里为什么要用这个递增的m_table_id,而不用与表元数据的id(类似表空间的tablespace id)。因为完全可能出现这种情况,master上先DML一些数据-->然后执行了alter table-->然后继续DML,此时如果使用的与表相关的固定id, 那么第一次DML在slave上执行时就会将与之相关的表信息hash到cache中,然后alter table之后,这些cache中的数据并没有更新,当接在在DML的时候因为是用相同的id去get_table,得到不是最新的表数据信息,那么接下来真正对每行进行更新时就可能出问题了。而用了递增的m_table_id后,只要m_table_id发生了变化,那么是一定取不到之前非最新的表信息。
        些许感悟,有时候只是表面上的安全,尤其使用开源产品;多谢淘宝这样的大公司,有这样的业务才能出这样的案例;做MySQL还真得懂点源码(一个团队有人懂差不多就行了,遇到这种问题不懂源码怎么解决问题?)

参考文章:
  1. http://hickey.in/?p=146 物理MySQL slave复制数据部分丢失问题
  2. http://vdisk.weibo.com/s/guZ8J 沃趣科技对此问题的详细分析
  3. http://blog.chinaunix.net/uid-26896862-id-3329896.html CUer对binlog中table_id的源码分析