异地多活与单元化
背景介绍
名词解释
ldc logical data center
idc internet data center
ldc 是 idc 的进化版,是一种单元化部署方案。
扩展模式 vs 镜像模式
扩展模式是把服务/数据库分拆,然后部署到不同的机房里面,相当于放大了一个物理机房。
镜像模式是每个机房里部署的服务都是一样的,每个机房承担一定流量。
镜像模式的容灾效果更好,难度在如何切分流量上。容灾还要考虑机房级容灾、部署地容灾的问题。多地部署带来距离,距离带来延时,延时带来 replica 的风险。
单元化部署
所谓 cell,是一个能完成所有业务操作的自包含集合(每个单元,都是其他单元的镜像)。一般的 soa 架构,服务是分层的,而且每一层的任一节点都可以被其他机房调用。而单元化部署的结果是,本单元的上层节点,只会调用本单元的下层节点。它具有一个站点全部的功能,但不具有一个站点全部的流量。
这种单元化部署实际上就要求底层的数据也要做 sharding。单元化的结果是,数据库连接可以更好地被复用-多个单元互相跨 db 连接,其实很浪费资源。
单元化是按核心数据维度,对业务系统的部署及核心流量进行隔离,从而达到更灵活的扩展能力和更强的容灾能力的解决方案。
腾讯云在海量数据云存储上采用的术语叫 Multiple Availability Zone。
现实妥协
架构总有显示的黑暗面。
如何解决:
- 分拆解决不了全局数据依赖
- 分拆一定会带来异地时延
所以有些部署单元要全局冗余,然后对拷。
问题定义
高可用架构要去除单点的潜在威胁。
涉及多数据中心、多地部署的设计的意义和价值很难在日常工作中体现出来,但体现了超前布局的思考。
所谓的单点包括:
- 单服务器
- 单应用
- 单数据库
- 单机房
- 单地部署
多活架构
- 机房维度:机房越少、服务的部署就越受限,容灾的灵活度就越小。单机房最不灵活,因为冗余最少。每多一个机房,机房间容灾就越弱。但跨机房容灾部署很难保证一致性(全世界范围内,目前没有解决方案来解决这个问题)。
- 热备/冷备维度:热备对资源的利用率高,但要求有跨机房流量调配能力。
蚂蚁的单元化
所谓单元,是指一个能完成所有业务操作的自包含集合,在这个集合中包含了所有业务所需的所有服务,以及分配给这个单元的数据。
引入服务治理
引入服务治理机制,使整个服务可以在物理上灵活扩展。
强依赖:服务注册中心/服务命名+查找机制。
在阿里的语境里,是由服务注册中心的中间件来进行流量调配的。
分库分表
一个比较好的实践是:逻辑拆分先一步到位,物理拆分慢慢进行。
以账户表为例,将用户 ID 的末两位作为分片维度,可以在逻辑上将数据分成 100 份,一次性拆到 100 个分表中。这 100
个分表可以先位于同一个物理库中,随着系统的发展,逐步拆成 2 个、5 个、10 个,乃至 100
个物理库。数据访问中间件会屏蔽表与库的映射关系,应用层不必感知。
换言之,蚂蚁的数据总是分成 100 个逻辑分片。至于这 100 个逻辑分片是 100 张表,还是会膨胀为 100 个库 * x 张表,都可以由数据库中间件进行屏蔽,且自由扩展。
强依赖:数据库中间件,把逻辑读写转换为物理表读写。
同城多机房
所有机房共用一个服务注册中心,所有的请求由它统一分发
要突破单机房的容量限制,最直观的解决办法就是再建新的机房,机房之间通过专线连成同一个内部网络。应用可以部署一部分节点到第二个机房,数据库也可以将主备库交叉部署到不同的机房。
这一阶段,只是解决了机房容量不足的问题,两个机房逻辑上仍是一个整体。
缺点:
- 服务层逻辑上是无差别的应用节点,每一次 RPC 调用都有一半的概率跨机房;
- 每个特定的数据库主库只能位于一个机房,所以宏观上也一定有一半的数据库访问是跨机房的。
每个机房独占一个服务注册中心,所有的请求独立分发
改进后的同城多机房架构,依靠不同服务注册中心,将应用层逻辑隔离开。只要一笔请求进入一个机房,应用层就一定会在一个机房内处理完。当然,由于数据库主库只在其中一边,所以这个架构仍然不解决一半数据访问跨机房的问题。
这个架构下,只要在入口处调节进入两个机房的请求比例,就可以精确控制两个机房的负载比例。基于这个能力,可以实现全站蓝绿发布。
两地三中心
城市 1:idc1(registry1 + leader) + idc2(registry2 + replica1)。所有的服务都访问 leader 数据库。
城市 2:idc3(registry3 + replica2) 另一个城市为备份的中心,访问replica,隔离对 leader 的依赖。
可以概括为同城热备,异地冷备(蚂蚁就是采用这种方案,通常每个 idc 平摊流量)。两地三中心还有一种形态,就是只有一个中心是活的,另有一个同城灾备,一个异地灾备。
所谓 “ 双活 ” 或 “ 多 活 ” 数据中心,区别于 传统 数据中心 和 灾备中心的模式,前者多个或两个数据中心都处于运行当中,
运行相同的应用,具备同样的数据,能够提供跨中心业务负载均衡运行能力,实现持续的应用可用性和灾难备份能力,所以称为 “双活 ” 和 “ 多
活 ” ;后者是生产数据中心投入运行,灾备 数据中心处在不工作状态,只有当灾难发生时,生产数据中心瘫痪,灾备中心才启动。“两地三中心”是一种在金融系统中广泛应用的跨数据中心扩展与跨地区容灾部署模式,但也存在一些问题。异地灾备机房距离数据库主节点距离过远、访问耗时过长,异地备节点数据又不是强一致的,所以无法直接提供在线服务。
在扩展能力上,由于跨地区的备份中心不承载核心业务,不能解决核心业务跨地区扩展的问题;在成本上,灾备系统仅在容灾时使用,资源利用率低,成本较高;在容灾能力上,由于灾备系统冷备等待,容灾时可用性低,切换风险较大。
两地三中心看起来很美好,其实无法解决立刻切换的问题-异地延迟无法消除。
单元化
蚂蚁金服发展单元化架构的原始驱动力,可以概括为两句话:
- 异地多活容灾需求带来的数据访问耗时问题,量变引起质变;
- 数据库连接数瓶颈制约了整体水平扩展能力,危急存亡之秋。
单元化的设想,涉及到业务层和核心层的分离(单一系统的业务层和核心层,中台层的业务中台和核心中台):
单元化架构基于这样一种设想:如果应用层也能按照数据层相同的拆片维度,把整个请求链路收敛在一组服务器中,从应用层到数据层就可以组成一个封闭的单元。数据库只需要承载本单元的应用节点的请求,大大节省了连接数。“单元”可以作为一个相对独立整体来挪动,甚至可以把部分单元部署到异地去。
单元化有几个重要的设计原则:
- 核心业务必须是可分片的(有些业务是不可分片的:比如风控、营销的全局配置)
- 必须保证核心业务的分片是均衡的,比如支付宝用用户 ID 作分片维度
- 核心业务要尽量自包含,调用要尽量封闭
- 整个系统都要面向逻辑分区设计,而不是物理部署
逻辑上只分 10 个 RegionZone,每个 R Zone 承载 20 个逻辑分片。
注意,这种两地三中心的 ldc 之间存储的不是同构数据,而是异构数据,即一部分分片,这是一种自然而然的想法-现实中的 idc 通常全局都是同构的,且全局只有一个主同时存在。
进入三地五中心,正好每个中心一个 R Zone。
回到前面买早餐的例子,小王的 ID 是 12345666,分片号是 66,应该属于 Regional Zone 04;而张大妈 ID 是 yig54321233,分片号 33,应该属于 Regional Zone 02。
应用层会自动识别业务参数上的分片位,将请求发到正确的单元。业务设计上,我们会保证流水号的分片位跟付款用户的分片位保持一致,所以绝大部分微服务调用都会收敛在
Regional Zone 04 内部。但是转账操作一定会涉及到两个账户,很可能位于不同的单元。张大妈的账号就刚好位于另一个城市的 Regional Zone
02。当支付系统调用账务系统给张大妈的账号加钱的时候,就必须跨单元调用 Regional Zone 02
的账务服务。图中用红线表示耗时很长(几十毫秒级)的异地访问。
涉及多个账户的操作,完全可能跨单元。
城市级容灾的方案,强依赖于 OB 的选主、换主的机制:
这也意味着,不同城市的不同 idc能够承载多个 R zone的数据,进而激活多个 R zone 的中间件的流量切换规则。
一个城市整体故障的情况下,应用层流量通过规则的切换,由事先规划好的其他单元接管。
数据层则是依靠自研的基于 Paxos 协议的分布式数据库
OceanBase,自动把对应容灾单元的从节点选举为主节点,实现应用分片和数据分片继续收敛在同一单元的效果。我们之所以规划为“两地三中心”“三地五中心”这样的物理架构,实际上也是跟
OceanBase 的副本分布策略息息相关的。数据层异地多活,又是另一个宏大的课题了,以后可以专题分享,这里只简略提过。这样,借助单元化异地多活架构,才能实现开头展示的“26 秒完成城市级容灾切换”能力。
强依赖的技术组件:
- DNS 层(最顶层基础设施层)
- 反向代理层(子网网关)
- 网关 /WEB 层(接入层网关)
- 服务层(可再分为业务层和核心层)
- 数据访问层。
单元化流量管控是一个自上而下的、复杂的、系统性工程:
- DNS 层照理说感知不到任何业务层的信息,但我们做了一个优化叫“多域名技术”。比如 PC 端收银台的域名是 cashier.alipay.com,在系统已知一个用户数据属于哪个单元的情况下,就让其直接访问一个单独的域名,直接解析到对应的数据中心,避免了下层的跨机房转发。例如上图中的
cashiergtj.alipay.com,gtj 就是内部一个数据中心的编号。移动端也可以靠下发规则到客户端来实现类似的效果。- 反向代理层是基于 Nginx 二次开发的,后端系统在通过参数识别用户所属的单元之后,在 Cookie 中写入特定的标识。下次请求,反向代理层就可以识别,直接转发到对应的单元。
- 网关 /Web 层是应用上的第一道防线,是真正可以有业务逻辑的地方。在通用的 HTTP 拦截器中识别 Session 中的用户 ID 字段,如果不是本单元的请求,就 forward 到正确的单元。并在 Cookie 中写入标识,下次请求在反向代理层就可以正确转发。
- 服务层 RPC 框架和注册中心内置了对单元化能力的支持,可以根据请求参数,透明地找到正确单元的服务提供方。
- 数据访问层是最后的兜底保障,即使前面所有的防线都失败了,一笔请求进入了错误的单元,在访问数据库的时候也一定会去正确的库表,最多耗时变长,但绝对不会访问到错误的数据。
一般应用层或者业务中间件只能加上拦截器进行 forward/routing。
统一路由规则:
这么多的组件要协同工作,必须共享同一份规则配置信息。必须有一个全局的单元化规则管控中心来管理,并通过一个高效的配置中心下发到分布式环境中的所有节点。
规则的内容比较丰富,描述了城市、机房、逻辑单元的拓扑结构,更重要的是描述了分片 ID 与逻辑单元之间的映射关系。
服务注册中心内置了单元字段,所有的服务提供者节点都带有“逻辑单元”属性。不同机房的注册中心之间互相同步数据,最终所有服务消费者都知道每个逻辑单元的服务提供者有哪些。RPC
框架就可以根据需要选择调用目标。
注意看,上图的右边提供了一个细分的物理寻址的结构。
(注解放在接口定义上比较优雅)。
RPC
框架本身是不理解业务逻辑的,要想知道应该调哪个单元的服务,信息只能从业务参数中来。如果是从头设计的框架,可能直接约定某个固定的参数代表分片
ID,要求调用者必须传这个参数。但是单元化是在业务已经跑了好多年的情况下的架构改造,不可能让所有存量服务修改接口。要求调用者在调用远程服务之前把分片
ID 放到 ThreadLocal 中?这样也很不优雅,违背了 RPC 框架的透明原则。于是我们的解决方案是框架定义一个接口,由服务提供方给出一个实现类,描述如何从业务参数中获取分片
ID。服务提供方在接口上打注解,告诉框架实现类的路径。框架就可以在执行 RPC 调用的时候,根据注解的实现,从参数中截出分片
ID。再结合全局路由规则中分片 ID 与逻辑单元之间的映射关系,就知道该选择哪个单元的服务提供方了。
这里通过接口指定了注解的参数的契约,用注解来解耦了配置对流程的入侵。用配置来减少对原流程契约的改造,使服务成为整体框架的一个插件。
改造这些东西带来的工程经验,是书本上学不到的。
蚂蚁要改造的业务分别是:交易、收单、微贷、支付、账务。
不同的数据的延时性、闭合性不同,影响了 zone 的分法:
可以按照选择好的维度进行分区的数据,真正能被单元化的数据。这类数据通常在系统业务链路中处于核心位置,单元化建设最重要的目标实际上就是把这些数据处理好。比如订单数据、支付流水数据、账户数据等,都属于这一类型。 这类数据在系统中的占比越高,整体单元化的程度就越高,如果系统中全部都是这样的数据,那我们就能打造一个完美单元化的架构。不过现实中这种情况存在的可能性几乎为零,因为下面提到的两类数据,或多或少都会存在于系统当中。
不能被分区的数据,全局只能有一份。比较典型的是一些配置类数据,它们可能会被关键链路业务访问,但并不频繁,因此即使访问速度不够快,也不会对业务性能造成太大的影响。 因为不能分区,这类数据不能被部署在经典的单元中,必须创造一种非典型单元用以承载它们。
乍看与上面一类相似,但两者有一个显著的区别,即是否会被关键链路业务频繁访问。如果系统不追求异地部署,那么这个区别不会产生什么影响;但如果希望通过单元化获得多地多活的能力,这仅有的一点儿不同,会让对这两类数据的处理方式截然不同,后者所要消耗的成本和带来的复杂度都大幅增加。究其原因是异地部署所产生的网络时延问题。根据实际测试,在网络施工精细的前提下,相距约 2000 公里的 2 个机房,单向通信延时大约 20ms 左右,据此推算在国内任意两地部署的机房,之间延时在 30ms 上下。假如一笔业务需要 1 次异地机房的同步调用,就需要至少 60ms 的延时(请求去,响应回)。如果某个不能单元化的数据需要被关键业务频繁访问,而业务的大部分服务都部署在异地单元中,网络耗时 60ms 的调用在一笔业务中可能有个几十次,这就是说有可能用户点击一个按钮后,要等待数秒甚至数十秒,系统的服务性能被大幅拉低。这类数据的典型代表是会员数据(全体客户信息),对于支付宝这类 To C 的系统来说,几乎所有的业务都需要使用到会员信息,而会员数据却又是公共的。因为业务必然是双边的,会员数据是不能以用户维度分区的。
- Rzone:最符合理论上单元定义的 zone,每个 RZone 都是自包含的,拥有自己的数据,能完成所有业务。
- GZone:部署了不可拆分的数据和服务,这些数据或服务可能会被RZone依赖。GZone 在全局只有一组,数据仅有一份。
- CZone:同样部署了不可拆分的数据和服务,也会被 RZone 依赖。跟 GZone 不同的是,CZone 中的数据或服务会被 RZone 频繁访问,每一笔业务至少会访问一次;而 GZone 被 RZone 访问的频率则低的多。
RZone 是成组部署的,组内 A/B 集群互为备份,可随时调整 A/B 之间的流量比例。可以把一组 RZone
部署的任意机房中,包括异地机房,数据随着 zone 一起走。GZone 也是成组部署的,A/B 互备,同样可以调整流量。GZone 只有一组,必须部署在同一个城市中。
CZone 是一种很特殊的 zone,它是为了解决最让人头疼的异地延时问题而诞生的,可以说是支付宝单元化架构的一个创新。 CZone
解决这个问题的核心思想是:把数据搬到本地,并基于一个假设:大部分数据被创建(写入)和被使用(读取)之间是有时间差的:
把数据搬到本地:在某个机房创建或更新的公共数据,以增量的方式同步给异地所有机房,并且同步是双向的,也就是说在大多数时间,所有机房里的公共数据库,内容都是一样的。这就使得部署在任何城市的
RZone,都可以在本地访问公共数据,消除了跨地访问的影响。整个过程中唯一受到异地延时影响的,就只有数据同步,而这影响,也会被下面所说的时间差抹掉。时间差假设:举例说明,2 个用户分属两个不同的 RZone,分别部署在两地,用户 A 要给用户 B 做一笔转账,系统处理时必须同时拿到 A 和 B 的会员信息;而 B 是一个刚刚新建的用户,它创建后,其会员信息会进入它所在机房的公共数据库,然后再同步给 A 所在的机房。如果
A 发起转账的时候,B 的信息还没有同步给 A 的机房,这笔业务就会失败。时间差假设就是,对于 80%
以上的公共数据,这种情况不会发生,也就是说 B 的会员信息创建后,过了足够长的时间后,A 才会发起对 B 的转账。
总结:
- 分 AB (服务)组可以调配流量,也可以互为主备-是为多活。
- RZone 准备 AB (服务)组,每个 ldc 有且只有一组,共用一组 sharding 的单主数据库。
- 全局只有一组 GZone,在同城的一个 ldc 内(不跨城低延迟),只有一组单主的数据库(无分片),每个 ldc 有一个组(非 A 即 B)。
- CZone 在不同城市里多主部署(无分片数据),每个 ldc 有自己的一组,两地四中心则有 ABCD 四个(服务)组。
- 弱一致性带来弱依赖假设,我们可以接受主从延迟,大部分的交易依赖于同一个交易上下文,大家其实都是写后读交易。但少部分数据不是写后读的,适合放在 Czone 里。大部分情况下,弱一致性可以制造流畅的交易,但要对弱一致性准备兜底的重试方案,不然的话就会丢失交易(掉单)。RZone 和 CZone 存的都是交易流水数据,CZone 是一种技术创新。写后读的弱一致性问题在分布式场景下广泛存在,倒也不限于单元化。着眼点应该放在分布式系统本身上面。
- 如果业务使用的主要是没有前后依赖的流水型数据,则比较易于单元化改造;如果是状态型数据,则单元化改造难度较高。因为单元化总是会造成比较明显的时延,除非流程有办法禁写、禁读,否则无法保证交易无差错。业务系统必须对这种分布式不一致性有所防范,甚至引入校对工具。
- 因为单元化不能在完整地切换数据存储,且数据切换完毕以后再切换上层业务流量,则可能出现脑裂多写,数据不一致的情况。所以自底向上切服务,且有强一致性切换是很重要的。
- 如果没有强一致性切换,则数据在远程拷贝的时候要注意数据回环问题,使用校验工具做好校验。
容灾的基本步骤包括
自底向上激活、预热,切换流量,把有状态的服务先启动起来,然后启动无状态服务,然后切换其他中间件,最后切换流量调配规则。这样可以让慢的服务热到足够快,提供丝滑般的体验:
- 数据库切换
- 缓存容灾切换
- 多活规则切换
- 中间件切换
- 负载均衡切换
- 域名解析切换
基于 OB 的单元化
![OB灰度能力](OB灰度能力.png)
可以看出:
- CZone 也从 Rzone 拷贝数据,被 RZone 访问。这样说的话 CZone 存的也不是交易数据,而是不可拆分的全局数据(会员、风控)的低时延副本。
- GZone 和 RZone 使用独立的 OB 集群。
- 在三地五中心架构下,每个 RZone 在每个中心都有一个副本,冗余非常高。
- 容灾分为同城容灾和异地容灾:OB 切主的时候,单元的 Zone 也会切主。
单元化对不同的企业可以有不同的实现
- 城商行大 GZone 模式:即把城商行的所有服务和数据不做拆分,直接装入一个GZone内,在GZone的基础上实现同城双活即应用同城双中心部署,数据库同城三中心部署。
- 区域银行 RZone 模式:即将这家区域银行的主要业务拆分成两个逻辑业务单元两个分片,将其装入一个城市的两个IDC内,在另外一个城市建设冷备,其数据库每个分片实现5副本部署,其中4副本在主城市两个中心内部署,1副本部署在了本机房内。该架构实现了同城容灾能力,同时也实现了细粒度的灰度能力和弹性能力,但同样无法实现异地容灾能力。
另一种 Set 化方案
- 每个的 Set 的入口就部署在各个城市之中,这是由 CDN 和接入层机房决定的。
- 接入层管理所有的流量,这样可以让属于某个 Set 的流量进入特定的 Set(换言之,凡是有可能带有跨 Set 可能的系统边界上,流量调拨的服务都应该是跨 Set 的)
- 每个 Set 之间完全镜像。
- Set 路由服务也部署在全局数据和全局服务之前。
- Set 之间也做数据同步。
- 结算和红包和每个 Set 单独部署的。
- 所有可能出 Set 和入 Set 的流量,都需要中间层。
- 行业内常见的思路无非是异地冷备或者异地多活,异地冷备浪费资源,而且切换不一定平滑。活是相对于备而言的。
- 中国人民银行的2号令,第三十二条:支付机构应当具备必要的技术⼿段,确保支付指令的完整性、 ⼀致性和不可抵赖性,支付业务处理的及时性、准确性和支付业务的安全性。具备灾难恢复处理能⼒和应急处理能⼒,确保支付业务的连续性。
- 只要存在异步的拷贝数据的方案,就一定无法解决延迟带来的数据不一致性问题。到底数据不一致对业务带来多少影响,要仔细评估写后写类业务自身的情况。
- 切换时要业务停写,然后切换路由规则,真正切流量的时候,强一致性业务,要做好数据校验。
- 退款、运营等后置、异步流程,可以容忍低活。元数据服务可以部署城市级 Set。
- 增加 Set 可能出现 100% 的资源增加的情况。
- 业务改造要改造:
- 识别流量中的分片标识
- 接入路由 SDK
- 存储层要加 Set 列
- 接入支持 Set 的中间件
- 要有故障切换和流量管控平台。
- 理论上一个 Set 只能属于一个机房,一个机房可以部署多个 Set(一个物理 idc 可以映射多个逻辑 ldc)。
- 这种架构里面,流量最大,对可用性要求最高的就是接入层服务、网关服务(其实还包括日志服务)。
- 在存储层没有准备好的情况下,流量层 -> 存储层 -> 单个业务到全链路 -> 异地。
- 在有多个上游的时候,下游服务可以考虑跟上游的分片规则解耦。解耦的话,上下游不用相互配合改造。
- 有一些柔性 FO 的方案,可以让一部分数据放进黑名单里,不影响其他数据的可用性。
- Set 化架构要求服务和数据的分区隔离,于是老服务都必须进行一定的改造才能适应Set化架构,订单功能包括用户SET和商家SET的功能,所以订单的服务相对来说有很多特殊性的地方。
- 如果商家信息和用户信息属于不同的信息类型,使用不同的订单中心,需要考虑下单时跨 Set 的问题,至少有两种跨 Set:用户在 A Set 向 B Set 下单,用户从 A Set 去到 B Set,然后向 B Set 的商家下单。
- 有些店铺属于大连锁,所以可能会产生两套数据,门店数据会 Set 化,大连锁数据不 Set 化。
- 如果交易有多维度查询的需求,可能有 id 维度写,然后从用户、商家和大连锁维度各自的副本进行分片和查询。
- Set 间的数据按理来说是要彼此隔离的,但 id 的全局唯一性还是要仔细考虑的,只有这样做,才能够预防迁移(特别是容灾导致的迁移)。除此之外,还可以有:重新生成主键并建立 mapping;容灾时独立写入;使用 step 分段模式生成独立的 leaf 号段。
- 给数据打标/染色的方式很多,逐一改表是最麻烦的方法,简单的基础方法是给流量染色,异步地流量染色可以考虑把标记写进 id 的号段里面。一个例子:48 位 set 内自增 id + 4 位预留 + 6 位商家 Set + 6 位用户 Set。但,即使实现了流量染色,可能还是不可避免地要改表。改不改表,可以作为一个区分方案工程量的标准。一个好的 id 生成算法,能够精确地确定时+空+自增,其中时要明确时间精度,空要确认-单元-城市-集群-机房-链路-库等不同的颗粒度,自增要保证在特定的时间里的并发可以被满足。对于例子里的架构,原始的集群和机房的关系是单集群多机房。
- 为了兼容 Set 化和非 Set 化的上游服务,所有请求在进入下游之前,要先通过路由服务。用中间层来制造变量设计,解决常量设计的问题。
- 跨域消息的兼容性最好的实现方案是:跨域 set 复制-这也是蚂蚁的跨 zone 的基础方案。复制转发的最简单方式是通过消费到了用户的消息再通过自构建RPC的方式进行转发(模仿 client 是最简单的消费复制方式,另一种方式是在 broker 里改造,这两种方法都不需要改造 producer,充分利用解耦来减少对生产方的改造,而且可以充分利用 mq 的堆积性和持久性)。
- 订单数据改造特别重要,基本方式有:在 SQL 上添加 SET 标识的注释,databus 解析 binlog 感知到了进行同步;只拉取关键数据的 binlog,通过组装方式查询获取;所有数据加上版本,依靠版本确认最终一致;延迟敏感型业务使用同步调用同步,其他情况下用 MQ 同步。不同步数据,无法解决跨 Set 查询的问题-这个问题有两个基本解决思路:跨 Set 路由查询;复制其他 Set 数据到本地。
- Set 化是采用业务属性进行划分,如果这个属性没有很好的 Set 属性,那么 Set 化收益将不复存在。
- 中心服务是一种特殊的 Set。如果这种 Set 支持其他 Set 的 fallback,则这种 Set 本身可能被 fallback 打爆。有一类架构方案认为,中心 Set 应该处理 Set 标丢失的流量。
- 读扩展是有限的,MySQL 的从库扩充不能超过 20 台。
- 单机房容量是有限的。扩展受限,意味着容量受限和冗余受限。
- 如果大量 idc 的主库集中在同一个 IDC,机房故障的时候,可能导致大量的主从切换。主从切换产生的脑裂问题很难在业务层面解决,只有在数据层自己解决。换言之,部署的灵活性是由切换方案决定的。
- 单机-单服务-单数据库-单地域,机构的拓扑结构变复杂一倍,就要引入新的中间层中间件,让架构变量化程度更高一点。多地域的 Set 化,最终导致我们要对接入层进行隔离改造。
- 机房的隔离,是解决机房容量全局共享的不二法则。
- 一般 Set 的互备,一定要基于数据拷贝。
- 阿里只单元化了买家链路(按照用户 Id 拆分),而卖家链路没有单元化,这可能和流量的特点有关系。而蚂蚁则完全进行了单元化。阿里使用 MySQL 加 DRC 进行单元化(这证明 MySQL 没有很好的远程拷贝方案);而蚂蚁使用 OB,直接强一致性可切。
- 单元化的具体方案本身高度依赖于业务特征(流量和数据划分规则,这是业务系统改造要回答的首要问题,这个问题没有完美答案,任何一种规则都有它不能解决的边界问题),也需要公司基础服务和组件进行改造,所以没有同行的技术路径,只有一些基础要领。
- 外卖类业务的划分规则要遵循其强 LBS 属性的核心特征,交易方在一个地域内,容易形成闭环。
- 用户应该属于二级 region,二级 region 动态地分配在 Set 中,所以 region 是流量标识里面的常量部分。各层通过 region 和 Set 的动态绑定,把流量路由到特定的 Set 中。
- 数据如果做分 Set 冗余,则需要考虑是不是做全量冗余;冗余越多,成本越高,但可切换性越高。所以最终设计出来的方案,几冗余几分片,非常重要。
- 要解决基于 binlog 产生的数据回环,有一种做法是给 binlog 写注释,注明本操作源于哪个 set,让 client 过滤,还有第二种方案是给数据表加两个列,一个表明 insert_set,一列表明 update_set。
- 跨 set 订单的基本逻辑是,先写远端 Set,然后会写回本 Set,如果 mq 出问题,降级到 rpc,主动同步。这样,交易实际上可以在远端做,也最终不影响本 Set 的数据闭环。
高可用中牺牲多少一致性
- 幂等总是难以保证的,柔性设计会牺牲一致性。
- 不柔性的设计,强一致性的设计,会增加切换的难度。
- 牺牲一致性,意味着有些单会在脑裂中出错。什么样的错误单可以被容忍?有异步化流程,或者可以事后通过对账补偿的业务,可以被容忍,这是流水型业务。余额、库存类实时生效的关键交易因子产生的错误,是不可以被容忍的,这是状态型业务。这也意味着,这类业务在做高可用的时候,必须评估集群切换对业务的破坏。强一致性业务不能容忍有损运行。流水型业务和状态型业务的区别是,到底写是不是会影响写后写,写后写依赖的全局状态到底有多深和广。
LiteSet
有些公司会把灰度链路叫作 LiteSet。LiteSet 要求全链路的 Tracer 传递完全正确(有一些方案会传入“gray-release-xxx”相关的流量标记字段进 cell 里)。它作为链路粒度的流量隔离方案(基本不支持存储层隔离的方案,还支持 fallback 回中心链路),多用于全链路灰度、按业务线流量隔离部署、全链路压测(这样真的是全链路压测吗?)等,需要围绕流量在中间件里的流动,建造单独的监控体系。与此相比还有一种泳道方案,只在线下测试环境里面做链路灰度隔离。set 化方案和泳道方案本质上是一种方案,前者偏稳定,固若金汤;后者偏灵活,随用随建随销毁,因此也能衍生出 LiteSet 这个新方案。
参考文献: