双活架构之路

18年春节之后,我司商城部门正式确定了今年的三大重点项目,其中的技术项目即为双活架构。面对早已突破千万的用户规模,单机房的单点故障,对线上线下服务的稳定性来说,是一个不容逃避的隐患。作为商城中台的技术负责人,由我主导,与运维和平台部门一起,对商城中台的业务系统进行了双活架构的设计和升级改造。

经过 18 年上半年对同行双活方案的调研和内外部的交流讨论,综合我们目前的的业务规模,我们逐渐确定了采用主流的同城双活+异地冷备的两地三中心方案。然而,因为我们的自建 IDC 机房在无锡,而无锡也没有第二个机房能满足我们的要求。因此,我们决定把无锡机房内的商城服务逐渐迁移到上海公有云,然后在上海公有云选择两个机房做同城双活,无锡则作为异地的冷备机房。

商城中台在无锡机房有几百台的应用服务器和几十台的 DB,而且上海公有云机房的机器和整个网络链路也没有经过线上流量的验证,所以不可能一晚上就把这些应用和 DB 数据迁移过去。因此,我们采取了灰度迁移的方案,中间过程中两个机房各分担一部分的业务流量。机智的你肯定发现了,这不就是异地双活吗,还要啥自行车啊?带着这个问题,一起来看下我们做双活架构升级中踩过的坑和总结的经验。

在这里插入图片描述

1 当我们谈双活时,是在谈什么?

首先我们来简单的聊三个问题:何为双活,主要挑战是什么,以及为什么从同城开始。

1.1 何为双活?

顾名思义,双活的关键点就是“双”和“活”。其中“双”就是指两个机房:异地的话,就是业务部署在相距较远的不同城市的两个机房,比如北京和广州;同城的话,一般是不同区的两个机房,比如北京的通州和海淀。“活”则是活动、活跃的意思。与其相对的字是“备”,即备份,正常情况下不对外提供服务。如果需要提供服务,需要人工的干预和操作,花费一定的时间把“备”提升为“活”。

判断一个系统是否是双活,需要满足两个标准:

  • 正常情况下,用户无论访问那个机房的业务系统,都能得到正确的业务响应;
  • 某个机房的业务异常的时候,用户访问其他机房的正常的业务系统,能够得到正确的业务响应。

1.2 主要挑战是什么?

双活或多活的主要目标,是为了提升业务系统的可用性。然而,根据 CAP 定理,在一个分布式系统中,Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可兼得。一般来说,分区无法避免,特别是在网络环境更复杂的多活架构中,因此可以认为 CAP 的 P 总是需要满足的。那 CAP 定理告诉我们的,是剩下的 C 和 A 无法同时做到。

在这里插入图片描述
因此,双活的主要挑战是,如何在提升业务系统可用性的同时,又不丧失对业务数据一致性的保证。双活架构设计的关键和核心,也都是围绕着一致性这个限制。

1.3 为什么从同城开始?

如上所述,双活是为了避免某一地区机房的故障,导致整体业务系统不可用的问题,而将业务系统部署在不同地区的两个机房,实现守望相助的目标。注意这里双活解决的问题:一个地区机房的故障。所以,从影响范围上,大体可以把这种故障分为两类,即地区级的和机房级的。对于地区级的故障,如美加大停电、新奥尔良水灾,同城异区的双活似乎并不能发挥什么作用,那我们为什么还要做同城双活呢,为什么不直接上异地双活呢?我们从两方面来分析下。

一方面,我们来看看两种架构的设计实现的复杂度和成本。同城的两个机房,一般距离也就几十公里。通过搭建高速的专线网络,延迟可以降低到 1ms 左右。1ms 的延迟意味着,在架构的设计实现上,我们可以不用区分地理位置上的两个机房,逻辑上把它们当做一个机房。这将大大降低设计实现的复杂度,同时,短距离的专线也意味着较少的成本。而距离较远的异地机房,比如北京到广州 2000 多公里的距离,50ms 左右的延迟,则会带来架构复杂度上的质变,下文我们会详述。同时,长距离专线网络的搭建或者使用的成本也会高不少。

另一方面,我们来看看地区级和机房级两种故障的发生概率。同城双活架构确实对地区级大范围的故障无能为力。但是,这种故障的发生概率是很低的,可能几年或者几十年才发生一次。而机房级别故障,像火灾、停电、空调故障、出入口光纤被挖断等等这类故障发生的概率更高,而且破坏力一样很大。而对这些故障,同城异区的双活架构可以很好地解决。
因此,综合复杂度、成本和故障发生概率来考虑,我们的双活之路,先从同城开始。

