达达双云双活实践

达达双云双活实践

文章来源:infoq杨森

引言

过去6年,达达集团秉承“万千好物 即时可得”的初心和愿景,不断迭代和升级技术能力,持续提升履约效率和服务体验。为保障系统持续稳定和业务不间断地高效运行, 我们在数据库高可用架构升级、数据库垂直/水平拆分、 微服务治理及可观测性、容量弹性和多活容灾等方面进行了不断实践并取得一定成果。

本文主要分享达达在双云双活容灾能力建议方面的实践和经验。

为什么做双活?

首先,介绍一下高可用(High Availability)和容灾(Disaster Recovery),两者相互联系、相互补充,但也有明显区别:

  • 从故障角度看,高可用主要处理单组件故障导致的负载在集群内服务器之间做切换,容灾则是应对大规模故障导致的负载在数据中心之间做切换。
  • 从网络角度看,局域网是高可用的范畴,广域网是容灾的范畴。
  • 从云的角度看,高可用是一个云环境内保持业务连续性的机制,容灾是多个云环境间保持业务连续性的机制。
  • 从目标角度看,高可用主要是保证业务高可用,而容灾则是在保证数据可靠的基础上,业务对外连续可用,以尽可能降低RPO(灾难发生时丢失的数据量)和RTO(灾难发生时系统恢复时长)。

由此,我们可以看出,高可用往往是指本地的高可用系统或架构,表示在任一服务器出现故障时,应用程序或系统能迅速切换到其他服务器上运行的能力;而容灾是指异地(同城或者异地)的冷备/热备/双活系统,表示在大规模集群或云级别灾难发生时,数据、应用和业务在灾备数据中心的恢复能力。容灾通常可以从网络接入、应用治理、主机、数据存储等层面去实施,业界如大型银行的“两地三中心”同城双活+异地灾备、蚂蚁金服的“三地五中心”异地多活、饿了么的北京上海双机房“异地双活”都是典型的容灾架构方案。

从支付宝527大规模宕机事故、携程528数据库全线崩溃、AWS北京光缆被挖断、腾讯云硬盘故障、谷歌云K8s服务(GKE)宕机19小时不可用、微盟删库事件,再到近期的华为云香港机房因制冷设备故障导致整个机房崩溃,无论是天灾,还是人祸,居安思危并建立容灾能力对一个企业的重要性是不言而喻的。

达达在2017-2018年饱受网络故障之苦,如云网关组件故障、NAT网关故障、单运营商线路网络连接故障等,这些问题往往让我们束手无策。为规避单云灾难级风险,我们在调研并比较业界容灾方案后,最终选择双云架构做同城双活的探索。

双云双活一期

双活一期主要是围绕服务双云部署、业务分流和接口功能/性能等方面的探索验证。

双云双活一期架构图

双云双活一期方案细节

双云双活一期方案主要有以下几个方面:

  • 跨云专线:双云间通过2家供应商建立了4根高可用的跨云专线,带宽4Gb,时延3-4ms
  • 服务注册:达达重度使用Consul,并基于Consul 1.1.0版本二次开发,实现了服务注册发现、链路隔离、数据源发现和高可用切换等高级功能。Consul采用一致性算法Raft来保证服务列表数据在数据中心中各Server下的强一致性,这样能保证同一个数据中心,不论哪一台Server宕机,请求从其他Server中同样也能获取最新的服务列表数据。数据强一致性带来的副作用是当数据在同步或Server在选举Leader过程中,会出现集群不可用(在接下来遇到问题中会分享我们遇到的问题)。

在双云双活一期中,双云共用原有生产Consul集群,J云的服务节点处于单独拉取的链路而非base基础链路,确保部署在J云的核心服务间调用在J云内部闭环。

  • 配置中心:双云共用原有生产配置中心Config,通过Config获取服务相关参数、缓存和数据库的连接地址。
  • 服务部署:J云只部署了核心服务,并注册到原有Consul集群,而配置中心、缓存、队列、数据库等均跨云访问U云侧原有集群。
  • 流量分发:达达的负载均衡LB是基于OpenResty+Consul建设的,并结合自身业务特点自研了一套流量控制逻辑,具备生产流量、灰度流量、压测流量的转发控制。在双活一期针对J云类似灰度链路的流量控制,根据J云机器的tag标签,可以做到指定域名根据CityId实现可调百分比的外网流量经由跨云专线打到J云的服务节点之上。
  • 监控告警、日志系统、应用性能、发布系统等均共用原有U云生产环境。

双云双活一期遇到的问题

在双云双活一期架构中,J云侧核心服务的订运单主流程功能验证通过,但也遇到棘手问题。

1.接口响应时间过长

由于只有部分核心服务部署在J云,其依赖的Redis、DB、队列和其他服务等仍需要跨云读写访问,跨云专线的3-4ms时延在多次跨机房请求下将延迟问题彻底放大,例如U云侧一个发单接口响应时间约在200ms,而在J云侧需要500ms+,这个响应时间对业务而言是无法接受的。

2.跨云共用同一Consul集群有偶发风险

跨云专线的网络波动和3-4ms时延,这会造成跨云使用LAN Gossip协议通信的同一个Consul集群发生紊乱情况。我们曾遇到过J云侧Consul节点将U云侧正常节点投票下线 继而影响U云生产集群的问题,也遇到过这种紊乱情况下,J云的节点会疯狂地消息广播投票确认并导致专线带宽急剧上升。基础架构团队针对这些问题专门优化了Consul Gossip协议以降低对网络延迟的敏感度。

3.灰度用户前后请求体验有明显差异

双云双活一期的流量分发方案可指定到域名+按百分比分发流量+默认负载均衡轮训后端,这样虽然可以灰度验证业务功能,但开启双活流量城市的骑士前后两次请求如果流向不同云,接口响应时延差别较大,相应体验上会有较大差异。

双云双活一期总结

实际结果证明,按以上所描述的一期方案来实施的双云双活,没有办法达到预期结果,特别是比较大地影响了用户体验,更无法支持诸如抢单这类实时性较高的场景。达达的双活一期最终也以失败告终。

双云双活二期

针对同城双活一期遇到的跨机房调用时延问题、跨云网络波动可能引发Consul集群紊乱,以及业务流量精准分发一致性体验等问题,我们进行了深度思考,形成了服务间交互单云内内敛、数据库双云双向复制、流量总控精细化配置三大核心点,并且结合业务场景依据数据访问频次、时延要求和数据一致性要求等维度,我们对数据和关联服务进行了梳理和RCG模型分级。

此外,我们还在双云统一配置中心、服务一致性部署和发布、系统可观测性、工具系统主备、容量规划和成本控制方面做了相应功能迭代和能力建设。

双云双活二期架构图

双云双活二期的架构图如下所示

双云双活二期方案的三大核心点

1.Consul多DC方案

双活一期遇到的跨机房调用时延问题及跨云网络波动可能引发Consul集群紊乱,这让我们更清楚地认识到服务间请求单云内内敛的重要性,对 Consul LAN Gossip与WAN Gossip协议适用不同场景的理解。

因此,双活二期,我们采用了Consul多数据中心方案,官方示意图如下:

每个云有各自的Consul Server集群,consul client默认以LAN Gossip协议join到所在云的Consul Server,不同云之间Consul Server通过WAN Gossip协议join到一起。在达达内部,每个服务会默认携带一个Sidecar并join到所在云的Consul集群, Mysql和Redis也同样以consul client方式join到所在云的Consul集群,这样通过Consul多DC架构和Consul服务发现,我们实现了服务间交互、服务与绝大多数数据源间交互在单云内内敛,也尽可能规避了一期遇到的问题。

2.数据库双云双向复制

为实现RZone模型的DB本地读写,且在数据库水平项目没有大规模开展的情况下,我们调整了双云两侧数据库的自增步长为2,即auto_increment_increment=2,主键也做奇偶数区分,即U云侧主键是奇数,J云侧主键是偶数。

