查看原文
其他

超越“双十一” | ebay支付核心账务系统架构演进之路

贾奇 eBay技术荟 2022-03-15


(图源网络)

供稿 | Payments 贾奇

编辑 | 顾欣怡本文4289字,预计阅读时间14分钟更多干货请关注“eBay技术荟”公众号

 导读 

ebay支付新一代核心账务系统FAS(Financial Accounting System)自今年3月上线以来,帮助全球百万卖家处理交易合计超百亿条,日均实时处理线上千万支付流量,每日payout金额超千万美元。系统上线至今稳定运行,并成功做到“三无”:无一次数据丢失,无一次转账错误,无一次整体宕机。系统后台仅由5台普通计算机组成,对硬件资源要求极低,且几乎无需运维。本文旨在介绍该系统背后的架构演进,希望为大家解决同类问题提供一种不一样的思路。


 Part 01

业务背景


在正式介绍架构前,先简要看下业务流程,示意图如下:

图 1(点击可查看大图)

  1. 上游客户端往账务系统发出转账请求,从A账号往B账号转100元;
  2. 系统收到请求后,查看当前两个账号的余额,分别为100元和50元;

  3. A账号有足够的余额,转账开始,系统从A账号扣除100元,并在B账号加上100元。

业务逻辑足够简单,但在功能上要保证的一点是,对余额的查询、扣款、加款必须处于同一事务中,否则会造成多扣和少加的情况,对卖家造成无法估量的损失。后面每一次的架构升级都会严格保证这一点。此外,还有四大非功能性要求:
  1. 数据高可靠:账户余额等信息是极为重要的核心业务数据,绝对不能丢。
  2. 服务高可用:作为一项互联网服务,交易每时每刻都在发生,秒级别的不可用也将损害用户体验。

  3. 处理高吞吐:面对ebay主站百万卖家时刻都在产生的支付流量,要求系统能在短时间内处理大量请求。

  4. 数据高安全:必须具有防盗防篡改的数据保护机制(本文暂不作介绍)。


 Part 02

架构演进:从1.0到5.0


架构1.0


图 2(点击可查看大图)

图2是最简单的单进程单线程架构,所有账号的余额都在内存中。它有很多问题,其中最大问题是当进程退出后,余额信息就会永远丢失。
图 3(点击可查看大图)如图3所示,马上能想到的一个改进是每做一笔转账,就将最新的余额写到本地磁盘,这样即使进程退出,重启后依然能从磁盘恢复。但紧接着的问题是,如果这台机器的磁盘坏了,或者机器被恶意损毁,那么数据也就无法找回了。

图 4(点击可查看大图)

有经验的读者会想到把信息保存在数据库,数据库有主从备份,这样即使机器突然没了,换台机器连上数据库依然能提供服务(图4)。这里有个技术细节是,为了提高效率,一般备份是异步进行的,主备库之间的数据往往有延时,如果极端情况发生,主库突然挂了,主库中的最新数据在备库里是没有的,就可能发生多扣少加的情况。余额等信息是支付中极为重要的核心金融数据,绝不能丢。所以首先要解决的问题就是保证数据不丢失。


架构2.0


图 5(点击可查看大图)

架构2.0和1.0最大的区别在于采用了Raft共识算法[1]来做数据的实时备份。该算法保证只要集群中的多数派存活,数据就不会丢失。根据对数据可靠性的不同要求,有相应的部署方案。一般生产环境下采用三地五中心的部署,这样即使一个数据中心因为天灾人祸挂了,我们依然可以在剩下的两个数据中心找到数据。该架构最大的问题在于FAS进程(图5中蓝色框)是单点,一旦挂了,服务对上游就完全不可用。对于日均千万转账流量来说,将极大影响用户体验,完全不可接受。有同学可能会想到引入一个管理器(Process Manager),和FAS进程保持心跳,如果一段时间内收不到心跳,管理器就自动创建一个新的FAS进程。这样做的问题有两个,一是管理器本身就是单点;二是心跳超时不足以说明进程已经不存在。而多个FAS进程同时处理上游请求的问题在于无法保证对同一账户转账的事务性。到这里,首要挑战变成了系统的可用性。

架构3.0


提高系统可用性,首先要有多个FAS进程同时存在。把处理请求的FAS进程称为leader,其他称为follower。这个设计必须保证:
  1. 同一时刻只能有一个leader;
  2. 当发生failover时,新leader的状态(账户余额)必须和上任leader下台前达到完全一致,才能开始处理新的请求。

图 6(点击可查看大图)