2 同行怎么玩?

说完了双活是什么,我们再来看看同行们都是怎么玩的。这也是我们在上半年主要调研和沟通的问题。

2.1 淘宝

从阿里巴巴高级技术专家谢吉宝在 2017 AS全球架构师峰会上的《阿里异地多活与同城双活的架构演进》分享里可以看到,阿里自 2013 年从杭州同城双活开始,经历了三年,到 2015 实现千里之外的三地四单元架构。

在这里插入图片描述
阿里淘宝的同城双活,乃至后来的异地多活方案,始终围绕着一个关键词——单元化,这也是阿里内部多活项目的名字。所谓单元化,即对数据和业务流量做单元化切分。

在这里插入图片描述
单元化切分的好处显而易见,我们从可用性和一致性两方面来看下:

  • 可用性:按用户分片后,不同用户的数据和业务流量分布到不同的数据中心,每个数据中心内的业务最大限度闭环。任何一个数据中心的故障,都只影响分发到那个数据中心的用户请求。而且,通过数据中心间的数据同步,用户的数据并不会丢失,用户请求可以分发到其他数据中心继续处理。
  • 一致性:每个数据中心内的业务闭环,在各个数据中心都正常时,强一致性可以得到保证。在某个数据中心发生故障时,流量会切换到其他数据中心。在数据完全同步完成之前,会出现短暂的(依赖数据中心间的网络延迟)不一致。数据完全同步完成之后,重新恢复一致。所以,可以保证最终的数据一致性。

然而,对于这个方案的现实挑战是,电商业务涉及的数据维度比较多,典型的就有买家、卖家和商品三个维度,而数据和流量的切分只能选择一个维度,按照哪个维度来切分呢?淘宝选择了拿买家“开刀”。其单元化的基本原则是:

  • 按买家维度来进行数据切片
  • 只取与买家链路相关的业务(单元)做“多活”
  • 单元内最大限度的封闭
  • 无法接受数据最终一致的跨单元单点写

在这里插入图片描述

可以看到,淘宝的多活主要针对的是买家维度相关的应用和数据,商品、卖家、库存和其他长尾应用依然是中心化的单点写。其对应的技术架构如下:

在这里插入图片描述
总结下,淘宝通过对买家维度数据和流量的单元化切分,从而达到可用性和一致性的有效平衡。从技术架构的设计图中可以看到,这个方案的另一大优势是,其流量切分只涉及对 CDN 等路由组件的升级,对业务代码没有侵入,业务流程也无需修改,非常有利于多活架构的快速推进和演化。这个思路也对我们之后的机房灰度迁移方案提供了重要的参考,在我们的设计中也有体现。

2.2 点评

对点评双活方案的了解,起先来源于其官方技术站 2018 年的一篇文章 《大众点评账号业务高可用进阶之路》。之后,经过前司同事的牵线搭桥,有幸跟这篇文章的作者又进行了几次技术交流。

整体上,点评主要对账号业务做了高可用的异地双活。由于账号业务数据维度众多(手机号、邮箱、UID等等)、划分困难,其异地多活的方案设计,并没有采取业界成熟的主流思路,也即上文中淘宝使用的单元化。而是针对点评账号业务读多写少的特性(读写比 350:1),在数据库层采取一主多从的部署方案,优先解决读多活的问题。这样的话,数据库层的一致性就规避了。再去除无状态的应用层,点评的双活设计主要集中在对缓存层 Redis 的改造上。

其最终技术方案如下:
在这里插入图片描述
DB 层上文说过了,一主多从。在这个图上,即是在上海放一个主库,承接读写请求,上海和北京分别跟一个从库,承接读请求。

我们主要来看 Redis 缓存层的设计。在缓存层,因为 Redis 本身主从同步机制的缺陷,点评并没有采用Redis 主从模式。而是采用双主模式,通过美团的消息中间件 Mafka(类Kafka)来做双向同步。双主模式下,为了解决两地并发写可能会造成的数据不一致的问题,点评为每个 redis 数据的 value 存入一个版本号。当两个写入发生冲突的时候只要比较这个版本号的大小即可,版本号大的覆盖小的。
版本号
写并发时数据同步过程如下:
在这里插入图片描述
点评双活架构下的缓存方案,还有一些很好很细致的优化设计,比如缓存新增和更新时的加载模式等等。这里不再赘述,感兴趣的可以原文查阅。我们再花点篇幅来分析下,点评的版本号设计,是否真的能保证数据的最终一致。

