支付宝账务热点架构解决方案


背景

2021年 5 月份的时候,我师兄告诉我,希望由我牵头为深圳国际支付团队做出一些技术上的改变和创新,思来想去,最终我们将目标聚焦到了国际账务系统的账务热点难题上。首先是出于业务层面的考虑,记账操作作为金融业务最核心,同时也是最基础的一项工作,如果能够在确保交易稳定性的同时又大幅度提升交易频率,这对研发团队和用户来讲都无疑是一件双赢的事情;其次,主站的账务团队在这方面已经积累了足够丰富经验,并且最终方案也成功通过了双 11 的终极大考。所以最终由我负责牵头国际账务团队,拜访了主站的账务团队,以及 Maxwell 时序计算引擎团队的相关负责人和核心同学,针对账务热点的解决方案展开了深入讨论,并希望能够借此机会完成对国际账务系统的架构进化。

传统实时记账面临的挑战

在正式开始本文之前,我们需要先了解一个事实,尽管 Alipay(支付宝)的全站峰值交易笔数可以达到惊人的 25w/s(2017 年的公开数据),但在传统的同步实时记账模式下,Alipay 单账户的交易笔数最多只能做到几十+/s,也就是说,单账户的交易笔数上限存在着严重的性能瓶颈,在务必优先确保记账准确性的前提下,这是一个世界级的难题,优化谈何容易
图1-热点账户
或许有些同学会感到不可思议,支付操作不就是简单的扣钱、加钱操作吗,为什么会导致这么低的吞吐量?在此大家需要注意,金融类业务,无论是流入、还是流出场景,都存在着一个最基础,同时也是最核心的工作,那就是记账。记账的本质除了需要实时反应账户余额的变化外,还需要满足来自上游的一些对账要求,以及会计层面提出的诸多要求。因此,在一笔交易的背后,账务系统除了需要更新目标账户余额数据外,还需要记录大量的额外信息。也就是说,一次记账操作,账务系统会在一个事务中同步执行几十条已经优化到极致的 SQL 语句,期间必然会伴随大量的网络 I/O、磁盘 I/O 等资源开销,并且为了确保记账操作的准确性,还必须对目标账户显式加上行锁(for update)。这样一来,在双 11 等高频交易场景下,活动力度大的商户账户就一定会成为热点账户,如图 1 所示,由于单位时间内账务系统只会处理目标账户的一个记账请求,而其它上游的应用层线程因为拿不到 DB 的行锁只能被迫选择长时间等待,RT 时间被放大,严重影响系统整体的 TPS 和用户支付体验,甚至还有可能导致系统出现雪崩。
图2-基础记账流程
抛开技术层面的问题来看,单纯讨论业务层面记账语义的复杂度也足以让人望而生畏。比如 UserA 向 UserB 转账 50 元,最终 UserA 的账户余额会减少 50 元,而 UserB 的账户余额会相应的增加 50 元,由于不同的用户账户信息可能会被落盘到不同的存储系统上,因此如果要确保分布式数据的一致性,就必须启用分布式事务,出于性能方面的考虑,在基于 TCC 的柔性事务中,我们需要考虑和解决诸如:幂等、事务悬挂、空回滚等一系列复杂问题,稍有差池就会影响记账操作的准确性,引发资损问题。如图 2 所示,在 Prepare 阶段的时候,账务系统并不会直接对用户账户余额进行实际的资金操作,而是先对目标账户的余额、状态进行 check,然后触发资金冻结流程,记录冻结金额和未到账金额,也就是说,Prepare 阶段的主要任务就是负责基础的资源预留;当顺利进入到 Confirm 阶段后,账务系统才会根据冻结表中记录的冻结资金来发起实际的账户余额变更,以及记录账务明细(比如:记账发生额、记账后余额等)和日终余额等数据;如果 Prepare 阶段执行失败,便会进入到 Cancle 阶段,Cancle 阶段的主要任务就是回滚 Prepare 阶段的分支事务,本质上就是删除冻结表中的冻结资金记录。