我们创造性地提出了“双主合一”的设计,将业务处理和数据同步放到同一个进程中,很好地解决了第一个问题。这是因为Raft leader所在的进程,也必定是业务处理的leader,而Raft算法保证了多数派leader的唯一性。即使因为脑裂出现两个leader的情况,由于少数派的leader无法向多数派同步数据,对外部系统来说也是绝对安全的。以下是线程模型:
图 7(点击可查看大图)但是,该设计最明显的问题在于吞吐率极低。数据中心之间的时延决定了每秒处理能力的上限。一般跨数据中心的时延为100毫秒,处理上限每秒仅为10笔转账,无法接受。
图 8(点击可查看大图)架构3.1(图8)在处理线程和同步线程之间加入了队列线程。处理结果放入队列后直接开始处理下个请求。队列线程会按照一定的策略将队列中的数据打包(batch)发给Raft线程去做同步。这样做极大提高了处理线程和同步线程的吞吐率,从而提高了系统整体的吞吐率。值得一提的是,只有同步成功的请求才会通知客户端处理已完成。若在同步过程中发生了Raft选主,队列中的数据没有被多数派接受,客户端将会被通知处理失败,并开始重试。


架构4.0


接下来要解决的问题是当发生换主,如何让新leader的状态恢复到和前任leader下台前一致。为此,我们引入了一个基于领域驱动模型设计(Domain-Driven-Design)[2]和事件溯源(Event Sourcing)[3]的新编程模型(图9)。
图 9(点击可查看大图)该模型中整个FAS系统被设计成一个状态机(State Machine),账户余额代表了状态机的状态(State),来自上游的请求称为命令(Command),状态机对命令的执行结果称为事件(Event),事件为既定事实,一旦产生无法更改(Immutable)。所有的事件会以追加的方式(Append-only)写入到事件仓库(Event Store)。整个处理过程分为两步:
  1. Process:状态机处理命令,产生事件,即本次转账A账号减了100元,B账号加了100元。这一步不更新状态。
  2. Apply:根据事件更新状态。

需要指出的是,Apply是一个确定性操作,只要输入(当前状态和事件)相同,输出(新的状态)一定相同。根据该特性,新leader要恢复到和上任leader同样的状态,只需把仓库中的所有事件按序跑一遍Apply即可:......

公式 1

然而工程实现中我们并没有这么做,因为恢复时间(即服务不可用时间)也会相应变得很长:
  1. 恢复时间和事件数量成线性正相关。假设Apply一个事件耗时10微秒(us),一天产生一千万个事件,Apply一天的数据也需要2分钟。
  2. 本地磁盘空间有限,只会保留最近几天的数据,其余数据会被远程备份到数据仓库,考虑到下载,实际所需时间会更长。

我们首先使用快照(Snapshot)来缩短时间。系统定期把当前状态做成快照写到本地磁盘,恢复时,先加载最近一次快照,接着Apply后面的事件。优化后状态恢复所需时间为:

公式 2

快照加载时间和文件大小有关,假设磁盘读文件的速度是500MB/秒,读10GB的快照文件只需20秒。Apply所需时间和快照时间到当前的事件个数有关,不再与历史总数据量相关。由于快照保存在本地磁盘,也省去了网络下载时间。假设一天打一次快照,状态恢复只需2分钟。为了进一步缩短时间,我们引入了EventApplyLoop(EAL)线程,只要有新的事件写入事件仓库,EAL就会立即调用Apply更新状态。由于近实时更新,当新leader上台后,需要Apply的事件数量非常有限,毫秒级别即可完成状态恢复,上游几乎无感知。
图 10(点击可查看大图)一旦EAL将最新状态交付(swap)给新leader,便会开始自身状态的恢复过程(公式2)。


架构5.0


架构4.0基本上能满足本文刚开始提到的所有需求,也确实在预生产环境中跑了相当长一段时间……直到我们在数据迁移过程中遇到了内存问题(图11)。

图 11(点击可查看大图)由于状态都放在内存,系统跑得越久,内存占用越大。再加上EAL中还有一个状态机,实际内存使用是两倍。当跑了一年的数据后,内存被耗尽。为解决上述问题,我们对状态存储进行了分层设计(图12):
  1. EAL所Apply的事件来自于事件仓库,产生的状态更新直接写入RocksDB[4]
  2. CPL所Apply的事件包含尚未成功提交到事件仓库的部分,产生的状态属于临时状态,不会写入RocksDB,而是留在内存中。一来可以提高查询速度,二来如果换主导致事件无法写入仓库,状态可以直接丢弃。

  3. CPL在查询时,会先看内存中是否有相关数据,只有当不存在时才会查询RocksDB。