为实现双A数据库双云双向稳定同步,我们采用了阿里开源的分布式数据库同步系统Otter,其架构图如下:

生产环境中,我们通过Otter不同的Channel管理不同双A数据库的双向同步

如下是生产环境Otter同步列表的TOPN

目前,数据库双向复制时延平均0.9s,最大时延为2.2s(瞬间)

3.流量精准分发

我们利用OpenResty根据请求头/体中/CityId/TransporterID/ShopID及域名+URI以精准分流。

如下图,相比一期我们可以做到,针对pop.imdada.cn的某两个具体的和订单相关的URI,对城市ID 313和146 流量分发到J云,其余城市或pop其他接口默认分发到U云(默认主IDC)。

同时,我们也做到骑士ID对齐城市ID,即如果某个骑士属于这个城市,而且这个城市针对该骑士对应的业务也开启了双活流量,那么该骑士的每次请求都会走到J云。

双云双活二期方案之RCG模型分级

参考业务双活解决方案,并结合业务场景依据数据访问频次、时延要求和数据一致性要求等纬度,我们对数据和关联服务进行了梳理和RCG模型分级,如下

双云双活二期之工具/系统适配双活

主要有以下几个方面的工具系统改造支持并适配双活

  • 配置中心:配置中心从Config迁移至Apollo,涉及达达数百个服务,生产环境的Namespace下,通过不同集群Cluster区分U云和J云的个性化配置, 并通过服务本地配置文件server.porperties对齐 apollo的cluster名字和consul 的datacenter名字以取得正确的配置信息。同时,Apollo本身依赖的几个DB也在双云间做了数据库主从同步,以备灾难时切换。
  • 发布系统:得益于配置中心和中间件包版本的统一,我们实现了服务一次打包双云一致发布,这里包含发布系统对双云两侧同一服务版本的一致、发布/回滚/重启等变更批次的一致、变更逻辑步骤及消息通知的一致性。
  • 业务监控:业务监控系统为了更好的支持双云服务监控,对齐Consul集群datacenter名,业务只需选择对应dc的tag即可切换到具体某个云上查看服务的监控情况。
  • 应用性能监控:为了更好的监控服务间在单云内敛的交互情况,我们在双云各自部署了一套Pinpoint系统。
  • NTP时间同步服务:双云间时间步调不统一会直接影响业务问题的排除,而且在流量切回瞬间同一个未完成状态的订单可能会发生意想不到的情况。我们在双云间建立了NTP服务主备,双云所有服务器默认与U云的NTP Server每分钟同步时间。
  • 镜像仓库:目前Harbor在双云各部署一套,默认是从U云增量同步到J云,灾难时会切换J云的Harbor为主镜像仓库。

双云双活二期之容量弹性和成本控制

通常建立双活会使云资源成本接近等量的增加,我们也面临同样的困难,如何做好成本控制,我们的答案是从容器规划和弹性扩缩容方面下功夫。

  • 数据库:几乎等量,但在调研从一主多+冷备集群往MGR方案迁移
  • 无状态服务:引入原生容器,即时即用,在效率和成本上得到有效控制

在业务流量总量一定的情况下,流量在双云间调配,意味了双云无状态服务的负载也在随之变化。鉴于此,我们基于K8s HPA算法自研了Auto Scaling自动扩缩容系统支持双活架构下的容量弹性,通过实时收集业务系统的多种metrics指标,实现了双云依据各自业务负载实时调节所需资源。

这样,我们在保障业务系统双云稳定性的同时,又做到了成本有效节约。

双云双活二期方案与业界双活方案对比

双云双活现状

目前落地配业务已稳定运行在双云之上,并且达达也具备了以城市为粒度任意在指定云间切换业务处理量的能力。不仅具有了抗IDC级别故障的能力,也达到了按业务和地域灵活灰度切换的预期。

未来规划

未来需要在业务入层增加更多的Sharding Key(城市ID/骑士ID/商家ID/订运单ID等),引入API Router以便于其他业务更好地在双活架构下流量分发到准确的云或机房;兜底Job改造为Pulsar延迟消息并在各个云内执行,以避免因数据库双向同步时延且Job单边运行造成业务上数据的错乱;业务监控特别是单量的监控水位,需要能及时对齐流量切换比例;账户服务目前是默认在U云的全局服务,拟采用TiDB方案以适配双活架构;最后,双活需要对齐数据库水平拆分,流量切换从CityID维度变化为ShardingID维度便于流量调配和管理。

总结

回顾双云双活两期,我们希望尽可能借助架构和运维的方式来解决同城双活中遇到的问题,尽量对业务透明,减少业务改造成本,同时以落地配业务作为同城双云双活架构的业务试点,以减少全局风险。同时,我们希望积累相关经验,为后续开展单元化和多云多活打好基础。双云双活的建设是一项复杂的系统工程,需要有点、线、面、体的维度思考和前瞻规划。在这里特别感谢云平台各团队、落地配研发团队、物流平台团队)、落地配运营团队对双活项目的大力支持。

摘要: 异地多活(双活)技术一直是行业内一个比较大的技术挑战,当中要突破诸多的技术难点,很多公司都做过类似的尝试,但是往往止步于灾备阶段难以向前;饿了么双活项目通过前期丰富的调研,真正启动改造实施只有短短三个月就完成了上线,并且一次性上线成功,当中也遇到了不少的问题踩过很多坑,尤其是在数据库这一块的问题会比较多,因为在多活设计中大家最担心的点是怎么保证数据在多个机房实时同步、如何才能保障数据不被写坏和怎么兜底保障数据的一致性,这些点对数据库方面的挑战很大(极容易出现重大事故),所以本次分享会对这些难点做一个全面介绍,内容包括饿了么多活中数据库的架构、改造、难点、收益与展望等,重点会介绍数据库为多活所做的改造、多活过程中和上线后DBA所面临的挑战和我们是怎么克服这些困难的,期望能为大家揭开多活技术的在数据库这层面纱,为大家进行类似技术方面的改造提供参考。

分享大纲:

1、多活难点&设计原则

2、多活架构&切换

3、数据库改造&挑战

4、收益&展望

正文:

1、多活难点&设计原则

从多活落地后回过头来看,多活的难点还是有很多的。

首先,同城Or异地的问题。如果是做同城多活,和异地比成本投入会少很多,起码光纤距离和带宽费用就会少很多。另外,异地多活实现起来会更复杂,涉及跨网络的延时,需要有更周密的方案,而同城访问一般不需要做太多的改造,相当于是同机房的调用。异地多活的劣势是改造大、成本高,但是与同城相比,存在天然优势,最直观直观来看我们的扩容就不会受地域的限制,而同城机房就不能解决这个问题。

异地多活除了成本高外,异常情况下的数据处理方案,如数据出现错乱,冲突,环路,或者一致性问题等,都需要重点考虑。

总的来看,异地多活的难点其实主要有三个,第一,如何做好分流和控制;第二,如何解决跨机房延时带来的问题(访问&数据);第三, 如何解决数据安全性。

基于上述这些问题,我们抽取了一些设计原则:

1)、业务内聚。跨机房自然会存在延时,但我们希望一笔交易能在同一个机房完成,减少跨机房的调用; 即一个订单的生命周期在一个机房中完成,这样可以避免跨机房延迟带来的影响。

2)、可用性优先。绝大部分互联网公司都是基于这个原则(base),优先保证系统可用,对饿了么来讲就是先让用户可以下单吃饭,容忍暂时数据不一致,事后修复。

3)、保证正确性。在确保可用的情况下,需要对数据做保护以避免错误。

4)、业务可感。业务团队修改逻辑,能够识别出业务单元的边界,只处理本单元的数据,打造强大的业务状态机,在出现异常时能发现和纠正错误。