如果是单纯的时间戳+自增序列的版本号,那在一个两地数据同步延迟的时间窗口内(北京-上海平均 30 ms)的并发写请求,假设原数据版本号一致,那自增之后也将产生同样的版本号,同步数据时将出现冲突。为了解决这个问题,点评在版本号的最后引入了“数据源”(见上图),用来标识不同的地区。这样相当于人为的对不同的地区设置了一个优先级。在出现这种冲突时,数据源较大的数据将覆盖数据源较小的数据。从结果来看,确实达到了最终的数据一致。

这种在数据冲突时只保留一方的一致性设计,在账号业务中确实可以采用。一方面,账号业务的数据变化主要由会员或者运营人员触发,同个数据同时被更新的概率不高;另一方面,即使有一方的更新被覆盖了,也可以重新发起修改。

但是,如果想把这种设计推广到其他业务系统,比如库存,那就有问题了。比如对某个商品,上海和北京分别扣减了 1 个库存。如果最终只保留上海的扣减,丢掉了北京的扣减。虽然两边的数据达到了一致,但库存却少扣了 1 个,正确性丢失了。

总结下,点评通过对 DB 层的单点写和缓存层的双向同步,在对业务代码无侵入下,完成了对账号业务的双活升级。虽然 Redis 缓存的同步设计,有其业务系统的局限特点。但点评对数据层的一致性问题,区分各层特点分层来解决的思路,对我们之后的机房迁移方案也提供的另一个主要的参考。

2.3 京东

京东的双活方案,没有查阅到公开的文章,是在一次跟京东缓存负责人的技术交流中了解到的。京东的双活架构中,缓存层的技术方案跟点评有很大不同。点评是通过缓存层的双向同步来实现缓存数据一致,而京东则是通过缓存层监听 DB 层数据变更的 binlog,来实现 DB 数据变更时缓存数据的更新重建。

这个方案的优势很明显,只要 DB 层数据能保证一致,那依赖其重建的缓存层理论上也自然可以跟随其保证一致。这样就把两个数据层的一致性问题,缩减到 DB 一层。

然而,其引入的问题也同样显著:

  • 对 DB 的强依赖。在实际的电商业务系统设计中, 有一些非结构化的业务数据,比如购物车,可以利用 Redis 的持久化模式完全存储在缓存中。那在这样的双活设计下,这种业务系统就无法升级了。
  • 对业务代码的侵入和改造。一般缓存层的数据结构,不完全跟 DB 表一一对应,可能是由多个业务系统多张表的数据组装而成。这样的话,缓存数据的重建工具的开发和长期维护,将变成每个业务都不得不面对的问题。如果生生把缓存改造成表缓存,业务代码的改造量可能会非常高。

因此,京东的双活方案适用于使用表缓存+DB的业务系统。如果现有的业务系统不是这种模式,改造起来,工作量可能会很高。

3 我们怎么做?

如篇头所述,我们的双活技术路线是先把无锡机房的商城业务灰度迁移到上海公有云,然后在上海再做同城双活。这两个阶段,前者类似于异地双活,后者则是同城双活,技术方案有明显的不同,所以我分别来说明。

3.1 异地机房迁移

从无锡到上海的异地机房迁移,涉及到商城的所有领域:有买家维度的,有商品维度,也有卖家维度;有数据落 DB 的,也有数据纯缓存的。单纯的淘宝单元化或京东 binlog 触发缓存重建都不完全适用。因此,我们综合淘宝和点评的方案,在不侵入和改造业务代码的前提下,接入层按用户来源分发流量,缓存层一地双写+单向同步,来实现前端流量的灰度迁移。整体技术架构如下:

在这里插入图片描述
:图中蓝色实线代表用户请求触发的同步调用,蓝色虚线代表用户请求触发的异步系统调用,绿色虚线则代表数据同步及其方向。