图 12(点击可查看大图)经过这一改造,不仅内存占用过大问题被彻底解决,而且还带来了一个额外的好处:可以在回归测试中使用Chaos Monkey[5]。Chaos Monkey是Netflix公司发明的一种测试分布式系统容错性的工具。这些monkey会在生产环境中随机挑选一个进程并杀掉,以此来测试系统是否足够健壮。我们将这一思想发扬光大,并加入了“暴走”模式,该模式下monkey会每20秒随机杀掉一个节点,平均100秒就会触发一次换主。只要新leader上台的状态恢复过程中有一点做得不对,就会导致处理结果和没有换主时的结果不同。加入了暴走模式的Chaos Monkey,不仅可以测试系统的健壮性,还验证了系统的正确性。但在架构4.0,我们并没有采用暴走模式,因为flip-flop情况下会造成服务不可用。所谓flip-flop就是当前leader下台后又马上上台,间隔时间在秒级。由于第二次上台后EAL还在恢复状态,在分钟级别,故造成了这段时间内无法处理新的请求(图13)。
图 13(点击可查看大图)架构5.0的状态保存在RocksDB,省去了从磁盘加载快照的时间,极大提高了状态恢复的速度,从而使得暴走模式成为可能。

图 14(点击可查看大图)

从图14可以看出,跨度为两天的压力测试,经历了800多次换主。一方面,流量丝毫没有受到换主影响,始终保持高可用;另一方面,数十万账号之间上亿笔的转账,只要有任何一笔处理有误,就会波及剩余大部分账号。我们将测试结果和预期结果进行一一比对,不仅比对每个账号上的余额,还按序比对每个账号每一次转账的事件,结果全部匹配。

 Part 03

未来工作


经工具分析,系统能力还有30~50%的提升空间。未来将对系统进行全面深入的优化,包括进一步简化业务逻辑,处理过程串行改并行,用Reliable UDP代替TCP来传输数据等。不久的将来,ebay支付将服务全球所有卖家,层出不穷的衍生业务也将带来更多流量。由于单个系统处理能力有上限,为了应对不断增长的流量,FAS整体架构必须具有动态扩容能力。我们在去年的百万TPS实验上已经证明了这一点,接下来的工作就是要对它进行打磨改进,正式用于生产环境。(详见超越“双十一”—— ebay百万TPS支付账务系统的设计与实现)FAS产生的数据可以作为其他业务的信息源(Source of Truth),例如各类金融报表、查询服务、智能预测等。这类业务种类繁多,需求千差万别,但都是基于上游FAS数据。我们将提供一整套服务+框架的解决方案,以一种安全、准确、有序、高效、简单的方式让这些应用接收并处理数据。FAS是一个分层架构,应用层和Raft层解耦。对于同类应用,只需替换应用层的业务逻辑即可。未来将支持更多的主流编程语言,让用户能以更低的成本开发业务逻辑。

 Part 04

结语


目前架构5.0已经在生产环境下稳定运行超过7个月。对上游,它就像单进程一样对外提供服务,却具有单进程所缺少的工业级的数据防丢失、服务高可用、高吞吐等特性。我们认为在不久的将来,在金融、医疗等领域,这类应用会成为一种新常态,我们也已对该项目开源[6](业务逻辑除外),希望可以为同类应用的开发带来帮助。

参考文献:

[1]https://raft.github.io/

[2]https://martinfowler.com/bliki/DomainDrivenDesign.html[3]https://martinfowler.com/eaaDev/EventSourcing.html[4]https://rocksdb.org/[5]https://github.com/Netflix/chaosmonkey[6]https://github.com/eBay/Gringofts

您可能还感兴趣:

超越“双十一”—— ebay百万TPS支付账务系统的设计与实现
平台迁移那些事 | eBay GC调优策略的实践平台迁移那些事 | eBay百亿级流量迁移策略分享 | eBay TESS,我心中的那朵“云”前沿 | BERT在eBay推荐系统中的实践eBay云计算“网”事|网络重传篇eBay云计算“网”事|网络丢包篇eBay云计算“网”事 | 网络超时篇数据之道 | Akka Actor及其在商业智能数据服务中的应用数据之道 | 属性图在增强分析平台中的实践数据之道 | SLA/SLE监控与告警
数据之道 | 进阶版Spark执行计划图
👇点击阅读原文,一键投递 

    eBay大量优质职位,等的就是你

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存