除此外,还有设计分区原则。我们在云端部署了一个智能DNS,并且是在多个云端,根据用户所在的POI位置,来完成用户的分流。相当于是对机房的一个映射,把全国分成很多区,机房和分区是一对多的关系,如果用户从某个位置进来,就会对应到某个逻辑分区,分区最终会路由到对应的物理机房,完成基于用户位置的分流。

2、多活架构&切换

那么,多活的实际架构是怎样的呢?

我们会在云端部署智能DNS,完成用户的分流,用户通过DNS进来就决定它的流量会落到哪个机房,他的整笔交易基本上是在同一个机房来完成的。所以,用户的使用不会受到跨机房的影响。另外,我们在底层也有相应兜底的架构应对方案,防止因前段路由异常情况下造成数据的错乱。

这是我们在做数据同步时很重要的一个组件,叫做DRC。主要任务是在底层完成两个机房之间的数据同步。设计原则是在每个机房部署相应的通道,一端接受的是数据变更,然后把变更同步到另一端机房,完成数据双向同步。

在DB这端,我们看到最前面的架构图有两个。一个是ShardingZone,另外一个是GlobalZone。ShardingZone的主要特点是,适用于业务能按区域维度进行切分的应用,实现真正的多活, 而且读写都在本地机房进行;这个架构正常情况下,只需要本地机房的数据,对其他机房数据无依赖,所以跨机房数据延时是无影响的,我们设计时只需要考虑避免数据同步冲突(DRC冲突处理、自增值DB控制)。GlobalZone架构适用于特殊场景,比如需要对数据有强一致性的要求或者没有分区标签,它的写是在同一个机房,但读是在本地机房完成,这样在读的时候会有数据延时,所以我们要按照业务分,有些业务能接受一定程度的延时,他才会选用这种数据完全一致的架构。

具体DB的切换动作是怎么操作的呢?

其实绝大部分情况DB都是不需要做切换动作的,因为只有GlobalZone的写入需要变动的时候才是需要DB切换的,其他情况的流量调整和切换只需要GZS控制前端做路由调整就行(DB不需要跟着切)。DB在切换的时候,会有锁、等待动作,是为了保障数据完成同步。当然,等待也有时间限制,如果超时也会强切。

看下大概过程,比如我们把1机房的DB访问切换到2机房,会先发出一个BLOCK的操作,把1机房的DB写入先锁住,等后面的数据同步和验证操作完成后,GZS会控制其他组件把1机房的访问流量改到到机房2,DAL会完成DB写入的机房间切换。

3、数据库改造&挑战

在做多机房最先要做的就是要全量同步数据,包括测试环境、生产环境数据全量同步等问题。第二DRC为了解决数据冲突问题,需要增加毫秒级别的时间戳,但是我们在数据库设计阶段,并不会有这样的字段,所以要做很多更新。第三做多活后很多自增列也会调整,防止溢出。第四多活有不同的架构类型,所以不同类型的DB需要迁移,将同类型的DB拆分到对应的架构里,不同类型的拆开。还有原生复制改成DRC复制、账号网段调整、各个集群参数一致性调整、按集群类型调整HA配置等等动作。总的来说,首先要做数据搬移,然后适配多活的体系改造,还有个各种数据库的配置也需要调整。做完多活以后,实例、集群、Proxy、HA、数据量、DDL、机器故障等几乎都是翻倍状态。

为了应对数据库改造问题,DBA做了哪些基础工作呢?

首先,要有检测数据是不是一致性的最终方案。当然,我们在前端、后端有一些相应的保护。比如在路由这层,会有几个路由来判断订单是不是正确地进入了对应的机房,DAL这层也会判断这笔交易是不是符合路由规则,不正确的话会直接拒绝。但是还是会出现一些问题,比如:软件上的BUG,或者是没考虑到的问题和设计之外的异常场景,会导致数据穿透到底层,造成多个机房数据不一致,所以我们要做兜底的最终数据是否一致的校验。我们开发了DCP数据校验平台,能完成分钟级别的数据校验, DCP不只完成全量、增量、延时校验、手动校验,还有数据结构、多维校验,还要考虑各种延迟、并发、校验时长等情况;最重要的是要有一套比较方便的修复数据的方式或者配套工具,因为你找到不一致数据后,工具如果不能直观告诉你怎么去修复,又会是一个大问题,而且数据还会一直在变,可能会造成其他的影响。

其次,会涉及到数据的迁移、会有大表拆小表这种动作。所以我们研发了D-Bus这套工具,它可以完成DB&Table迁移 、增量和实时同步(包括暂停、断点续传)、单表和Sharding表数据互转、数据校验等工作。

第三,在系统切换的时候,HA如何与其他系统完成对接,这也是一个重要问题。我们做了一套自动化系统,叫做EMHA 。在HA切换的时候,可以和其他组件完成互动,进行配置、切换、联动(DAL、DRC)。

第四,DDL翻倍的问题。比如100G的一个表,我们如果通过DRC把变更数据传递到另外一个机房,而且是在跨地域网络的情况下,网络可能会爆掉。所以,在DRC这层实际上是不方便来做DDL操作的传递的,DRC要把这个动作滤掉。我们DDL操作类型比较多,有原生能支持Online的DDL直接分发,还有PT的工具,还有自己研发的mm-ost的工具等。

多活场景下DDL具体要考虑什么问题呢?首先是控制,DDL的时候空间&延时&锁&定时&低峰期&风险&预估时长等要得到有效的控制;另外,我们的类型比较复杂,包括:非多活 、ShardingZone、GlobalZone、多推、 Sharding(分开分表)等业务,要控制好DDL的同步,同时要保障所有的表都达到一致的状态;还有多机房的问题,我们通过Mm-ost,保证多机房一致性问题,同时保证跨机房延时在3-5s之间。

之前DDL很大一部分工作是由DBA来做(研发提供单DBA负责处理),现在已经不需要DBA做太多的事情了,由研发自助发布,自助发布的比例超过90%以上,极少数情况下才需要DBA来干预,还有一部分是系统自动执行(不要研发,也不需要DBA来做)。

4、收益&展望

很多人可能会有疑问,多活花费这么大的成本,是否值得?

首先,看下多活的收益。第一,打破了单机房(地域)容量瓶颈,当主机房不能再放机器,而且系统容量已经到上线时,你可以把流量引流到其他机房,让多个机房能承载容量。第二,你的故障不受单机房(地域)故障影响,在做多活之前,其实我们有主力机房核心交换机出现故障的情况,当时没有多活的只能厂商来解决,核心的交换机宕机了三个小时,影响非常大,但是我们却束手无策,而且我们的业务特点有两个尖峰,尤其在中午尖峰的时候挂了,损失会很大。第三,动态调整各机房流量,如果有机房资源紧张,可做动态调配,或者哪个地方访问不均衡,也可以做动态调整。第四,Online维护(通过GZS、DAL、DRC、D-Bus、DCP这些组件的配合),有多活之后如果你想做哪些升级,可以先把流量切到一个机房,在没有流量的机房完成各种动作,都没问题,所以我们现在主要业务基本没有停机维护之说。

针对企业未来发展,我们也有一些相应的计划。首先是,想做多个机房 。现在很多企业都在计划上云,我们也希望在云上做一个多活的Zone,去分担一部分流量,而且云上还可以动态调整资源。其次是,Data-Sharding,现在做的还是全量数据,上层流量访问是分流量的,底层我们也在考虑对数据分流。其三是, 自动动态扩缩容,这也是我们想上云的原因,考虑在业务高峰的时候多添加一些资源,低峰的时候释放掉这些资源,合理控制成本。其四是,多机房强一致,我们也在做相应方案调研,希望对一致性要求高的部分也能做到多活。

陈永庭,饿了么框架工具部高级架构师,主要负责MySQL异地双向数据复制,支撑饿了么异地多活项目。曾就职于WebEx、Cisco、腾讯等公司。

