谈谈银行核心系统建设--篇6:核心系统的热点账户
文章来源:e路向上,文章仅供个人学习
篇6本打算谈谈核心系统的性能设计,如何支持高并发的,然则一来高并发设计并不只是核心系统专有,二则高并发支持更多与架构设计有关,未来在银行系统架构设计系列文章中再行探讨。对核心系统建设本身来说,如果架构已经支持弹性伸缩、服务进行了拆分、读写请求做了分离,再加上不差钱地堆计算资源(主动的降级、熔断、限流,估计大多数银行还做不到),一般来说就不会有性能问题,但有个问题,却常常成为核心系统建设过程中需要面对的问题–即热点账户问题。本文解决方案部分的内容引自笔者前蚂蚁同事登良的大作,经其授权发布,此致谢意。
什么是热点账户
银行系统中,一个账户接收到记账请求后,需要进行一系列操作:检查账户状态、检查账户余额(如果是扣账)、更新账户余额、登记交易记录、生成发送会计引擎的总账更新消息、并返回记账结果。此过程中,需要保证记账请求是幂等的、余额的更新必须准确且不能出现透支。对一般普通账户而言(比如我们的个人存款户),无论是普通转账、银证转账,还是缴费,对这个账户的记账请求都是低频的。但在银行业务中(其实不止银行,常见的还有支付机构的账户),有一类账户会出现短时间内有大量的“记账请求”集中在其上的场景,这一类账户就是“热点账户”。
普通账户记账可以通过单元化、分库分表等分布式技术提高并发量。对于热点账户来说,由于单个账户是不可拆分的最小原子单位,且发生的请求是记账请求,每一次的请求都需要准确更新“账户余额”,过程中需要保证下一次请求时的账户余额必须是准确的,因此绝不可以出现幻读幻写问题。为了保证准确性,记账请求处理过程中通常需要采用悲观锁(行锁),不可避免地导致海量并发的记账请求,出现交易排队情况,并快速填满系统最大可支持请求数,导致其他的交易请求阻塞,引起系统可用性故障。
典型的热点账户场景
热点账户与场景密切相关,常见的热点账户一般发生在企业结算户、备付金账户、需要实时更新余额的内部账户、Nostro及Vostro账户中,以下简单列举几种常见场景:平台企业的资金归集。如双11期间,海量的C端扣账资金归集到支付机构的备付金账户;二清业务中的平台资金清分给商户,有向平台企业提供二清服务的银行机构,需要为平台企业将资金从其主账户清分至商户账户;微信红包场景(特别是如除夕),资金在财付通备付金账户的海量入账、扣账;理财/国债/大额存单等高收益产品抢购时,如果使用了内部户分录规模,需要进行实时扣减控制的(不建议这么做,详见:浅谈银行内部户治理一文);跨境电商平台实时结汇,多币种资金频繁转入转出结算账户;银证转账,券商开立在银行的托管户的资金频繁清算等等。其共同特点都是同一时间对单一账户有海量并发的记账请求。
常见的解决方案
热点账户问题一直是金融业务系统的技术难题,为了应对这个问题,业内有很多不同的解决方案,常见的有异步汇总入账、账户拆分等方案等等。而技术能力更牛的大厂,在应用架构之外,更在最底层探索一些新的解决方案,例如微信(PaxosStore)和支付宝(Maxwell)分别引入一个新的存储系统来替换传统数据库的存储,从而大幅提高热点账户的记账性能。但引入额外的存储,复杂度和成本都比较高(其实主要还是大部分商业银行没这两下子–囧),本着能解决问题的方案就是好方案的逻辑,常见的解决方案思路有:1)、将记账处理在时间上进行分散。即在收到记账请求后,只是将请求保存下来就返回,记账请求分散到未来某个时间点进行处理。常见的处理方案是异步缓冲记账、定时汇总补账。这些处理方式需要在应用层引入更多的机制,如在余额检查上做更多设计,来防止账户透支。2)、将记账请求在空间上进行分散。单个账户是记账的原子单位,为将记账处理分散,需把热点账户拆分成多个子账户,从而把大量处理分散到多个物理资源上。这种处理方案可以提高记账性能并防止透支,但应用层需要设计子账户余额调度方案,且会降低账户的查询性能。3)、优化记账处理的性能,尽可能减少记账处理的时间。常见的优化方式有代码优化、锁优化和无锁方式、网络IO优化、磁盘IO优化,数据库优化、使用缓存数据库,对于并发量不是特别高的热点账户,进行这些优化后,在短期内通常都能满足需求。
热点账户的本质思考
实时记账的本质是什么?实时记账一定是同步记账吗?首先,这里所说的实时记账其实是从请求方出发来看的,即记账发起者调用账务系统的记账接口,接口返回时即表示记账成功或失败。而同步和异步记账是从服务方的角度来看的,同步记账指的是在同一个线程内完成请求受理和完成记账,异步记账则将记账请求和记账处理分离在不同的线程中处理。常见的误区是将实时记账等同于同步记账,将异步记账认为是非实时记账。其实,从请求方来看,只要在接口调用规定的时间内接收到了确切的记账结果,即可认为是实时记账,与同步还是异步没有关系。
记账请求是按什么顺序记账的?高并发场景下记账的顺序是否能严格保证?对同一个账户的记账,记账的顺序有两种类型,一种是依赖于调用方的逻辑控制(在同一个线程中调用前一个记账接口完成后才调用下一个记账),一种是依赖于自然时间(调用方在不同的线程分别调用记账,发起的时间有先后顺序)。对第一种情况,调用方在收到前一个记账的结果后才发起下一个,记账的顺序是严格保证的;对第二种情况,虽然两个记账请求在在自然时间上是有先后顺序的,但由于机器、网络等环境因素会造成各种各样的延迟,实际上的服务端接收到记账请求时间可能和发起时间是不一致的。对这种类型,由于接收记账的顺序无法保证,所以记账处理的时间就显得不是特别重要,只要在一个较粗的时间粒度上保证顺序就行了。记账的顺序通常会影响记账的结果(例如一个余额为0的账户同时进行相同金额的一增一减的两个请求),需要根据不同的业务场景制定不同的记账策略,例如对需尽可能保证记账成功的场景,执行先增后减。
记账处理的最高效方式是什么?从吞吐量来看,最高效的方式就是汇总记账,即先从数据库中获取账户锁和账户余额,在批量处理记账请求,更新最终余额,最后在更新到数据库中。汇总记账的问题是为非实时记账,可能造成记账流水丢失。
本文的参考方案
请求受理流程:
结合以上几个问题的思考,供参考热点账户方案主流程如下:Step1:账务系统接收到记账请求,将请求记录插入到数据库中,请求状态为待处理,请求流水号为唯一索引;Step2:将记账请求放入一个大小有限的待处理阻塞队列中(每个账号每个节点一个队列):1)、判断待处理队列是否已满,如果不满,则当前线程调用记账请求对象的wait()进入休眠;如果队列已满,则在当前线程中从队列批量抓取待处理的记账请求,进行汇总记账,记账完成后,对每个记账请求(当前请求除外)调用notify()唤醒原处理线程;2)、如果队列不满但到达一定时间(50ms)后,另一个线程从队列批量抓取待处理的记账请求,进行汇总记账,记账完成后,对每个记账请求调用notify()唤醒原处理线程;Step3:原记账处理线程唤醒后,更新请求记录状态为已处理,同时插入一条明细(可分库分表),然后返回记账结果给调用方。
汇总记账流程:
Step1:开启数据库事务,从数据库中获取账户锁和账户余额信息;Step2:处理记账请求列表,先处理余额增加的记账请求,再处理余额减少的记账请求,在内存中更新余额信息;Step3:将处理结果组装bitmap;Step4:更新数据库的余额信息,同时将bitmap保存到数据库中,提交数据库事务。
这里引入了两个bitmap,用流水号的前n位作为前缀,后m位决定了bitmap的大小。引入bitmap主要是为了在节点crash后,已经完成记账的请求能够根据bitmap更新记账明细。第一个bitmap表示记账请求是否已经处理,第二个表示记账请求是否成功。如果请求已处理就可以直接根据处理结果更新流水,如果请求未处理且判断请求已超时,就可以直接按失败处理。使用bitmap可以最大承兑减少汇总记账阶段的DB开销,需要注意的一点是请求的流水号需尽可能是连续的。
性能分析:
这个方案与Reactor模式有些相似,接受请求的线程把请求放入待处理队列后,如果队列未满就将自己阻塞,如果已满,本线程就会变成一个handler,从队列把所有请求取出,进行一次汇总记账,处理完后唤醒其他线程。当并发量大时,队列很快就会变满并触发记账,因为阻塞的时间很短,对调用方来说感觉就是一次实时记账。如果并发量不够大,或一段时间内的请求数不是队列大小的整数倍,可能没有请求线程能触发记账,这时就需要引入一个单独的线程负责定时触发记账,如果间隔的时间足够短,例如设为50ms,也能保证调用方收到返回的响应时间为50ms。由于汇总记账的耗时非常接近单笔记账的耗时,在不考虑线程切换开销的情况下,单账户的TPS提升的倍数可以接近队列的大小。考虑线程开销时,也可以很方便的对记账节点进行扩容,扩容后多个节点并行接收请求,只有在需要汇总记账时才从数据库竞争锁。测试中设置队列大小为1000,在同城灾备RPO=0的环境下,单账户实时记账的TPS轻松突破10万,在异地灾备的环境中,也能达到很高的性能。由于这个方案没有引入其他的中间件,只依赖于原来的关系型数据库,可靠性方面可以达到传统数据库的最高级别,成本也非常低,只有对记账处理的核心代码进行改造的成本。该方案增加了一种新的记账处理方法,任意账户都可以在新旧记账法中来回切换,当应用节点识别到某个账户请求量上升时,就可以直接切换到新的记账逻辑中,请求量下降时再切回到老的逻辑,切换过程中没有任何额外成本。
压测表现
使用阿里云ECS(8C / 16G)* 5,基于阿里云数据库RDS3节点版本(8C/16G),在并发线程5000的情况下,单账户节点TPS平均W,总TPS可支持10W+。
结语
热点账户是核心业务中常见的需要解决的行锁级别的高并发场景,高并发问题对技术人员来说,永远都是幸福的烦恼。排除因为基本的架构设计都做不好,应用代码写的太烂引起的问题外,性能问题的出现,通常意味着业务发展蒸蒸日上,而解决因此带来的性能问题,除了成就感爆棚外,通常还意味着,未来能多收三五斗,期待更多的“热点账户”….