常规解决方案

为了解决双 11 等大促场景下单账户高频交易存在的性能问题,市面上也诞生出了诸多解决方案,比如拆分子账户、流控限制、汇总明细记账、缓冲记账等,但这些解决方案都是相对有损的,并没有从根本上解决问题。我们先来看看拆分子账户方案,简单来说,就是将热点账户拆分打散成多个子账户,以便于让记账流量均匀分布到各个子账户上,以此来提升记账效率,如图 3 所示。
图3-拆分子账户方案
虽然每一个子账户在物理上都是相互独立的,但从逻辑上来看仍然是一个整体。子账户最大的问题就是实现难度大,流出类场景要考虑到某个子账户余额不够时,多子账户合并扣款等复杂操作,记得当初在云集的时候,针对爆款商品的库存扣减操作我们就采用过类似的解决方案,只是在灰度环境下验证未达预期,最终只好回滚了此方案。
图4-汇总明细记账方案
而流控方案就不过多解释了,高频交易场景下带来的用户体验是非常糟糕的。而汇总明细记账和缓冲记账是业界采用得最多的 2 种账务热点解决方案,算是一种在效率和准确性、稳定性之间换取平衡的准实时记账手段。当采用汇总明细记账时,账务系统只会落明细数据,也就是说,只会负责执行 INSERT 语句的插入,而不会锁账户(FOR UPDATE)去更新余额,最终会通过一个定时任务来对明细数据 SUM 汇总后完成对指定账户余额的更新,如图 4 所示。
图5-缓冲记账方案
缓冲记账方案的思路就是将记账请求写入消息队列进行消峰处理,然后立即给上游返回 SUCCESS,待消费端根据后端的处理能力依次消费完成记账操作,如图 5 所示。在此大家需要注意,尽管汇总明细记账和缓冲记账都是准实时的记账操作,但却并不适用于所有场景。换句话说,这 2 个方案更多是应用在大商户(热点账户)的收单场景上,并且在活动开始前需要运营和研发同学提前配置热点账户,一但期间出现漏配、误配,那么仍然会产生热点问题;其次,记账操作由于会存在一定的数据延迟,所导致的直接后果就是商户无法实时看见余额的变更,这一点需要提前和商户沟通清楚;然而最严重的是,流出类业务在这种情况下必然会产生透支问题继而引发资损风险,因为账务系统在记账时并没有及时校验账户余额,只能在后续补账时发现。

Alipay 的解决方案

账务热点的核心问题就是对账户执行频繁的加/解锁操作导致的应用层线程大量 hang 住,并且事务的执行时间较长(在最坏的情况下,一个事务的 RT 时间可能会达到秒级),严重拖垮了全链路的支付效率。如图 6 所示,在一次记账事务中,业务层面首先会对目标账户显示加行锁(FOR UPDATE),基于 DB 的事务隔离策略,其它未拿到锁的事务则需要执行等待,而成功获取到锁的事务在执行期间,由于业务特性必然会导致产生大量的网络 I/O 请求和磁盘 I/O 请求,这些都是影响支付 RT 的关键因素。
图6-账务热点核心瓶颈
那么我们的核心改造点就主要集中在如下 2 点进行发力:

  • 去锁化;
  • 降低网络和磁盘 I/O 消耗;