今天我主要分享饿了么多活的底层数据实施,会和大家介绍在整个多活的设计和实施过程中我们是怎么处理异地数据同步的,而这个数据同步组件在我们公司内部称之为DRC。

异地多活背景

在讲DRC或者讲数据复制之前,先跟大家回顾一下异地多活的背景。

去年我们在做多活调研的时候,整个公司所有的业务服务都是部署在北京机房,服务器大概有四千多台,灾备的机器是在云端,都是虚拟机,大概有三千多台。当时我们峰值的业务订单数量已经接近了千万级别,但是基本上北京机房(IDC)已经无法再扩容了,也就是说我们没有空余的机架,没有办法添加新的服务器了,必须要再建一个新的机房,于是我们在上海建一个新的机房,上海机房要在今年的4月份才会投入使用,所以需要在上海机房建成之后,异地多活项目能具备在生产环境上进行灰度。

异地多活的底层数据同步实施

这是异地多活的底层数据同步实施的一个简单的概要图,大家可以看到,我们有两个机房,一个是北京机房,一个是上海机房。在这个时候,我们期望目标是北方所有的用户请求、用户流量全部进入北京机房,南方所有的用户请求、用户流量进入上海机房。困难的地方是,这个用户有可能今天在北方,明天在南方,因为他在出差,还有就是存在一些区域在我们划分南北shard的时候,它是在边界上面的,这种情况会加剧同一个用户流量在南北机房来回漂移的发生。还有个情况,当我们某个机房出现故障,如核心交换机坏掉导致整个机房服务不可用,我们希望可以把这个机房的所有流量快速切到另外的数据中心去,从而提高整个饿了么服务的高可用性。

以上所有的因素,都需要底层数据库的数据之间是打通的。而今天我所要分享的DRC项目就是饿了么异地MySQL数据库双向复制的组件服务,即上图中红色框标记的部分。

异地多活对底层数据的要求

我们在前期调研DRC实现的时候,主要总结了的三点,而在后续的设计和实施当中,基本上也是围绕这三点来去解决问题。

第一个我们觉得是延迟要低,当时给自己定的目标是秒级的,我们希望在北京机房或上海机房写入的数据,需要在1秒钟之内同步到上海或者北京机房。整个延迟要小于1秒钟。

第二个就是我们要确保数据的一致性,数据是不能丢也不能错的,如果出现数据的不一致性,可能会给上层的业务服务、甚至给产品带来灾难性的问题。

第三个就是保证整个复制组件具备高吞吐处理能力,指的是它可以面对各种复杂的环境,比方说业务正在进行数据的批量操作、数据的维护、数据字典的变更情况,这些会产生瞬间大量的变更数据,DRC需要面对这种情况,需要具备高吞吐能力去扛住这些情况。

数据低延迟和一致性之间,我们认为主要从数据的并发复制这个策略上去解决,安全、可靠、高效的并发策略,才能保证数据是低延迟的复制,在大量数据需要复制时,DRC并发处理才能快速在短时间内解决。数据一致性,用户的流量可能被路由到两个机房的任何一个机房去,也就是说同样一条记录可能在两个机房中被同时更改,所以DRC需要做数据冲突处理,最终保持数据一致性,也就是数据不能出错。如果出现冲突且DRC自身无法自动处理冲突,我们还提供了一套数据冲突订正平台,会要求业务方一道来制定数据订正规则。

高吞吐刚才已经介绍了,正常情况用户流量是平稳的,DRC是能应对的,在1秒钟之内将数据快速复制到对端机房。当DBA对数据库数据进行数据归档、大表DDL等操作时,这些操作会在短时间内快速产生大量的变更数据需要我们复制,这些数据可能远远超出了DRC的最大处理能力,最终会导致DRC复制出现延迟,所以DRC与现有的DBA系统需要进行交互,提供一种弹性的数据归档机制,如当DRC出现大的复制延迟时,终止归档JOB,控制每轮归档的数据规模。如DRC识别属于大表DDL产生的binlog events,过滤掉这些events,避免这些数据被传输到其他机房,占用机房间带宽资源。

以上是我们在实施异地多活的数据层双向复制时对DRC项目提出的主要要求。

数据集群规模(多活改造前)

这是我们在做多活之前的北京数据中心的数据规模,这个数据中心当时有超过250套MySQL的集群,一千多台MySQL的实例,Redis也超过四百个集群。

DRC服务的目标对象就是这250套MySQL集群,因为在正在建设的第二个数据中心里未来也会有对应的250套MySQL集群,我们需要把两个机房业务对等的集群进行数据打通。

多活下MySQL的用途分类

我们按照业务的用途,给它划分了多种DB服务类型。为什么要总结这个呢?因为有一些类型,我们是不需要复制的,所以要甄别出来,首先第一个多活DB,我们认为它的服务需要做多活的。

比方说支付、订单、下单,一个机房挂了,用户流量切到另外新的机房,这些业务服务在新的机房是工作的。我们把这些多活服务依赖的DB称为多活DB,我们优先让业务把DB改造成多活DB,DRC对多活DB进行数据双向复制,保障数据一致性。多活DB的优势刚才已经讲了,如果机房出现故障、核心交换机出问题,整个机房垮了,运维人员登不进机房机器,那么我们可以在云端就把用户流量切到其它的机房。有些业务对数据有强一致性要求,后面我会讲到其实DRC是没有办法做到数据的强一致性要求的,它是有数据冲突发生的,需要引入数据订正措施。

业务如果对数据有强一致性要求,比方说用户注册,要求用户登录名全局唯一(DB字段上可能加了唯一约束),两个机房可能会在同一时间接收了相同用户登录名的注册请求,这种情况下,DRC是无法自身解决掉这个冲突,而且业务方对这个结果也是无法接受的,这种DB我们会把它归纳到GlobalDB里面,它的特性是什么呢?

它的特性是单机房可写,多机房可读,因为你要保证数据的强一致性的话,必须让所有机房的请求处理结果,最终写到固定的一个机房中。这种DB的上层业务服务,在机房挂掉之后是有损的。比方说机房挂了,用户注册功能可能就不能使用了。

最后一个非多活DB,它是很少的,主要集中于一些后端的管理平台,这种项目本身基本上不是多活的,所以这种DB我们不动它,还是采用原生的主备方式。

DRC总体架构设计

This image has an empty alt attribute; its file name is image-5.png

这是DRC复制组件的总体架构设计。我们有一个组件叫Replicator,它会从MySQL集群的Master上把binlog日志记录抽取出来,解析binlog记录并转换成我们自定义的数据,存放到一个超大的event buffer里面,event buffer支持TB级别的容量。

在目标机房里我们会部署一个Applier服务,这个服务启一个TCP长连接到Replicator服务,Replicator会不断的推送数据到Applier,Applier通过JDBC最终把数据写入到目标数据库。我们会通过一个Console控制节点来进行配置管理、部署管理以及进行各个组件的HA协调工作。

DRC Replicator Server

这是DRC Replicator Server组件比较细的结构描述,主要是包含了一个MetaDB模块,MetaDB主要用来解决历史的Binlog的解析问题。

我们成功解析Binlog记录之后,会把它转换成我们自己定义的一种数据结构,这种结构相对于原生的结构,Size更小,MySQL binlog event的定义在size角度上考虑事实上已经很极致了,但是可以结合我们自己的特性,我们会把不需要的event全部过滤掉(如table_map_event),把可以忽略的数据全部忽略掉。我们比对的结果是需要复制的event数据只有原始数据size的70%。

DRC Applier Server

往目标的MySQL集群复制写的时候,由DRC Applier Server负责,它会建一个长连接到Replicator上去,Replicator PUSH数据给Applier。Applier把数据拿到之后做事务的还原,最后通过JDBC把事务重新写到目标DB里面,写的过程当中,我们应用了并发的策略。

并发策略在提供复制吞吐能力,降低复制延迟起到决定的作用,还有幂等也是非常重要的,后面有很多运维操作,还有一些Failover回退操作,会导致发生数据被重复处理的情况,幂等操作保障重复处理数据不会发生问题。

DRC防止循环复制

在做复制的时候,大家肯定会碰到解决循环复制的问题。我们在考虑这个问题的时候,查了很多资料,也问了很多一些做过类似项目的前辈,当时我们认为有两大类办法,第一大类办法一开始否决了,因为我们对MySQL的内核原码不熟悉,而且时间上也来不及,虽然我们知道通过MySQL的核内解决回路复制是最佳的、最优的。

靠DRC自身解决这个问题,也有两种办法,一种办法是我们在Apply数据到目标DB的时候把binlog关闭掉,另外一种办法就是写目标DB的时候在事物中额外增加checkpoint表的数据,用于记录源DB的server_id。

后来我们比较了一下,第一个办法是比较简单,实现容易,但是因为Binlog记录没有产生,导致不支持级联复制,也对后续的运维带来麻烦。所以我们最后选择的是第二个办法,通过把事务往目标DB复制的时候,在事务中hack一条checkpoint的数据来标识事务产生的原始server,DRC在解析MySQL binlog记录时就能正确分辨出数据的真正来源。

DRC数据一致性保障

在刚开始研发、设计的时候,数据一致性保障是我们很头疼的问题。并不是在一开始就把所有的点都想全了,是在做的过程当中出现了问题,一步步解决的,回顾一下,我们大概从三个方面去保证数据的一致性:

首先,因为数据库是多活的,我们必须从数据中心层面尽可能把数据冲突发生的概率降到最低,避免冲突,怎么避免呢?就是合理的流量切分,你可以按照用户的维度,按照地域的维度,对流量进行拆分。刚才我们讲的,北方用户的所有数据在北京机房,这些北方用户的下单、支付等的所有操作数据都是在北方机房产生的,所以用户在同一个机房中发生的数据变更操作绝对是安全的。我们最怕的是同一个数据同时或者是在相近的时间里同时在两个机房被修改,我们怕的是这个问题,因为这种情况就会引发数据冲突。所以我们通过合理的流量切分,保证绝大部分时候数据是不会冲突的。

第二个我们认为你要保障数据一致性,首先你要确保数据不丢,一旦发生可能数据丢失的情况,我们会做一个比较保险的策略,就是把数据复制的时间位置回退,即使重复处理数据,也避免丢数据的可能,但是这个时候会带来数据重复处理的问题,所以数据的幂等操作特别重要。

这些都是我们避免数据发生冲突的方法,那冲突实际上是不可避免的,冲突发生后,我们怎么解决?最终采用的办法是在数据库表上隐含地加一个时间字段(数据最后更新时间),这个字段对业务是透明的,主要用来辅助DRC复制,一旦数据发生冲突,DRC复制组件可以通过这个时间来判断两个机房或者三个机房中的哪条数据是最后被更新的,最新优先的原则,谁最后的修改时间是最新的,就以它为准。

DRC数据复制低延迟保障

刚才我们讲的是数据的一致性,还有一个点非常重要,就是数据复制的低延迟保障。我们现在延迟包括用户高峰时间也是小于1秒的,只有在凌晨之后,各种归档、批量数据处理、DDL变更等操作会导致DRC延迟出现毛刺和抖动。如果你的延迟很高的话,第一在做流量切换时,因为运维优先保障产品服务的可用性,在不得以的情况会不考虑你的复制延迟,不会等数据复制追平之后再切流量,所以你的数据冲突的概率就变的很大。

为了保证复制低延迟,我们认为主要策略、或者你在实施时主要的做法还是并发,因为你只有用高效的安全的并发复制策略,服务才有足够的吞吐处理能力,而不至于你的复制通道因为遇到“海量”数据而导致数据积压,从而加剧了复制延迟的产生。

我们一开始采用的基于表级别的并发,但是表级别的并发在很多情况下,并发策略没办法被有效的利用,比方说有的业务线的数据库可能90%的数据集中在一张表或者是几个表里面,而大部分表数据量很小,那基于表的并发策略就并发不起来了。我们现在跑的是基于行级别的并发,这种并发它更能容忍和适应很多场景。

DRC & MySQL Master切换

这个是DRC复制组件与MySQL集群的关系关联图,一旦MySQL集群里面的Master发生了主备切换,原来的Master挂了,DRC怎么处理?目前的解决方案是DBA系统的MHA工具会通知DRC控制中心,DRC的控制中心会找到对应的复制链路,然后把复制链路从老的Master切到新的Master,但是关键点是MHA在通知之前先把老的Master设置为不可写,阻断DRC可能往老的Master继续写数据。

DRC线上运行状况(规模)

这个是我们DRC上线之后的运行状况。现在大概有有将近400多条复制链路。这个复制链路是指单向的链路。我们提供的消息订阅大概有17个业务方接入,每天产生超过1亿条的消息。

DRC线上运行状况(性能)

这是DRC线上运行的一个性能监控快照,我们可以看到,它是上午11点多到12点多的一个小时的性能,你会发现其实有一个DB是有毛刺的,有一个复制链路有毛刺,复制延迟最高达到4s,但是大部分的复制链路的延迟大概也是在1秒或1秒以下。

我的分享到此结束了,谢谢大家。

Q&A

Q1:你好,想问一下饿了么是怎么避免各个机房中的PK冲突的?

A1:主键自增的步长在各个机房中是固定相同的,但是每个机房的增长offset是不同的,所以不会出现PK冲突。

Q2:DRC复制会不会对目标数据库造成性能影响?

A2:有影响。因为DRC会占用目标DB的IOPS。DRC Apply本身就是目标DB的上层服务。

Q3:DRC Applier采用JDBC去写目标DB,除了这个办法还有其它途径吗?

A3:目前我们分析binlog还原事务,然后通过JDBC把事务写到目标DB。我们曾经模拟过MySQL的binlog server,让目标DB启动一个Replication连接到我们伪造的binlog server上,我们的binlog server会把binlog记录发给目标DB,这个办法会存在很多问题,我们就放弃了这个办法。

Q4:有监测数据一致性的工具吗?

A4:这个是有的。DBA团队开发了一套checksum工具来实时监测数据一致性。

Q5:饿了么做异地双活主要的原因就是刚刚提到的单个机房是无法扩容吗?

A5:是的。

Q6:双向同步后期的运维成本高吗?

A6:对DBA的运维会造成影响,DBA的归档job、DDL发布等操作都需要考虑DRC的双向同步因素。

饿了么技术团队花了1年多的时间,实现了业务的整体异地多活,能够灵活的在多个异地机房之间调度用户,实现了自由扩容和多机房容灾的目标。本文介绍这个项目的整体结构,还简要介绍实现多活的5大核心基础组件,为读者建立基本的概念模型,后续会有系列文章陆续介绍每个组件的实现细节。读者能够从中了解到做异地多活的大方向,为实现自己的异地多活,或者是容灾备份提供参考。

背景:为什么要做异地多活?

饿了么要做多活,是受业务发展的驱动,经过几年的高速发展,我们的业务已经扩大到单个数据中心撑不住了,主要机房已经不能再加机器,业务却不断的要求加扩容,所以我们需要一个方案能够把服务器部署到多个机房。另外一个更重要的原因是,整个机房级别的故障时有发生,每次都会带来严重的后果,我们需要在发生故障时,能够把一个机房的业务全部迁移到别的机房,保证服务可用。

归纳起来,我们要达到两个目标:

  1. 服务可以扩展到多个机房
  2. 能够应对整个机房级别的故障

解决这两个问题的常见办法是做异地多活,把服务分散到多个机房,自然扩展和高可用的问题就迎刃而解了。

饿了么有自己的特殊情况,对于其他大部分公司来说,其实要不要做多活,主要就是看下图这样一个曲线。

