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