作者:马进
跑男热播,作为兄弟团忠实粉丝,笔者也是一到周五就如打鸡血乐不思蜀。
看着银幕中一众演员搞怪搞笑的浮夸演技,也时常感慨,这样一部看似简单真情流露的真人秀,必然饱含了许许多多台前幕后工作者的辛苦汗水,如果把一部真人秀比作一个互联网产品,那么在银幕中那些大明星就好比产品开发者:他们需要敏锐地把握观众的需求和口味,与终端用户直接打交道。而灯光,道具,服装,摄影这些就好比系统开发者,他们要尽一切努力满足产品开发者提出的需求,并且暴露给他们最简洁直观的接口,就像跑男一样,把一切复杂实现包装在幕后,留给众演员一个简单开阔的舞台飙戏。
对于数据库来说一样如此,在数据容量日益膨胀的今天,单机数据库的容量,IOPS等使用瓶颈已愈发明显,在这样的背景下,产品开发者往往面临两种选择:第一是采用mongodb,hbase一类的NewSQL系统来突破单机的限制,但这种做法在产品需求不断累加时往往弊大于利,因为任何一款NewSQL或NoSQL产品都无法支持关系型数据库那样多的特性,例如事务,电商场景下必然会用到,例如支持丰富的adhoc查询。为了能够使用关系型数据库的各类特性,又支持良好的扩展性和延展性,架构师们往往会倾向选择第二种方案:分库分表的关系型数据库,将数据分分布到不同的关系型数据库里,以突破单个数据库的各类限制。但是使用分库分表的方案会带来另一些问题,比如应用怎样像使用普通关系型数据库一样使用分库分表的系统?当前的分库分表方案无法满足业务需求时应该如何处理?这些问题就好比真人秀节目中演员们在表演时会遇到的各类障碍,需要我们幕后工作者,也就是系统开发者通过中间件的方案来解决。
网易分布式数据库DDB从06年开始为网易互联网核心产品提供透明的分库分表服务,10年风雨,可谓见证了各大产品兴衰荣辱,在网易核心互联网应用中基本都能看到DDB的身影。DDB的核心价值充分体现了系统开发者作为幕后工作人员的良苦用心:应用开发者在使用DDB时,基本上像使用mysql一样简单。篇幅所限,DDB的内容无法展开讨论,因为本文的重点是另外一套和DDB息息相关,同样展现了幕后工作者核心价值的中间件系统:数据迁移工具Hamal。
Hamal是一套基于MySQL binlog实现的数据迁移、数据同步以及数据分发系统。DDB的存在为我们很好地解决了应用在使用分库分表时的透明性问题,但是却无法解决数据分布扩展后带来的数据迁移问题,这里就是Hamal大展拳脚之地了,另外,由于Hamal拥有一套可以扩展的接口,它也可以在一定程度上解决数据同步和数据分发的问题。
我们首先来看看数据迁移,数据同步和数据分发这三种应用场景。
数据迁移
数据迁移的需求是我们团队开发Hamal引擎的初衷,在DDB漫长的技术支持中,曾经遇到很多数据迁移需求,这种需求可以归为两大类:
库的扩容缩容
数据库的扩容缩容是分库分表下一种非常典型的数据迁移场景,例如在DDB最早的应用产品博客项目中,出现过20个库扩40个扩的案例。在hamal工具出现以前,应对这种扩容场景的解决方案是依赖MySQL原生的复制机制,具体过程非常复杂,因为增量数据的迁移中包含脏数据,在做切换后需要一段删除增量脏数据的时间,因此实际的切换需要进行两次。基本步骤为:
以一扩二的场景为例,要扩容的原库为d1:
a) 全量复制:通过MySQLDump或者innobackup对要扩容的库d1进行全量备份和恢复,恢复出来的库为r1, r2
b) 删除全量脏数据:将r1,r2中不应该属于自己的全量数据删除,1扩2的场景下,这个过程会删掉r1,r2两个库累加起来后的冗余数据
c) 增量复制:将r1,r2分别作为d1的slave进行增量数据同步,注意同步点要以备份时的binlog位置为准
d) 增量追上切库,改写SQL:这个步骤其实涉及到三件事情,首先要等r1和r2作为slave追上d1的binlog位置,然后在DDB的中间件层将数据分布表从d1改写为r1和r2,由于数据分布表已经发生变化,新的查询和更新都会在r1,r2上进行,但是r1,r2上增量数据是“不干净的”,因此需要在下发的所有查询中增加对脏数据的过滤条件。在以前的DDB版本中,会为每个未来可能需要迁移的表增加一个默认的bucketNo字段,每个表有多少个bucketNo在建表之初必须指定,并且未来不能更改,DDB所有表的分区字段经过hash后会对应到bucketNo上,再由bucketNo与实际的数据库节点对应,即所谓的均衡策略。在数据分布表由d1切为r1,r2后,需要在所有查询后增加bucketNo not in(other bucketNos in d1)的条件,例如对下发给r1的查询后必须增加where bucket No not in (other bucketNos in r2)。这三个过程严丝合缝,由DDB内部完成,应用毫无感知。
e) 删除增量脏数据:在完成数据分布表切换后,DDB需啊启动一个删除增量脏数据的过程,删除的方法与第二个步骤类似。
f) 撤销SQL改写:在完成删除增量脏数据的过程后,不再需要DDB层为所有查询增加过滤脏数据的条件,此后撤销所有查询SQL的改写。
以上6步构成了老版本DDB的数据迁移方案,这个解决方案优势是对外部依赖少,整个过程除了改写SQL外,只需要一个额外的删除脏数据的进程,全量复制和增量复制都是通过MySQL自带的方案解决,但是缺陷也是非常明确的:
- 需要依赖bucketNo这个字段,事实上这个字段仅仅为了数据迁移这个应用场景而生,在很多不会有数据迁移的表中初始并不会有这个字段
- 改写下发SQL和删除增量脏数据的过程发生在数据分布表切换后,会对线上业务产生一定的性能影响
- 这个方案仅对1扩2,2扩3规则适用,对一些比价简单的扩容场景比如9扩10,由于在改写SQL环节实现会很麻烦,并不支持
- 整个过程复杂且慢,需要DBA对原理熟悉方能操作
老的数据迁移方案的实施过程较为繁琐,虽然我们在很大程度上实现了自动化,但是一旦出问题处理起来会比较麻烦,例如在切换在引入Hamal数据迁移工具后,一切的困难迎刃而解。我们来看看同样的数据迁移需求,H使用Hamal需要经历怎样的过程:
a) 全量复制:Hamal的全量复制方案有两种,第一种是使用MySQLDump或Mydump对原数据库进行逻辑备份,再由isql或DDB的DBI模块进行数据导入,由于全量数据通过isql或DBI导入后数据会经过数据分布表重新分布,不会产生脏数据,也就省去了之后的删除全量脏数据的过程。第二种全量复制方案是在Hamal engine内部启动一个全量复制线程定批对原库数据进行数据拷贝,这个过程通过对数据批量加锁可以与增量复制并发进行,这个过程与percona的在线修改表结构方案中的全量复制非常类似。无论是哪种方案,由于都经过数据重分布,不会向新库引入脏数据。
b) 增量复制:Hamal的增量复制方案是使用Hamal engine模拟MySQL slave向原库拉取binlog,经过自身模块的解析生成相应的数据迁移SQL,再逐条通过DBI模块apply到新库中,这样同样可以在增量复制的同时避免引入脏数据
c) 增量追上切库:由于增量复制也不会引入脏数据,也就避免了需要删除增量脏数据和改写SQL,在发觉binlog位置追上后经过短暂的数据分布表切换即可完成数据迁移过程。
基于Hamal的DDB数据迁移方案与老方案相比,步骤节省了一半,整个过程理解起来也比较容易,最重要的是它完全解决老方案的4点缺陷:迁移过程无需依赖bucketNo字段;因为没有改写SQL和删除增量脏数据,也不会对线上应用产生任何不利影响;由于全量和增量复制过程原理都是经过DDB的数据分布表来完成,且没有SQL改写,也就没有了对1扩2,2扩3规则的限制;整个流程理解和操作起来门槛降低很多。
另外,Hamal engine基于一套有向无环图算法实现了快速的并行复制,它的增量复制过程本身就比MySQL原生的复制过程快很多,但是使用Hamal的并行复制算法具有两个限制,一是必须基于row格式binlog,二是所有迁移表必须含有主键或唯一性索引。这两个限制是由算法原理本身决定的,原理会在下文展开。首先row格式binlog本身在使用上比Statement格式更加严谨,目前网易RDS中所有MySQL binlog都是基于row格式的,至于每张表必须含有主键或唯一性索引的要求也正是DBA对产品开发者耳提面命的要求,因此这两个限制可以忽略不计。
表的数据重分布
表的数据重分布与扩容缩容本质上是一样的,在应用场景上体现为表更换分区字段,或更换均衡策略。以更换分区字段为例,更换分区字段后,所有数据都要重新经过新的数据分布表来进行分布。表的数据重分布在需求上其实更为常见,比如一个项目初期选择分区字段上没考虑清楚,导致数据增长后数据分布不均匀,就要考虑更换一个更加均衡的分区字段,又如业务想对个别表做存储隔离,就需要把这个表从当前共享均衡策略中迁移到一个隔离的均衡策略里。
在DDB老的数据迁移方案中,并没有针对表的迁移方案,在实际操作过程中,一般DBA会根据场景的不同自己设计方案,多多少少都会要求产品方容忍一定的停服时间,而在引入Hamal的解决方案后,表的数据重分布也迎刃而解。
因为Hamal的全量复制和增量复制都是由第三方工具完成,可以高度定制化。在Hamal解决方案中,表的数据迁移与库的扩容缩容并没有本质区别,只是在全量复制和增量复制时过滤数据的方式不同。
数据同步
与DDB的数据迁移场景相比,数据库之间的数据同步需求更加常见,由于MySQL自带的replication原生复制方案,尤其最近几个版本的并行复制效果尚可,大家往往会倾向使用MySQL自带的数据同步功能。但是MySQL基于replication的数据同步方案有三个限制:
- MySQL原生复制只能做到点到点,无法点到组
- MySQL原生复制依赖MySQL底层,上层无法干预,同步方式无法定制
- 虽然新版本MySQL并行复制速度尚可,但在跨机房同步等场景中依然不够可观
在使用DDB的业务场景中,常常需要用到点到组的数据同步,最经典的莫过于“好友关系问题”和“买家卖家问题”。以易信的好友关系为例,好友表有两个字段,分别为userId和friendId,业务的查询需求可以分为以下两类:
- 查询我有哪些好友
- 查询我是哪些人的好友
“查询我的好友”场景比较多,会占主要部分,“查询我是哪些人的好友”用于判断评论可见性和好友推荐等场景,请求量也不小,在这种情况下,选择userId还是friendId作为分区字段成为一个难题,无论选择哪个字段都会顾此失彼。在这种状况下,我们的推荐做法是使用冗余表,假设主表为friendship,主键和分区字段为userId,为friendship建冗余表friendship_reverse,主键和分区字段为friendId,这样“查询我的好友”时走表friendship,“查询我是哪些人的好友”时走表friendship_reverse。如下图:
为了保证业务的正确性,需要确保friendship表和friendship_reverse表数据严格一致,如果在业务层做事务性双写,由于两表分区字段不同,双写会成为一个分布式事务,目前任何基于两阶段协议实现的分布式事务都无法做到单机事务保证严格的ACID,因此并不建议在采用双写的方式解决冗余问题。
推荐的做法是通过第三方的数据同步中间件来保证冗余表friendship_reverse的数据一致性,对于MySQL原生复制,由于分区字段不同,由一个源数据库产生的数据可能要同步到不同的终点数据库中,即所谓的“点到组”,另外,同步的两个表名本身也并不相同,在这里MySQL的原生复制功能无法定制的功能体现无疑。
Hamal engine可以很好地解决这类数据同步问题,我们需要做的是部署与源数据库实例数量相同的Hamal engine进程,根据同步条件稍作配置。每个hamal engine会以一个slave的身份向一个源数据库拉取binlog,根据同步条件过滤binlog事件,再根据apply配置生成相应SQL应用到目标数据库中,由于friendship_reverse在DDB中有已经定义好的数据分布表,hamal在apply过程中并不需要知道目的节点信息,只要通过DDB中间件去apply即可完成透明的数据路由,这也是Hamal和DDB配合使用的妙处之一。
除了业务上的冗余表外,跨机房的数据同步也是Hamal可以大展拳脚的应用场景之一,在2015年的中国数据库技术大会上,阿里巴巴集团第一次向外界分享了基于DRC的异步多活的多机房数据同步解决方案,而DRC的核心就是一个个旁路的拉取binlog并进行定制化跨机房传输并apply binlog的进程。在这方面hamal与DRC的设计异曲同工,未来可以基于hamal来定制我们自己的跨机房数据同步方案。
数据分发
数据分发是一个较为偏向业务的应用场景,例如通过数据分发更新缓存,创建更新索引等。由Hamal engine拉取源数据库的binlog,根据配置生成相应的下游任务,这个下游任务可以是向消息队列里插入一条数据,也可以直接更新缓存,调用RPC等。阿里类似的“精卫”系统已在线上为淘宝各种业务线提供了几年的稳定服务。
Hamal特性与原理简析
在介绍DDB数据迁移场景中,对Hamal工作原理已做过简要介绍,Hamal分为Hamal engine和Hamal admin两大部件,Hamal engine是真正负责数据迁移,数据分发和数据同步的进程,而hamal admin是负责管理hamal engine,调度任务的外部组件集合。目前Hamal engine已经开发完成,并且开始在实际的数据迁移场景中发挥作用。Hamal engine具有的特性如下:
- 支持行级和事务级并行复制,追赶binlog速度远快于MySQL原生复制
- 支持断点续传,在源端或目的端出现问题后,可以暂停任务,修复问题后恢复任务
- 保证数据一致性,其中行级事务并行复制适用于迁移场景,只保证最终一致性,在牺牲完全实时一致性的同时满足了高速的需求
Hamal engine的特性在于快和幂等,快保证了hamal在数据迁移场景下很好地缩短任务周期,也有益于数据同步场景下的同步实时性,幂等则保证了hamal engine只需要定时记录一个事务记录点,即可在不破坏数据一致性的前提下重启任务。
hamal实现幂等的核心思想是使用replace代替insert和update,保证数据最终的值一定会生效在目标数据库里。使用replace的前提是要保证同一份数据的操作必须排队执行。
同时,为了保证数据的正确性,Hamal engine也必须保证即便是在并行复制下,同一份数据的apply也必须按照在binlog位置中的顺序来执行,这个过程类似于MySQL中的“行锁”,只不过Hamal engine中没有保存真实的数据结构,也难以实现一套类似于MySQL的完整锁体系,Hamal engine内部的“行锁”机制是通过一个有向无环图算法,为一份数据在binlog不同位置上的修改建议依赖关系,可以理解为同一行上的修改通过我们的有向无环图算法形成了一个执行队列,这个队列中只有上一个节点执行完成才可能触发下一个节点的执行。
Hamal engine的核心组件如下所示:
其中,Extractor模块通过MySQL协议向源端MySQL拉取binlog,并存储到本地盘或云硬盘的relaylog中,这方面与MySQL原生复制中的IO thread完全一致。Poller是一个独立线程,会随时读取relaylog中最新的二进制数据,并转换为Hamal可以处理的Event,根据配置的不同,一个event可以代表一行数据的更改,也可以代表一个事务。一个event在被回放之前首先会进入一个叫做EventGraph的数据结构中计算与当前系统中所有event的依赖关系,对同一份数据,如果发现有先后更改的冲突,EventGraph会按照依赖关系将后面的更改先存储起来,当之前的更改回放完成后,再触发之后的更改。如果一个event不再依赖系统中任意其他event,这个event将会被丢入SlaveQueue,并最终被某个applier线程回放掉。
对于一张表,表上所有唯一性索引,包括主键都会对应一个EventGraph,因为只有通过唯一性冲突才能判断数据之间是否存在依赖。对于唯一健值更改的情况,还需要将更改分裂为delete和replace,并且分别将delete和replace的键值拿出来计算依赖。
总结
大数据时代,数据往往代表了一个互联网企业的核心价值,怎样去玩数据也是成为各个企业重点关注的问题。在这个背景下,类似于Hamal这种基于MySQL的数据迁移,数据同步和数据分发解决方案可以发挥不俗的价值,同样类型的系统,在阿里就有好几套。
我们对Hamal的期望,是以hamal engine实现一个核心架构和一套简洁的接口,在为DDB数据迁移,MySQL数据同步,分发等场景分别创建不同的插件实现。最后通过Hamal admin实现各种解决方案的串联和自动化,因为Hamal是一个独立于MySQL的中间件系统,我们可以在上面充分发挥"第三方"的优势,目前高速的并行复制和断点续传都是Hamal相比于其他数据复制方案的优势。未来,我们也可以在Hamal上定制更多的高可用功能,从而将它升级为一个多机房的数据同步解决方案。
本文来自网易实践者社区,经作者马进授权发布