对于一个业务快速增长的企业,每次故障带来的损失也相应是加速增长的,而技术的投入总体上是线性的,初期故障损失小于技术投入,在某个时间点,故障的损失会超过技术投入,这时就要用一些高可用方案,来避免故障,多活就是其中最重要的一种。

异地多活面临的主要挑战是网络延迟,以北京到上海 1468 公里,即使是光速传输,一个来回也需要接近10ms,我们在实际测试的过程中,发现上海到北京的网络延迟,一般是 30 ms。这 30 ms可以和运算系统中其他的延迟时间做个比较:

L1 cache reference ……………………. 0.5 ns

Branch mispredict ………………………. 5 ns

L2 cache reference ……………………… 7 ns

Mutex lock/unlock ……………………… 25 ns

Main memory reference …………………. 100 ns

Compress 1K bytes with Zippy …………. 3,000 ns = 3 µs

Send 2K bytes over 1 Gbps network ……. 20,000 ns = 20 µs

SSD random read …………………… 150,000 ns = 150 µs

Read 1 MB sequentially from memory ….. 250,000 ns = 250 µs

Round trip within same datacenter …… 500,000 ns = 0.5 ms

Read 1 MB sequentially from SSD* ….. 1,000,000 ns = 1 ms

上海之间两个机房的网络延时…………….. 1,000,000 ns = 1 ms

Disk seek ……………………… 10,000,000 ns = 10 ms

Read 1 MB sequentially from disk …. 20,000,000 ns = 20 ms

北京到上海的网络延时……………….. 30,000,000 ns = 30 ms

Send packet CA->Netherlands->CA …. 150,000,000 ns = 150 ms

北京上海两地的网络延迟时间,大致是内网网络访问速度的 60 倍(30ms/0.5ms),如果不做任何改造,一方直接访问另外一方的服务,那么我们的APP的反应会比原来慢 60 倍,其实考虑上多次往返,可能会慢600倍。

如果机房都在上海,那么网络延迟只有内网速度的2倍,可以当成一个机房使用。所有有些公司的多活方案,会选择同城机房,把同城的几个机房当成一个机房部署,可以在不影响服务架构的情况下扩展出多个机房,不失为一个快速见效的方法。我们在做多活的初期也讨论过同城方案,比如在北京周边建设一个新机房,迁移部分服务到新机房,两个机房专线连接,服务间做跨机房调用。虽然这个方案比较容易,也解决了机房的扩展问题,但是对高可用却没有好处,相反还带来了更高的风险。

与同城多活的方案不同,异地多活的方案会限制机房间的相互调用,需要定义清晰的服务边界,减少相互依赖,让每个机房都成为独立的单元,不依赖于其他机房。经过几番考量,我们最终选择了异地多活的方案,对这两个方案的比较和思考可以见下表,异地多活虽然更困难一点,但是能同时达到我们的两个核心目标,更为可行。

设计:异地多活的实现思路和方法

我们的异地多活方案的,有几条基本原则,整个多活方案都是这些原则的自然推导。但在介绍一下这些原则之前,先要说明一下饿了么的服务流程,才能让大家更好的理解这些原则的来由

下面这张简图是我们的主流程:

业务过程中包含3个最重要的角色,分别是用户、商家和骑手,一个订单包含3个步骤:

  1. 用户打开我们的APP,系统会推荐出用户位置附近的各种美食,推荐顺序中结合了用户习惯,推荐排序,商户的推广等。用户找到中意的食物 ,下单并支付,订单会流转到商家。
  2. 商家接单并开始制作食物,制作完成后,系统调度骑手赶到店面,取走食物
  3. 骑手按照配送地址,把食物送到客户手中。

整个下单到配送完成,有严格的时间要求,必须在短短的几十分钟内完成,我们的服务和地理位置强相关,并且实时性要求高,服务的地域性和实时性是我们的核心特性,多活设计最重要的是满足这两个特性。

进过反复讨论,我们的多活架构通过遵循以下几条基本原则,来满足这两个核心特性:

  1. 业务内聚:单个订单的旅单过程,要在一个机房中完成,不允许跨机房调用。这个原则是为了保证实时性,旅单过程中不依赖另外一个机房的服务,才能保证没有延迟。我们称每个机房为一个 ezone,一个 ezone 包含了饿了么需要的各种服务。一笔业务能够内聚在一个 ezone 中,那么一个定单涉及的用户,商家,骑手,都会在相同的机房,这样订单在各个角色之间流转速度最快,不会因为各种异常情况导致延时。恰好我们的业务是地域化的,通过合理的地域划分,也能够实现业务内聚。
  2. 可用性优先:当发生故障切换机房时,优先保证系统可用,首先让用户可以下单吃饭,容忍有限时间段内的数据不一致,在事后修复。每个 ezone 都会有全量的业务数据,当一个 ezone 失效后,其他的 ezone 可以接管用户。用户在一个ezone的下单数据,会实时的复制到其他ezone。
  3. 保证数据正确:在确保可用的情况下,需要对数据做保护以避免错误,在切换和故障时,如果发现某些订单的状态在两个机房不一致,会锁定该笔订单,阻止对它进行更改,保证数据的正确。
  4. 业务可感:因为基础设施还没有强大到可以抹去跨机房的差异,需要让业务感知多活逻辑,业务代码要做一些改造,包括:需要业务代码能够识别出业务数据的归属,只处理本 ezone 的数据,过滤掉无关的数据。完善业务状态机,能够在数据出现不一致的时候,通过状态机发现和纠正。

这几条基本原则,贯穿了饿了么多活的整个设计。

基于这几条原则,我们从服务划分,流量路由,业务改造等方面设计了多活方案,下面简要介绍一主要逻辑。

服务划分(Sharding):

为了实现业务内聚,我们首先要选择一个划分方法(Sharding Key),对服务进行分区,让用户,商户,骑手能够正确的内聚到同一个 ezone 中。分区方案是整个多活的基础,它决定了之后的所有逻辑。

根据饿了么的业务特点,我们自然的选择地理位置作为划分业务的单元,把地理位置上接近的用户,商户,骑手划分到同一个ezone,这样一个订单的履单流程就会在一个机房完成,能够保证最小的延时,在某个机房出现问题的时候,也可以按照地理位置把用户,商户,骑手打包迁移到别的机房即可。

所以我们最终选择的方案如下图,自定义地理划分围栏,用围栏把全国分为多个 shard,围栏的边界尽量按照行政省界,必要的时候做一些调整,避免围栏穿过市区。一个ezone可以包含多个 shard,某个 ezone 的 shard ,可以随时切换到另外一个 ezone ,灵活的调度资源和failover。

这样的划分方案,基本解决了垮城市下单的问题,线上没有观察到有跨 ezone 下单的情况。围栏的划分是灵活的,可以随着以后业务的拓展进行修改,因为每个机房都是全量数据,所以调整围栏不会导致问题。

对这种划分方法,有一些常见的疑问,比如:

1.如果两个城市是接壤的,会出现商家和用户处于不同 ezone 的情况,岂不是破坏了内聚性原则?

这种情况确实会出现,为了尽量避免,我们在划分shard的时候没有简单的用城市名称,而是用了复杂的地理围栏实现,地理围栏主体按照省界划分,再加上局部微调,我们最大限度的避免了跨ezone下单的情况。但如果真的出现了,用户下单也不受影响,最多只是状态有1s左右的延迟。

2.用户是会动的,如果用户从北京到了上海,那么划分规则应该怎么应对?

用户在北京下单,数据落在北京shard,到上海下单,数据则落在上海的 shard,借助于底层的数据同步工具,用户无论在什么地方,都能看到自己的数据,但是有1s左右的延时,对于大部分的业务场景,这个延迟是可以承受的。当然也有些业务场景不能接受这 1s 的延时,我们也提供了另外的方案来应对,参考下文介绍Globa Zone的章节。

