深入剖析 | ALIPAY 账务热点架构解决方案
背景
去年 5 月份的时候,我师兄告诉我,希望由我牵头为深圳国际支付团队做出一些技术上的改变和创新,思来想去,最终我们将目标聚焦到了国际账务系统的账务热点难题上。首先是出于业务层面的考虑,记账操作作为金融业务最核心,同时也是最基础的一项工作,如果能够在确保交易稳定性的同时又大幅度提升交易频率,这对研发团队和用户来讲都无疑是一件双赢的事情;其次,主站的账务团队在这方面已经积累了足够丰富经验,并且最终方案也成功通过了双 11 的终极大考。所以最终由我负责牵头国际账务团队,拜访了主站的账务团队,以及 Maxwell 时序计算引擎团队的相关负责人和核心同学,针对账务热点的解决方案展开了深入讨论,并希望能够借此机会完成对国际账务系统的架构进化。
传统实时记账面临的挑战
在正式开始本文之前,我们需要先了解一个事实,尽管 Alipay(支付宝)的全站峰值交易笔数可以达到惊人的 25w/s(2017 年的公开数据),但在传统的同步实时记账模式下,Alipay 单账户的交易笔数最多只能做到几十+/s,也就是说,单账户的交易笔数上限存在着严重的性能瓶颈,在务必优先确保记账准确性的前提下,这是一个世界级的难题,优化谈何容易。
或许有些同学会感到不可思议,支付操作不就是简单的扣钱、加钱操作吗,为什么会导致这么低的吞吐量?在此大家需要注意,金融类业务,无论是流入、还是流出场景,都存在着一个最基础,同时也是最核心的工作,那就是记账。记账的本质除了需要实时反应账户余额的变化外,还需要满足来自上游的一些对账要求,以及会计层面提出的诸多要求。因此,在一笔交易的背后,账务系统除了需要更新目标账户余额数据外,还需要记录大量的额外信息。也就是说,一次记账操作,账务系统会在一个事务中同步执行几十条已经优化到极致的 SQL 语句,期间必然会伴随大量的网络 I/O、磁盘 I/O 等资源开销,并且为了确保记账操作的准确性,还必须对目标账户显式加上行锁(for update)。这样一来,在双 11 等高频交易场景下,活动力度大的商户账户就一定会成为热点账户,如图 1 所示,由于单位时间内账务系统只会处理目标账户的一个记账请求,而其它上游的应用层线程因为拿不到 DB 的行锁只能被迫选择长时间等待,RT 时间被放大,严重影响系统整体的 TPS 和用户支付体验,甚至还有可能导致系统出现雪崩。
抛开技术层面的问题来看,单纯讨论业务层面记账语义的复杂度也足以让人望而生畏。比如 UserA 向 UserB 转账 50 元,最终 UserA 的账户余额会减少 50 元,而 UserB 的账户余额会相应的增加 50 元,由于不同的用户账户信息可能会被落盘到不同的存储系统上,因此如果要确保分布式数据的一致性,就必须启用分布式事务,出于性能方面的考虑,在基于 TCC 的柔性事务中,我们需要考虑和解决诸如:幂等、事务悬挂、空回滚等一系列复杂问题,稍有差池就会影响记账操作的准确性,引发资损问题。如图 2 所示,在 Prepare 阶段的时候,账务系统并不会直接对用户账户余额进行实际的资金操作,而是先对目标账户的余额、状态进行 check,然后触发资金冻结流程,记录冻结金额和未到账金额,也就是说,Prepare 阶段的主要任务就是负责基础的资源预留;当顺利进入到 Confirm 阶段后,账务系统才会根据冻结表中记录的冻结资金来发起实际的账户余额变更,以及记录账务明细(比如:记账发生额、记账后余额等)和日终余额等数据;如果 Prepare 阶段执行失败,便会进入到 Cancle 阶段,Cancle 阶段的主要任务就是回滚 Prepare 阶段的分支事务,本质上就是删除冻结表中的冻结资金记录。
常规解决方案
为了解决双 11 等大促场景下单账户高频交易存在的性能问题,市面上也诞生出了诸多解决方案,比如拆分子账户、流控限制、汇总明细记账、缓冲记账等,但这些解决方案都是相对有损的,并没有从根本上解决问题。我们先来看看拆分子账户方案,简单来说,就是将热点账户拆分打散成多个子账户,以便于让记账流量均匀分布到各个子账户上,以此来提升记账效率,如图 3 所示。
虽然每一个子账户在物理上都是相互独立的,但从逻辑上来看仍然是一个整体。子账户最大的问题就是实现难度大,流出类场景要考虑到某个子账户余额不够时,多子账户合并扣款等复杂操作,记得当初在云集的时候,针对爆款商品的库存扣减操作我们就采用过类似的解决方案,只是在灰度环境下验证未达预期,最终只好回滚了此方案。
而流控方案就不过多解释了,高频交易场景下带来的用户体验是非常糟糕的。而汇总明细记账和缓冲记账是业界采用得最多的 2 种账务热点解决方案,算是一种在效率和准确性、稳定性之间换取平衡的准实时记账手段。当采用汇总明细记账时,账务系统只会落明细数据,也就是说,只会负责执行 INSERT 语句的插入,而不会锁账户(FOR UPDATE)去更新余额,最终会通过一个定时任务来对明细数据 SUM 汇总后完成对指定账户余额的更新,如图 4 所示。
缓冲记账方案的思路就是将记账请求写入消息队列进行消峰处理,然后立即给上游返回 SUCCESS,待消费端根据后端的处理能力依次消费完成记账操作,如图 5 所示。在此大家需要注意,尽管汇总明细记账和缓冲记账都是准实时的记账操作,但却并不适用于所有场景。换句话说,这 2 个方案更多是应用在大商户(热点账户)的收单场景上,并且在活动开始前需要运营和研发同学提前配置热点账户,一但期间出现漏配、误配,那么仍然会产生热点问题;其次,记账操作由于会存在一定的数据延迟,所导致的直接后果就是商户无法实时看见余额的变更,这一点需要提前和商户沟通清楚;然而最严重的是,流出类业务在这种情况下必然会产生透支问题继而引发资损风险,因为账务系统在记账时并没有及时校验账户余额,只能在后续补账时发现。
无锁化+降低 I/O 频率
账务热点的核心问题就是对账户执行频繁的加/解锁操作导致的应用层线程大量 hang 住,并且事务的执行时间较长,严重拖垮了全链路的支付效率。那么我们的核心改造点就集中在去锁化和降低网络 I/O 和磁盘 I/O 消耗上,那具体应该怎么实现呢?2017 年我在设计云集的交易系统时,针对爆款商品的库存扣减其实也遇到了类似的问题,瓶颈点都是热点写,当时我们采用的解决方案就是在 Redis 中嵌入一套具备库存扣减语义的 Lua 脚本(包括:取库存、库存的记增、记减等),然后基于 Redis 自身的原子性来实现爆款商品的热点写优化。只是对 Alipay 而言,之所以不采用 Redis 作为账务库,除了需要考虑稳定性因素外,最重要的是 Redis 无法有效支撑账务系统复杂的业务逻辑编排,电商类场景和金融类场景所面临的问题维度和矛盾点不同,不可相提并论,但是它们的解决思路却天然存在着一定的相似性,这一点毋庸置疑。
看到这里,相信大家脑海中已经有思路了,其实核心的改造方案就是把基础的记账语义从账务系统中抽象出来,嵌入到一个支持 CPU 单核串行执行指令的存储系统中,然后将记账语义 API 暴露给上游调用。相对于传统实时记账的高频 I/O 交互模式,由于基础的记账语义被打包成了一条命令,因此在基础记账时上游仅需触发一次网络 I/O 交互,从整体来看,大幅度减少了网络 I/O 和磁盘 I/O 所带来的资源消耗,提升了整体的支付效率。Alipay 账务系统改造方案中所使用的账务库便是主站高性能团队为其量身定制的高性能时序计算引擎 Maxwell,如图 6 所示。
核心改造点
这里我们不能把 Maxwell 看做是一个和 OceanBase 一样的通用业务的存储引擎,至少目前它的应用场景是和记账业务强绑定的,所以 Maxwell 在对外宣传时,都会将自己标榜为 Alipay 高性能账务库。那么我们究竟需要从账务系统中抽象出哪些记账语义逻辑呢?简单来说,基础记账语义包括:账户余额记增/记减、金额的冻结/解冻等。
如图 7 所示,目前 Alipay 并不会把所有的记账逻辑都迁移到 Maxwell 中,哪个是主要矛盾就优先解决哪一个,因此其它的账务逻辑仍然是保持原有流程不变。出于对容错/容灾层面的考虑,Maxwell 中的余额数据在变更成功后会立即同步回 OceanBase 账务库做结果备份,而 Maxwell 和 OceanBase 之间的数据一致性则是通过 TCC 柔性事务来保证。
后记
关于 Alipay 账务热点架构解决方案就讲到这里,感兴趣的同学可以参考其他相关文献资料。如果在阅读过程中有任何疑问,欢迎在评论区留言参与讨论。
拓展阅读
推荐文章:
版权声明: 本文为 InfoQ 作者【九叔(高翔龙)】的原创文章。
原文链接:【http://xie.infoq.cn/article/736572214af0e9061afdca0e0】。文章转载请联系作者。
评论 (2 条评论)