以上改造点,如果是基于传统的 RDBMS 存储来实现几乎是不可能的,因此只能选择另辟蹊跷。2017 年我在设计云集的交易系统时,针对爆款商品的库存扣减其实也遇到了类似的问题,瓶颈点都是热点写,当时我们采用的解决方案就是将库存库从原先的 Mysql 切换为 Redis,同时开启 AOF/RDB 来解决数据容灾问题,至于容错方面则是通过搭建 Cluster、配置参数 cluster-require-full-coverage(不足 16384 个 slot 时 Cluster 仍可对外提供服务),以及控制最大内存等策略(哪怕 M/S 宕机也仅仅只是影响极小一部分数据区间不可用)等来提供支撑。而业务层面我们则选择在 Redis 中嵌入一套具备库存业务语义的 Lua 脚本(包括:获取库存、库存的记增、记减等),然后基于 Redis 自身的原子性来实现爆款商品的热点写优化。只是对于 Alipay 而言,之所以不采用 Redis 作为主力账务库,除了需要考虑极其严苛的稳定性因素外,更重要的是 Redis 在处理账务系统复杂的业务逻辑编排时成本较高。电商类场景和金融类场景所面临的问题维度和矛盾点不同,不可相提并论,但是它们的解决思路却天然存在着一定的相似性,这一点毋庸置疑。

而 Alipay 的最终解决方案就是自研一套账务库,并同时满足金融场景下所需的稳定性、高性能、原子性,以及支持嵌入“逻辑脚本”来实现业务功能的扩展,如图 7 所示。
图7-账户热点核心解决方案
看到这里,相信大家脑海中已经有思路了,其实核心的改造方案就是把基础的记账语义(包括但不限于:账户余额记增/记减、金额的冻结/解冻等)从账务系统中抽象出来,嵌入到一个支持原子操作的存储系统中,实现目标账户的去锁化,这关键的一步操作将会减少大量的应用层线程排队;然后再将记账语义 API 暴露给上游调用来减少 I/O 层面所带来的资源消耗。相对于传统实时记账的 I/O 密集型操作而言,由于基础的记账语义被打包成了一条命令(由存储系统负责将业务指令转换为相应的计算指令),因此在记账时上游主动触发的 I/O 请求将会有明显下降,从而换来的是大幅度的支付效率提升。
图8-主站Maxwell整体架构
Alipay 账务系统改造方案中所使用的账务库便是主站高性能团队为其量身定制的高性能时序计算引擎 Maxwell,如图 8 所示。

容灾/容错处理

目前 Alipay 并不会把所有的记账逻辑都迁移到 Maxwell 中,仅仅只是把基础的记账语义迁移到 Maxwell 中,哪个是主要矛盾就优先解决哪一个,因此其它的账务逻辑仍然是保持原有流程不变。出于对容错/容灾层面的考虑,Maxwell 中的余额数据在变更成功后会立即同步回 OceanBase 做结果备份,期间哪怕是 Maxwell 宕机,业务层面也能够通过开关快速切换回原有逻辑。
图9-数据一致性方案
如图 9 所示,Maxwell 和 OceanBase 异构系统之间的数据一致性则是通过 TCC 柔性事务来保证。在 Prepare 阶段的时候,账务系统并不会直接对用户账户余额进行实际的资金操作,而是先对 Maxwell 中目标账户的余额、状态进行 check,然后触发资金冻结流程,记录冻结金额和未到账金额,并同步写回 OceanBase 进行结果备份;当顺利进入到 Confirm 阶段后,账务系统才会根据冻结表中记录的冻结资金来发起实际的账户余额变更,并同步写回 OceanBase 进行结果备份,以及记录账务明细(比如:记账发生额、记账后余额等)和日终余额等数据;如果 Prepare 阶段执行失败,便会进入到 Cancle 阶段,Cancle 阶段的主要任务就是回滚 Prepare 阶段的分支事务,本质上就是删除冻结表中的冻结资金记录。

或许有些同学会存在疑问,为什么不把所有的记账逻辑都迁移到 Maxwell 中?基于目前的改造方案,单账户至少在很长一段时间内都不会再出现热点问题,至于未来,再看。

后记

关于 Alipay 账务热点架构解决方案就讲到这里,感兴趣的同学可以参考其他相关文献资料。如果在阅读过程中有任何疑问,欢迎在评论区留言参与讨论。


Author: gao_xianglong
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint policy. If reproduced, please indicate source gao_xianglong !
  TOC