3.为什么不简单点,按照用户的ID来切分?

阿里是按照用户ID的取模来划分单元的,比较简洁。我们如果也用ID做切分,同一地方的用户,商户,骑手可能被划分到不同 ezone,就会出现比较多的跨机房调用,这样就更可能出现延迟,难以保证实时性。所以,我们本地配送的业务模式,决定了需要用地理位置来划分服务。

流量路由:

基于地理位置划分规则,我们开发了统一的流量路由层(API Router),这一层负责对客户端过来的 API 调用进行路由,把流量导向到正确的 ezone。API Router 部署在多个公有云机房中,用户就近接入到公有云的API Router,还可以提升接入质量。

前端 APP 做了改造,为每个请求都带上了分流标签,API Router 会检查流量上自带的分流标签,把分流标签转换为对应的 Shard ID,再查询 Shard ID 对应的 eZone,最终决定把流量路由到哪个 ezone。

最基础的分流标签是地理位置,有了地理位置,AR 就能计算出正确的 shard 归属。但业务是很复杂的,并不是所有的调用都能直接关联到某个地理位置上,我们使用了一种分层的路由方案,核心的路由逻辑是地理位置,但是也支持其他的一些 High Level Sharding Key,这些 Sharding Key 由 APIRouter 转换为核心的 Sharding Key,具体如下图。这样既减少了业务的改造工作量,也可以扩展出更多的分区方法。

除了入口处的路由,我们还开发了 SOA Proxy,用于路由SOA调用的,和API Router基于相同的路由规则。APIRouter 和 SOAProxy 构成了流量的路由通道,让我们可以灵活的控制各种调用在多活环境下的走向。

数据复制:

为了实现可用优先原则,所有机房都会有全量数据,这样用户可以随时切换到其他机房,全量数据就需要对数据进行实时复制,我们开发了相应的中间件,对 mysql,zookeeper ,消息队列和 redis 的数据进行复制。

Mysql 数据复制工具 DRC:

Mysql 的数据量最大,每个机房产生的数据,都通过 DRC 复制到其他 ezone,每个ezone的主键取值空间是ezoneid + 固定步长,所以产生的 id 各不相同,数据复制到一起后不会发生主键冲突。按照分区规则,正常情况下,每个 ezone 只会写入自己的数据,但万一出现异常,2个 ezone 同时更新了同一笔数据,就会产生冲突。DRC 支持基于时间戳的冲突解决方案,当一笔数据在两个机房同时被修改时,最后修改的数据会被保留,老的数据会被覆盖。

ZooKeeper 复制:

有些全局的配置信息,需要在所有机房都完全一致,我们开发了 zookeeper 复制工具,用于在多个机房中同步 ZK 信息。

消息队列和Redis复制:

MQ,Redis 的复制与 ZK 复制类似,也开发了 相应的复制工具。

强一致保证:

对于个别一致性要求很高的应用,我们提供了一种强一致的方案(Global Zone),Globa Zone是一种跨机房的读写分离机制,所有的写操作被定向到一个 Master 机房进行,以保证一致性,读操作可以在每个机房的 Slave库执行,也可以 bind 到 Master 机房进行,这一切都基于我们的数据库访问层(DAL)完成,业务基本无感知。

切换过程和各种异常保护:

避免数据错误非常重要,在网络断开,或者是切换过程中,特别容易产生错误数据。比如由于复制延时,订单状态不一致,用户有可能会重复支付。为了避免我们采取了一些保护措施,避免在切换时发生错误。

  1. 在网络中断时,如果不是必要,不做切换,因为任意单个机房能够提供完整服务。
  2. 如果需要切换,对锁定切换过程中的订单,直到切换完成,数据复制正常,才开放锁定。这个过程也通过 DAL 来实现
  3. 对于标记为其他机房的写入数据,DAL 会进行保护,拒绝写入。
  4. DRC 会检查并报告错误的写入操作,方便检查隐藏问题。

通过以上4条的保护,我们保证了数据的正确性,频繁的切换也不会出现异常的业务数据。

多个机房的Cache刷新:

数据的变更信息,通过 DRC 广播到多个机房,实现缓存的刷新,保证各个机房的缓存一致性。

整体结构

以上介绍了各个考虑的方面,现在可以综合起来看,饿了么多活的整体结构如下图:

业务改造:

业务可感知是一条基本原则,通过中间件提供的服务,多活逻辑会暴露给业务方,例如:当前服务所属的 ezone,路由策略,数据的归属 shard 等,基于这些信息,业务可以执行很多的逻辑。包括:

  1. 后台任务可以过滤掉非本 ezone 的数据。
  2. 可以在发生切换时,执行特定的逻辑,触发特定动作。
  3. 业务需要准备一些数据修复逻辑,在万一发生不一致时,手工或者自动纠正数据。

实现:多活的基础中间件

下面简要介绍一下支持以上功能的中间件,我们归纳为多活 5 大基础组件,之后会有系列文章,介绍每个基础组件的具体实现。

APIRouter : 路由分发服务

API Router是一个HTTP反向代理和负载均衡器,部署在公有云中作为HTTP API流量的入口,它能识别出流量的归属 shard ,并根据 shard 将流量转发到对应的 ezone。API Router 支持多种路由键,可以是地理位置,也可以是商户ID,订单ID等等,最终由 API Router 映射为统一的 Sharding ID。

Global Zone Service:全局状态协调器

GZS 维护着整个多活的路由表,其他所有的服务都从 GZS 订阅路由信息。切换机房的操作也在 GZS 控制台中完成。路由表包括:地理围栏信息,shard 到 ezone 的归属信息,商铺ID/订单ID 等路由逻辑层到 shard id 的映射关系等。GZS 通过在 SDK 端建立 Cache,来保证shard 逻辑能够最快速度执行,基本不需要和 GZS 交互,同时也有实时推送机制,确保在数据变更后能够快速通知到其他的服务。

SOA Proxy:内部网关

SOA Proxy 实现了对 SOA 调用的路由,执行和 API Router 相似的逻辑,但只用在机房之间进行通信的场景。业务使用 SOA Proxy 需要对代码做一些修改,把路由信息加入到调用的上下文中。

Data Replication Center:数据复制

DRC 负责 Mysql 数据的实时双向复制,保证跨机房延时在 1s 以内。提供了基于时间的冲突解决方案,确保各个机房的数据一致。DRC 除了复制数据,还对外提供了数据变更的通知,让业务能够感知到其他机房的数据变化,做相应的处理,例如清除Cache等。除了DRC,我们还有 ZK复制工具,RMQ 复制工具,Redis复制工具,基本每个数据层次,都有对应的复制方案。

Data Access Layer:数据访问

数据访问层支撑了 Globa Zone 的逻辑,还提供了最后一道保护,拒绝路由错误的数据写入,是多活最底层的支撑。

未来:下一步多活的计划

目前饿了么的服务已经部署到2个异地机房,下一步我们会扩展到3-4个机房,并且在公有云上建立一个新的ezone,充分利用公有云的强大的扩展能力,未来我们将能够快速的在全世界各地搭建数据中心,也能够快速的利用各种公有云基础设施,实现全球规模的高可用和扩展性。

支付宝资深技术专家尹博学:新一代金融核心突破之全分布式单元化技术架构

2020-07-09 · 尹博学 · #分布式架构

过去几年是云原生理念高速普及的黄金时期。微服务、容器、无服务器架构、服务网格等新技术的出现,在技术社区中激起了一浪又一浪的创新热潮。然而由于金融行业对性能和安全的严苛要求,云原生技术在企业实际场景中的实施落地,特别是在金融场景的实施落地,仍然面临诸多挑战。

本文整理自2020阿里云线上峰会蚂蚁集团资深技术专家尹博学的主题演讲,为大家分享蚂蚁关于金融级 IT 架构及分布式架构的思考和应用实践。关注本网站,蚂蚁 SOFAStack 白皮书即将发布,不要错过哦~

以下为演讲整理全文:

大家好,我是蚂蚁集团的尹博学,今天和大家分享一下蚂蚁关于金融级 IT 架构及分布式架构的一些思考和应用案例,主要包含三个部分,分别是行业常见的分布式架构介绍、蚂蚁单元化架构的介绍以及单元化架构的应用案例。

行业常见分布式架构

行业常见的分布式架构主要包含,单活架构、双活架构和冷备架构。从容灾能力角度来看,双活架构和冷备架构均能做到应用级跨机房容灾,但是数据库因为使用了异步复制的技术,无法做到机房级 RPU=0 的容灾。再看灰度发布的能力,冷备架构和双活架构都只能做到机房级灰度发布,无法做到更细粒度的灰度发布。

蚂蚁集团单元化架构介绍

在介绍完行业常见的分布式架构后,我们来看一下蚂蚁的分布式架构发展历程,和单元化架构的详细介绍。

这是蚂蚁分布式架构发展历程。蚂蚁也经历了单活同城双活两地三中心三个阶段。其中两地三中心是同城双活加一个冷备。随着蚂蚁业务和业务量复杂度的越来越高,业务对于基础架构的要求也越来越高,即扩展能力容灾能力灰度能力要求越来越高。最终蚂蚁发展到了单元化架构,将主要业务拆分单元即分片,装入不同的逻辑单元内,每个分片的数据库实现三地五中心部署即三地五中心的单元化架构。

首先我们来看一下蚂蚁单元化架构的整体架构设计,整体架构包含 RZone、GZone 和 CZone。其中 GZone 部署的是无法拆分的数据和业务,GZone 的数据和业务被 RZone 依赖,GZone 全局只部署一份,而 RZone 部署的是可拆分的业务和对应的数据。每个 RZone 内的数据分片如图所示有五副本,实现三地五中心部署,每个分片内只有一个可写入的主副本,其余副本按照 Paxos 协议做数据强一致。每个 RZone 内实现业务单元封闭,独立完成自己的所有业务。而 CZone 的出现是因为 GZone 全局只有一份,不同城市的 RZone 可能依赖 GZone 服务和数据的时候需要远距离调用,延迟比较大,所以在每个城市部署一个 CZone 作为 GZone 的只读副本,为本城市的 RZone 提供服务。

介绍完单元化架构的整体设计之后,我们从容灾、灰度发布、弹性三个方面详细看一下该架构的能力。

首先看容灾能力,容灾能力分为同城容灾异地容灾,以图中所示为例,RZone1 出现故障先看同城容灾能力,我们目标将 RZone1 切换至同城容灾 RZone2。先做数据库分片切换,RZone1 对应的分片为分片1,把分片1在 RZone2 的副本提升为主副本,数据库副本提升完毕后将 RZone1 的流量切换至 RZone2,实现同城容灾 RPO=0、RTO<1min。

再看异地容灾,同样以 RZone1 故障为例。目标切换至 RZone3,先做数据库切换,分片1在 RZone3 的副本切换成主副本,完成后将 RZone1 的流量切换至 RZone3,实现异地容灾,该过程 RPO=0、RTO<1min。

接下来我们看弹性。弹性的背景是业务在大促、节假日等流量出现大幅上涨的过程,我们可以短期租借新的城市和新的 IDC。如图所示,我们租借城市 X 的 IDCX 作为 RZoneX,将 RZone5 的部分流量弹出至 RZoneX,对应流量的数据也弹出至 RZoneX 内。在节假日大促结束之后,将 RZoneX 内的流量和数据弹回至 RZone5,然后回收 RZoneX,这样大幅节约了机房成本。

介绍完弹性之后,我们来看灰度能力。如图所示,我们将四个 RZone(RZone1、RZone2、RZone3、RZone4)的业务和应用分为 A、B 组,日常 A 组和 B 组各承担50%的应用流量。在应用新版本发布时,我们将 A 组的流量全部切换至 B 组,此时在 A 组上部署新版本,部署完毕后将 B 组的流量按粒度切换至 A 组上,切换粒度等于数据分片的粒度。在切换的过程中可以做 A 组和 B 组的服务对比,如果发现 A 组的服务异常,可以快速将流量切换回 B 组。在 A 组服务一段时间后无异常发生,最终可以将 B 组的流量全部切换至 A 组,把 B 组的版本更新为新的版本,在整个切换的过程中实现了可灰度、可回滚、可监控。

我们再深入到架构内部,来阐释一下架构内关键模块是如何支撑该架构的。

首先我们看流量路由模块。流量路由模块的核心是将用户的 uid 信息和对应的 Zone 信息植入到 cookie 中,供流量路由模块做精准路由。我们以用户 uid=68、RZone=RZ03 为例来看流量路由模块是如何工作的,首次用户接入时 cookie 内无 Zone 信息,流量接入模块会随即将该请求发到一个 RZone 内,如发到 RZone1 内,RZone1 通过 zoneClinet 会准确计算该请求应发至 RZone3,即通过 RouteClinet 将该请求发送。发送过程中将计算出的 uid 信息和对应的 Zone 信息植入 cookie 内转发至 RZone3,RZone3 完成本次业务请求后将结果返回给用户,其后用户同意 session 内的其它请求,因为在 cookie 内已经有了准确的路由信息,会被流量路由模块准确的发至 RZone3 完成业务请求。

接着我们再看一下服务路由,服务路由分为本机房服务路由和跨机房服务路由调用。先看本机房服务路由,服务调用端向本机房服务注册中心订阅服务,发现服务地址后做本机房服务路由调用。再看跨机房服务路由调用,服务调用端向其他 IDC 的注册中心订阅服务地址,发现服务地址后做跨机房服务调用。

最后我们看数据是如何实现高可靠的。蚂蚁使用自研的分布式关系数据库 OceanBase,每个分片的数据库做5副本部署,部署地域实现三地五中心部署,5副本中有3副本实现强一致,如图所示可以实现同城、IDC 容灾和异地容灾。

单元化架构实践场景

介绍完蚂蚁单元化架构的主要概念即关键模块信息之后,我们看一下单元化架构在外部客户实施的一些案例。

第一个案例是一家城商行,它的业务系统、IT系统历史比较长,无法一步跨越到单元化架构,我们为其推荐了大 GZone 的模式,即把城商行的所有服务和数据不做拆分,直接装入一个 GZone 内,在 GZone 的基础上实现同城双活即应用同城双中心部署,数据库同城三中心部署,从而实现同城容灾能力,RPO=0、RTO<1min,但无法实现异地容灾能力,其可灰度能力和弹性能力都无法做到更细力度。

再看第二个区域银行的案例。我们为这家区域银行实现了同城单元化,即将这家区域银行的主要业务拆分成两个逻辑业务单元两个分片,将其装入一个城市的两个 IDC 内,在另外一个城市建设冷备,其数据库每个分片实现5副本部署,其中4副本在主城市两个中心内部署,1副本部署在了本机房内。该架构实现了同城容灾能力,同时也实现了细粒度的灰度能力和弹性能力,但同样无法实现异地容灾能力。

最后我们看一下蚂蚁网商银行的案例。网商银行实现了异地多活单元化完整的架构,网商银行的主要业务拆分成了4个分片,装入4个 RZone 内,这4个 RZone 分别部署在了两个城市内,各承担25%的流量,而数据库实现5副本三个城市部署。其中提供服务的两个地域两个城市部署4副本,远端部署1副本。该架构实现了同城容灾、异地容灾,同时也实现更细粒度的灰度能力和弹性伸缩能力。

介绍完这三个案例后,我们看到了单元化架构的一个灵活性,既可以大 GZone 部署,也可以同城单元化部署和异地多活单元化部署。今天我们介绍了蚂蚁集团架构的发展历程即单元化在一些关键外部客户的应用案例。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注