我们按照前端用户流量的路径,从上层到下层依次看下每一层的关键改造:

  • 接入层:前端用户的流量经过 DNS 和 CDN 后,首先进入阿里云 WAF。 在 WAF 里,我们对前端用户流量进行分流,一部分进入到无锡 IDC,另一部分进入到上海公有云。灰度的过程,也就是无锡 IDC 流量逐渐迁移到上海公有云的过程。
    这里踩过的一个坑是 WAF 的负载均衡方式。WAF 支持轮询和按 IP Hash 两种方式。其实,为了流量能尽量按照预期的比例分流,采用轮询更为合理,因为轮询是以请求维度来做均衡的。然而,使用轮询的方式,在高并发的流量下会出现的问题是,其中一地的缓存更新后,另一地在同步时间窗内还未更新,用户请求两地打就可能会读到老的缓存数据。所以,为了最大限度将一个用户来源的流量闭环到一地,我们选择的是按 IP hash 的方式。也即是,我们在接入层,以 IP 为键来做流量的单元化。
  • 应用层:对于应用层,我们的大原则是尽量不要侵入和改造。因为涉及的微服务太多,改造起来工作量会非常大,进度也会变得不可控。所以,最终方案对于无状态的应用层透明,无需改动。
  • 数据层:对于有状态的数据层的改造,是整个技术方案的核心。前文也提到了,最大的挑战是,如何在保证可用性的同时,又不损失一致性。我们分 DB 和缓存两部分分别来看下。
    对于 DB 层,无锡到上海的物理距离不是很远(150公里左右),经过专线网络加速之后,两地的数据延迟在 5~7ms。并且,我们的业务特点也是读写比比较高(会员数据读写比 50:1)。而大量的读请求,基本都在缓存一层闭环,不会走到 DB。只有小比例的写请求,需要打到 DB。因此,在 DB 这一层,我们采用单主的方式。在业务流量灰度迁移期间,上海公有云的 DB 读写都经过专线回到无锡 IDC。为了数据容灾,上海公有云机房为每个无锡主库挂一个冷备的从库。在业务流量灰度完成后,DB 再按业务从无锡 IDC 切换到上海公有云,即主备做一个互换。
    对于缓存层,由于物理上 6ms 左右专线延迟的存在,缓存不可能采用 DB 一样单主的方式。因为缓存读写的平均耗时才 1ms 左右,如果每次读写都要加上 6 倍的网络延迟,那高速缓存就没有了意义。一个用户请求里的多次缓存读写累积后,耗时将不可接受。所以,缓存层采用的是两地各部署一个主集群,对业务服务的调用来说,各地读写各地的集群。为了防止用户 IP 切换导致的流量迁移,上海公有云的缓存更新会异步写到无锡 IDC,而无锡 IDC 的缓存更新则通过 Kafka 消息组件单向同步到上海。理论上讲,只要异步的更新和同步消息不丢失,缓存在一个同步延迟的时间窗后,可以达到最终一致。实际的运行中也发现,对于秒杀这种高并发的业务,也没有出现缓存数据不一致的问题。
    当然,这里也碰到一些很棘手的问题。比如,因为两地的缓存数据量都很大,而且一直存在一个同步时延。就导致,两地的缓存数据没办法在数据层做实时的对账和补偿。一旦出现专线网络长时间抖动,导致缓存数据的异步写或者同步消息大量堵塞和丢失,只能把流量重新切回到一地,利用 Redis 的同步机制把数据同步一致后,再继续灰度切分流量。

3.2 同城双活

在业务流量和 DB 都迁移到上海公有云机房后,就可以开始做同城双活了。前文提到,因为同城的优势,两个机房在逻辑上可以视为一个机房。应用集群、Redis 集群、MySQL 集群都可以均匀分布在两个机房,互为主备。一旦一个机房发生故障,只会影响一半的用户业务,而在另一个机房的备机升级为主后,业务和数据都可以恢复。整体的技术架构如下图:
在这里插入图片描述

4 回答开篇

到这里,整个的双活架构之路也就走完了。我们回过头来再看下开篇提的问题。在异地的机房迁移阶段,实际上用户请求已经分布到两地机房了,已经实现异地的“双活”了,为什么还要继续迁移做同城双活呢?
从上面的机房迁移的方案和整体架构图里可以看到,在这个阶段,为了保证业务数据的强一致,DB 实际上是单点的。一旦无锡机房出现故障,即使上海公有云有可读的缓存数据和可用的应用,也无法响应写的请求。所以,这个实际上并不是双活,而只是机房迁移过程中的一个中间阶段。

参考资料:

  1. 李运华:业务高可用的保障:异地多活架构
  2. 谢吉宝:阿里异地多活与同城双活的架构演进
  3. 沙堂堂 孟德鑫 杨正 谢可 徐升:大众点评账号业务高可用进阶之路

猜你喜欢

转载自blog.csdn.net/seahl/article/details/89067258
今日推荐