腾讯游戏数据自愈服务方案简介
1. 引言
在正式介绍项目背景之前,让我们先看一组数据:
这是2个灰度的业务,都是Z3服务器,我们先只从时间成本的收益角度来看:
⑴ 左一业务数据量是330G,数据不一致时通过重做slave需要150分钟左右,而借助pt-table-sync只需要5分钟,速度提升30 倍。
⑵ 右一业务的量是93G,通过sync工具花费3分钟,而如果重做slave要35分钟,速度提升12倍。
引入这组数据意在指明,整个过程不仅解放了DBA的双手,也符合”零运维”的趋势,数据自愈将是互娱DBA团队在未来提供的服务之一。
2. 背景
MySQL数据库基于binlog的数据同步方案,绝大部分情况下能保证主备数据完全一致,但某些异常情况下,例如开发使用了unsafe statement的SQL(如带limit)及硬件故障等,都可能导致主备数据不一致。DBA通过checksum工具可以发现这些不一致的情况,但往往需要重新做一个热备来恢复主备数据一致性,并且此过程可能需要10小时以上,而实际情况上主备数据通常仅有少量不一致,在线修复这些数据差异可以更高效地完成一个数据一致的热备。
3. 收益
重做热备是我们目前首选的修复方案,但有时候没有备用的新机子却让修复步伐戛然而止,而如果没有修复,倘若master故障,由于数据不一致,切换到slave是存在数据丢失的风险,那么又不得不执行修复,DBA就需要评估以前slave上的连接切换到master是否会影响master的性能......这样DBA的工作量就无形中翻倍了。
我们在引言中也道出了sync工具相比传统的热备在时间上的收益,但除了这个,数据自愈服务的收益包括但不限于:
▼ 业务数据更安全,恢复热备时间变短
▼ 减少服务器资源,避免重做热备的机器申请
▼ 提升DBA做热备的处理效率
▼ 降低沟通成本,保证业务持续稳定运行
4. 数据自愈解决方案
我们从开源社区引入了Percona公司的pt-table-sync,该项目从2007年启动。
下面我们对该工具的实现细节以及互娱DBA团队在此基础上进行定制开发的部分内容进行讨论。
⑴ 流程图
Pt-table-sync有2种修复模式:replicate模式和非replicate模式,上图是replicate的,这也是我们所推荐,原因会在下文说明。
这里,我们先对上图作下简要介绍
① 对每一个chunk,再校验时加上for update锁,一旦获得锁,就记录下当前主库的show master status值。以我测试机的案例:
SELECT /*water2.t:1/1*/ 0 AS chunk_num, COUNT(*) AS cnt, COALESCE(LOWER(CONV(BIT_XOR(CAST(CRC32(CONCAT_WS(‘#‘, `id`, `name`, CONCAT(ISNULL(`name`)))) AS UNSIGNED)), 10, 16)), 0) AS crc FROM `water2`.`t` FORCE INDEX (`PRIMARY`) WHERE (1=1) AND ((1=1)) FOR UPDATE |
SHOW MASTER STATUS |
② 在从库上执行select master_pos_wait()函数,等待从库SQL线程执行到show master status得到的位置,以此保证,主从上关于这个chunk的内容均不再改变。
SELECT MASTER_POS_WAIT(‘binlog3306.000014‘, 139672350, 60) |
③ 对这个chunk执行checksum,然后与主库的checksum进行比较
DR: SELECT /*water2.t:1/1*/ 0 AS chunk_num, COUNT(*) AS cnt, COALESCE(LOWER(CONV(BIT_XOR(CAST(CRC32(CONCAT_WS(‘#‘, `id`, `name`, CONCAT(ISNULL(`name`)))) AS UNSIGNED)), 10, 16)), 0) AS crc FROM `water2`.`t` FORCE INDEX (`PRIMARY`) WHERE (1=1) AND ((1=1)) LOCK IN SHARE MODE DB: SELECT /*water2.t:1/1*/ 0 AS chunk_num, COUNT(*) AS cnt, COALESCE(LOWER(CONV(BIT_XOR(CAST(CRC32(CONCAT_WS(‘#‘, `id`, `name`, CONCAT(ISNULL(`name`)))) AS UNSIGNED)), 10, 16)), 0) AS crc FROM `water2`.`t` FORCE INDEX (`PRIMARY`) WHERE (1=1) AND ((1=1)) FOR UPDATE |
④ 如果checksum相同,说明主从数据一致,就继续下一个chunk
⑤ 如果checksum不同,说明该chunk有不一致,深入chunk内部,逐行计算checksum并比较,如果发现某行不一致,则标记下来,继续检测剩余行,直到这个chunk结束。
⑥ 直到修复该chunk所有不一致的行,继续检查和修复下一个chunk
⑵ checksum算法
有2个层次的校验算法,一是块级,一是行级。
① 单行数据checksum值计算
检查表结构并获取每一列的数据类型,把所有数据类型都转化为字符串,然后用concat_ws()函数进行拼接,由此计算出该行的checksum值,checksum默认采用crc32计算。下面是一个例子:
SELECT /*rows in chunk*/ `id`, `name`, CRC32(CONCAT_WS(‘#‘, `id`, `name`, CONCAT(ISNULL(`name`)))) AS __crc FROM `water2`.`t` FORCE INDEX (`PRIMARY`) WHERE (1=1) AND (1=1) ORDER BY `id` FOR UPDATE; |
② 数据块checksum值的计算
智能分析表上的索引,然后把表的数据split成若干个chunk,计算的时候以chunk为单位,可以理解为把chunk内的所有行的数据拼接起来,再计算crc32的值,即得到该chunk的checksum值。下面是一个例子:
SELECT /*water2.t:1/1*/ 0 AS chunk_num, COUNT(*) AS cnt, COALESCE(LOWER(CONV(BIT_XOR(CAST(CRC32(CONCAT_WS(‘#‘, `id`, `name`, CONCAT(ISNULL(`name`)))) AS UNSIGNED)), 10, 16)), 0) AS crc FROM `water2`.`t` FORCE INDEX (`PRIMARY`) WHERE (1=1) AND ((1=1)) FOR UPDATE; |
⑶ 数据精确切分
锁生命周期是我们一直很注重的问题。
2010年底和2011年初,彼时我们刚刚引入了数据在线校验方案不久,在企鹅gamedb每天日常checksum时,DB有锁数据情况,导致TTC大量数据无法写入的告警,mk-table-checksum 1.2.8 版本数据分片方法不合理,当表数据分布非常不均匀时,数据切片会导致某些块包含的数据行过大,其中innodb行锁实现为索引间隙锁,checksum过程会锁住chunk的数据。
felixliang修改mk-table-checksum的源代码,增加自定义recursive_dynamic_calculate_chunks函数,实现了精确数据切片控制,该函数内部调用explain查看每个chunk包含的rows,确保每个chunk中的行数不多于chunk-size参数设置的大小。当每个chunk切分均匀后,chunk数据校验在1秒左右完成,锁数据情况几乎很难感知得到。
这个方案已经在腾讯游戏日常数据校验稳定运行3年多,我们相信该切分算法比官方默认的切分而言更加健壮、同时也更加安全。而pt-table-sync同样会对不一致的表切分,由此我们迁移了该切分算法到pt-table-sync里面,这也是”前人栽树,后人乘凉”的好处。
⑷ 延迟控制
这是为什么我们要采用replicate模式的必要性。
非replicate模式只是普通的线程请求行为,跳过MySQL的Replication机制,本身不做延迟控制,然而,pt-table-sync在修复过程中是不能容忍从库延迟,如果延迟太多,pt-table-sync会长期持有对chunk的for update锁,然后等待从库的master_pos_wait执行完毕或超时,从库延迟越大,等待过程就越长,主库加锁的时间就越长,对线上影响就越大。但是如果不等待,这个工具是无法验证主从数据是否一致。
但是,replicate模式下,补偿SQL是通过master上执行,生成binlog,然后全量同步到slave,再在slave上回放,从而达到数据修复的效果。而数据安全是生命线,改错了master就得回档,就不是闹着玩的。改错了slave不会对玩家有影响,对DBA是个保护。
那么,我们既想要replicate模式带来的好处,又想避免补偿SQL在master执行带来的风险不可控因素,该如何做?
互娱DBA团队通过修改get_change_dbh函数让最后一步生成的补偿SQL不走binlog,直接在slave上跑,从而避免在master上修复带来的数据安全问题。
⑸ 普通索引
pt-table-sync采用replace into来修复主从不一致,必须保证被replace的表有主键或唯一键,否则replace into退化成insert into,而insert是不能在master上执行,因为那样只会使不一致扩散。
对发现主从不一致的行,采用replace into 语句,在主库上执行一遍以生成该行的全量binlog,并同步到从库,这会以主库数据为基础修复从库:
① 对于主库有的行而从库没有的行,采用replace在主库上插入(必须不能是insert)
② 对于从库有的行而主库没有的行,通过在主库执行delete来删除
因为现网环境比较复杂,我们不能保证腾讯全球游戏每个表上都有主键或唯一键。因此,互娱DBA团队通过源码修改,当发现主库上的表没有唯一键时不会致命退出,而是继续执行,但采用了另外一种算法,即在slave上采用delete + replace 方式修补。
⑹ 平台兼容
我们先看mk-table-checksum与pt-table-checksum表结构。
⒈ mk-table-checksum
Ⅱ. Pt-table-checksum
是的,这2个表结构是不一样的,而pt-table-sync的replicate模式是直接读取pt-table-checksum的表,但是我们:
① mk-table-checksum修复版已经集成到我们的GCS平台
② 保留pt-table-sync最新版本的功能,避免引入mk-table-sync老版本bug
基于上述2个理由,我们修改了find_replication_differences函数,让pt-table-sync兼容了mk-table-checksum,这样既能兼容现有平台的功能,又可以用得上pt-table-sync最新版本的新特性。
⑺ 超时控制
在我们测试过程中,发现官方提供的超时控制--wait参数有”bug”
① 对于非replicate模式,wait参数无效
② 对于replicate模式,wait只能是0和非0,当非0时,任何值都是一样的
下面是我们的测试现象
所以,无论哪种模式,wait参数都是没有用的。
我们修改源码来通过外部参数的动态控制超时行为:
5. 小结
数据自愈是数据校验的一种延续与补充,是随着后续业务全量铺开而互娱DBA团队不断定制开发演变的数据修复服务方案。在数据自愈的服务化之路上,相信我们会越来越好。