这是阿里技术2022年的第38篇文章
( 本文阅读时间:10分钟 )
阿里很早就开启了线上交易应用部署单元化,因为交易单元化部署要比简单的应用服务单元化难度大很多,首先要涉及到写操作,其次交易时涉及到用户的多项资产的写,例如交易下单会涉及到商品信息(价格)、买家信息(积分)、优惠信息(优惠券)、库存等多重信息,商家可能会修改商品的价格、库存数量等,这些信息本身就是多点写的,控制不好就会出现数据一致性问题。
将交易数据完全按照某一维度进行单元化的拆分比较难,所以尽量让交易的强依赖的数据做到单元闭环,若依赖的数据做跨单元异步调用,降低交易链路的复杂性是必须的一个选择。
01
几类单元化思路的比较
最早在讨论单元化方案时有几种方案,结合在一些其他公司了解到的方案,我把它归结为4种单元化方案,分别都有一些优缺点,需要根据公司的实际情况进行选择。
1.1 主备机房模式
主备机房模式是最容易想到的单元化模式,就像存储系统的主备模式一样,也可以把整个机房的系统在另一个地方做一份备份系统,早期在青岛的机房有过这个模式的实际应用,这种模式有其优缺点:
- 优点:应用、存储改造小,架构简单易操作。大部分存储系统都支持主备模式的切换,所以在另外一个机房做一个备份系统就可以,当然如果机房相距太远还需要考虑延时问题,不能做同步写,所以可能有一定的丢数据风险。
- 缺点:成本浪费严重,可用性存疑。这个方案虽然简单,但也存在一些问题,备份机房的机器基本上要和主机房的配置一样,要具备随时切换的能力,但这个备份的机房平时是不用的,存在很大的浪费。
另外这种模式还存在一个问题,数据库等一些成熟的基础系统的备库功能已经比较成熟稳定,但应用系统由于日常需要大量的代码变更和测试,所以要保持备份机房的代码稳定性,备份机房的维护成本也比较高。平时机房里的应用没有流量,即使代码同步更新,但是应用出现问题时也不容易被暴露出来,所以需要做机房流量切换时,备份机房能不能正常工作也是一个疑问。由于成本浪费、维护成本高而且稳定性也保障困难,最后未采用这种模式。
1.2 根据业务数据进行单元划分
这种是真正的流量的单元化,即根据访问用户的流量进行切片和划分,分别导到不同的机房,尽量做到每个机房的流量能够闭环,这样不仅能做到容量的扩展性,也能进行机房的容灾。按照访问者的角色又能分为两种进行划分:消费者和商家。
✪ 1.2.1 消费者维度单元化
消费者访问的数据主要包括商品数据、交易订单数据以及个人权益相关数据,其中商品数据主要是读,而交易下单和个人权益可能涉及到数据的写。读的数据比较好解决,跨单元主要解决数据写的一致性。
交易订单数据可以说是消费者维度最重要的写的数据,所以把消费者按照一定的维度进行切分到每个机房,归属于这个机房的消费者交易下单写的数据就落在这个机房中,然后再把每个机房的数据汇总到中心机房,中心机房再把其他单元机房的全量数据复制回单元机房。这种模式也有其优缺点:
- 优点:按照用户进行划分比较合理,一旦出现问题可以最大程度保证交易能够完成;流量可以根据用户进行灵活调配,可以兼顾容量的扩容和单元容灾。
- 缺点:牺牲商家体验,商家的数据需要汇总,有一定的延时;商家的数据的一致性不好保障。
✪ 1.2.2 商家维度单元化
交易订单数据既涉及到买家也涉及到卖家,我们可以按照商品归属的卖家进行单元切分,然后让交易订单跟着商品走,商品在哪个机房消费者就到哪个机房下单,订单就在哪个机房。
- 优点:商家数据进行单元划分,商品数据的一致性和实时性容易保障。
- 缺点:按照商品维护划分,有些商品可能是热点商品,导致机房容量不好均匀;用户进行跨商家下单时,存在跨机房数据的读写问题,影响消费者体验;同时消费者数据的一致性也不好保障。
从上面两种思路的分析可以看出,由于订单数据聚合了商家和消费者的数据,所以无论按哪种维度进行划分,都会影响另外一方的体验和数据一致性,从购物的体验出发,消费者频度更高、影响更大,所以保障消费者体验,选择消费者维度单元化更合理。
1.3 就近访问单元化
根据用户访问的地理位置就近单元化也是一个思路,例如把全球划分成东亚、欧洲、北美和南亚四个区域,每个区域的消费就近访问这个区域的机房。这种模式是在前面的消费者维度单元化基础上,按照消费者的地理位置再进行划分。这种模式比较适合适合国际化业务和打车等业务。
- 优点:按照城市维度进行逻辑划分比较简单,用户就近访问体验较好,尤其是跨国情况下。
- 缺点:用户数据漂移和跨区域流动较难处理;用户流量调度不太灵活,也会出现区域热点容量问题。
1.4 计算资源单元化
还一个场景是解决计算容量的问题,只把应用层进行单元化,存储层仍然集中在一个地点。应用层尤其是一些离线数据需要大量消耗计算资源或网络带宽,这种情况下把机房建设到电力较便宜的地方是个合理的选择。这种方式是把大量计算中间数据存储在本地,然后把计算结果数据统一存在其他地点,所以只解决了一部分问题。
- 优点:技术实现简单,可以解决技术资源的容量问题,适合有大量计算的业务。
- 缺点:单元之间不封闭,不能解决容灾问题,只能解决容量问题;用户体验也可能较差。
总结一下,无论是哪种单元化方式,单元最重要的是要解决两个问题:第一是解决容量问题,第二是解决容灾问题。对于交易型系统来说,主要是要兼顾用户体验和数据一致性,尤其对写链路的一致性要求非常高。
02
交易实现单元封闭的难点
如前所述,交易型业务要实现单元化,最重要的是保障写链路的单元化,而写链路最核心的就是交易链路,所以要保障交易在一个单元内闭环,就要把交易依赖的数据尽量在单元内闭环。
交易涉及到的数据非常多,重点看交易中涉及的写数据哪些是新产生的数据,哪些是需要变更的数据。新增的数据较好解决,难的是变更的数据,库存作为交易中强依赖的重要数据,被多买家并发扣减,同时卖家又有增加库存的操作。
要解决库存数据变更就涉及到两个问题,一个是保证单点写,另一个就是数据的最终一致性。
2.1 单点写
要保证数据的一致,最好的方式就是保证数据只在一个地方修改,控制住数据变更的源头,否则就会出现多点写导致数据相互覆盖的问题,因此如何保证数据的单点写就成了关键。
以库存为例,一个商品的库存会有多个买家来并发减库存,同时卖家也会变更库存。这种情况下,都到一个地方完成变更会更好控制,所以当前的交易单元化方案中,所有减库存的请求都到中心机房扣减。虽然交易本身实现了单元化,但是交易的减库存要跨单元在中心机房扣减,其实是实现了半单元化,交易并没有在单元内封闭。
为了保证单点写,还要对所有的请求进行路由。因为按照单元流量进行切分,用户的请求一开始并不是都到正确的机房,所以在单元内需要对请求进行服务路由,例如库存写服务是从单元机房路由到中心机房进行数据库操作。
2.2 数据一致性
想做到交易的单元封闭,那就必须打破数据的单点写,需要进行一定维度的拆分,要进行数据的拆分。比如数据需要在多个机房和多个数据库之间保证数据的一致性,则要引入分布式事务来保障,对性能就会有很大的挑战。
另外由于数据被拆分,比如存在总量和各单元分量之间的数据计算,还有单元之间的扣减不平衡带来的跨单元数据的调拨,这种情况下数据的移动都会存在数据一致性问题。
03
为什么当前
可以做和必须做这件事
前面介绍了这些难点,导致我们要想实现真正的交易单元封闭,真正做到多机房的交易容灾,那么就必须做出突破。
3.1 单点写的瓶颈越来越大
单元化13年启动后,双11交易订单峰值从开始到现在翻了十几倍。直播将库存高并发推到了一个新的高峰,不仅是大促,平时的直播就有可能发生大热点,高并发场景也很容易出问题,单行单库的性能很容易达到瓶颈。
3.2 交易没有单元封闭仍不能起到容灾效果
做单元化时一个很重要的目的就是解决交易容量和容灾问题,尤其是容灾,当发生极端情况时能保住最重要的交易核心下单能力。虽然交易链路上一些非核心应用可以进行降级,但是减库存却不能降级,而减库存是中心机房模式,所以完整的交易下单不能在一个单元内封闭,必须进行跨单元调用,最核心的交易也就起不到容灾的效果,如果库存不能实现单元化部署,严格的来说单元化只做了一半。
3.3 降低交易链路的延时
如前面所述,完整的交易下单需要进行跨单元库存的扣减,会增加整个交易下单的RT,而减库存是强依赖,必须同步调用,所以减库存的RT很难被优化,如果是批量下单的RT会更明显。
3.4 为什么当前必须要做
前面介绍了一些库存没做单元化存在的问题,这么多年一直没做,是否说明这些问题也不是非常致命?其实从本质上来看,如果最坏的情况发生,我们是否愿意为了承担那个后果而去付出成本。除了成本之外还有2个因素:
库存单元化本身有一定技术难度,涉及到交易最核心链路的修改,有点像修改发动机,技术难度大、风险高,吸引着大家去克服这个挑战。
技术人都有一颗追求完美架构的心,遇到不完美的架构或者bug,不会去想做这个事值不值得,有没有好处,而是觉得就应该去做,可能这也是一种技术文化。
04
库存单元化的整体思路
库存要实现单元化部署有两种方案:
- 一种是按照商品维护进行划分,即一部分商品在一个机房进行交易,而库存和商品所在的机房绑定,商品在哪库存扣减就在哪,这样也能起到容灾的效果。一个机房出现问题也只会影响到这部分商品交易下单。
- 另一种是把商品的库存数拆成多份,每个机房分一部分数量,分别在每个机房扣减,总数等于库存数。
由于目前我们的整体的单元化架构是按照用户维度进行的单元化,所以第一种方案不合适,所以将采用第二种方案来实现库存的单元化部署。
库存单元化的整体思路看起来较为简单,就是把一个商品的总库存行按照单元机房拆分成多个单元库存行,每个单元机房的交易下单只减自己本单元的库存行,用户交易订单和库存扣减单据在同一个单元内,不需要跨单元调用。每个单元库存行库存数相加等于总库存行的库存数。卖家编辑库存时只编辑中心单元的库存行值,再按照一定的规则分配到其他单元库存行。
我们仍然把某一个单元库存设置为中心单元库存的概念,当某个单元库存行库存数扣完后,再到中心库存行去调拨,如果中心库存行也扣完,就会把单元库存行全部回收,直到把所有单元库存全部扣完为止。
单元库存表在每个机房之间的数据是双向同步的,虽然表之间是双向同步,但里面的单行是单向同步,因为要保证单行的数据只能在一个单元写,例如单元E的行数据只能在单元E写,这行的数据会单向同步到中心单元C的单元E行。总库存行表是中心单元向其他单元单向同步,总行数据是在中心单元一个计算模块把各单元的库存数据进行汇总写入,再同步到其他单元。
05
需要克服哪些难点?
为什么说库存单元化有一定的技术难度?目前电商业界还没有实现过真正的交易减库存单元化,如果能做到就是业界的一大突破,其中需要解决几大难题。
5.1 单元库存的初始预测分配
我们把一个总库存拆分成多个单元库存,每个单元只扣本单元的库存,由于每个单元的下单量不一定一样,可能会出现有些单元库存先扣完,进而走到调拨的链路,这样会影响性能,所以初始的库存分配就是一个关键。
影响单元库存扣减量的最大因素就是流量,而当前流量的分配没有明显特征,所以不用考虑流量转化效率的差别,因此初始单元库存的分配比例可以按照单元机房的流量比例进行分配。
5.2 跨单元库存怎么调拨
划分单元库存时很可能出现的一个场景就是当A单元机房的库存扣完了,而该商品的总库存还有时,A单元的用户应该还要能够购买。这种情况下,应该允许从中心单元C调拨库存过来完成这次下单。而要能很好地完成调拨,可能要考虑多种情况:
1)单元库存数完全不满足用户要扣减的数据
例如用户要扣减2件,单元库存数已经是0了,怎么去调拨?这种场景比较简单,用户的请求数据到中心单元去代扣,由于用户请求是在单元发生,所以用户的库存扣减单据记录保存在单元机房(扣减单据类似于订单信息,用户后续对账等),要扣减中心单元的库存,需要完成以下几个调拨动作:
- 中心单元的库存要出库用户请求的库存数,并生产出库单据
- 单元库存要生成入库单据
- 单元库生成用户的扣减单据
由于最终扣减的数量就是中心单元出库的数据,所以单元机房的库存行实际没有增加和扣减库存数据。
2)单元库存数部分满足用户要扣减的数据
例如用户要扣减2件,单元库存数还剩1件了,怎么去调拨?这种情况和上面类似,只不过这种情况下数量都是去中心单元扣减,到中心单元去调拨2件过来,中心单元的出库和单元机房的入口单据是2件,用户扣减单据是2件。这样做也是为了简化扣减逻辑,避免操作更多的数据,引起数据的计算和不一致性。
3)中心单元部分满足单元机房的调拨数量
前面2种需要到中心单元去调拨库存,当中心单元满足要调拨的数量时是没问题的,但是当中心单元也不满足或者部分不满足要调拨数量时怎么办?这个时候就要触发中心单元回收单元库存。
回收库存的逻辑和调拨的逻辑也有点类似,就是单元机房的库存需要先记录出库单据,然后中心单元记录入库单据,如果再需要调拨给单元机房去扣减,这个过程就类似于上面的调拨逻辑。