背景介绍
亿贝 (www.ebay.com) 于 2018 年全面展开了下一代支付系统的设计和实现。支付宝的双十一和微信财付通的春节红包已经向世界展示了支付行业当前的工程成果。那么作为下一代支付系统,如何才能做得更好?支付宝的支付系统能支撑双十一期间高达几十万的并发量,我们的支付系统有没有可能支持得更高、更稳定?俗话说擒贼先擒王,支付系统最重要的组件之一是账务系统,有着极其严格的业务正确性和稳定性要求,我们便以此为突破点,开展了攻坚项目。
我们支付团队在 2018 年底接到项目后对支付行业做了充分的调研,首先确定了下一代账务系统的设计目标:
业务目标
支持所有支付业务功能
扩展接口支持所有互金功能
容灾目标:
三地五中心部署,数据中心级别容灾
数据毫秒级实时异地同步备份,业务秒级异地主备切换
性能目标:
自动线性扩容
有能力承接全球所有支付公司峰值流量之和
数据目标:
实时数据中台
具备一定的区块链所具有的数据防篡改能力
接下来我们开展了为期半年的系统设计和原型实现。2019 年,第一季度原型系统通过验收,下一代核心账务系统正式立项。经过半年紧锣密鼓的开发,系统于今年第三季度正式灰度上线,并通过了性能和容灾验收。
本文主要介绍核心账务系统的性能和容灾能力,包含以下内容:
核心账务系统简介
百万TPS压测实验
系统架构分析
开源计划
核心账务系统简介
业务简介
支付系统为账户间的资金流动提供系统性的解决方案。支付系统在处理转账业务的时候需要根据当前账户资金及流水情况对资金流动的金额、方向、业务合理性等做必要的业务校验。所有这些核心转账业务和所有账户的资金及流水的维护均由核心账务系统负责。支付系统几乎所有的支付能力均建立在核心账务系统之上,因此核心账务系统是最重要的系统之一。
系统简介
账户资金是账户当前的状态,每一笔资金流动都会对这些状态进行查询和修改。当用户数和业务数超过一定规模后,单台机器就无法满足系统的性价比要求,因此互联网行业普遍采用了集群化的多机解决方案。但多机方案会遇到很多单机方案所没有的挑战:
如何将状态划分至多台机器
多台机器之间的连接出现问题时如何正确维护状态
单个数据中心出现故障时如何保障业务的正确性和稳定性
跨城数据备份如何处理网络延时问题
系统如何快速地扩容和缩容
如何保证分布在多节点上的数据不会被恶意篡改
核心账务系统还面临着很多其它的挑战,这些挑战会随着系统流量的增加而变得更加严峻。当系统出现量变时,我们需要寻求质变。接下来让我们看一看下一代的核心账务系统如何应对量变和质变的挑战。
百万 TPS 压测实验
实验目标
下一代核心账务系统的首要设计目标是更好地支持电商业务。电商业务通常会面临秒杀等短时超大规模用户请求,此时系统需要具备瞬时线性扩容能力来确保用户体验。为此我们设计了百万 TPS 压力测试来验收下一代核心账务系统的相关能力。
实验意义
支付宝的双十一和微信的春节红包等项目在十万 TPS 的问题上已经有了成熟的解决方案。但业界对于百万甚至更高 TPS 的问题尚无代表性的系统案例。百万 TPS 意味着支付宝、微信财付通、国际卡组织、国际第三方支付等全球所有支付系统的线上峰值流量之和。由于支付业务与人类活动有关,存在物理上限,解决了百万 TPS 的问题意味着一劳永逸地解决了账务系统的基础架构问题。
另外,系统性能数量级的提升往往意味着架构的迭代更新,我们借此机会就老的问题提出新的解法,希望这些新的想法能应用于更多其它系统,解决更多其它行业的问题。
实验流程
实验分为两个部分:第一部分验证单组节点的性能,业务场景为单商户秒杀业务,系统验证点为单组节点能支持的最高 TPS;第二部分验证多节点集群的性能,业务场景为主站大促,系统验证点为当 TPS 请求逐步上升时系统的自动扩容能力。
系统准备
核心账务系统在正式生产环境中的部署方式为三地五中心,本次压测采用了简化版的两地三中心部署。每组为 3 个节点,跨城部署在 2 个数据中心。压测共准备 667 组,合计 2,001 个业务节点。周边辅助节点为 2,350 个。一共准备 4,351 个节点。这些节点总带宽为 128Gbps,总 cpu core 数量为 14,767 个,总内存为 45T,总硬盘存储空间为 84T。所有硬件消耗 3 个机柜,共 300 台物理机,总成本为 450 万美元/3 年。
实验结果
单组节点测试
水电煤缴费、用户还款、电商秒杀等业务均有大量用户需要在指定时间点对单一账户做资金转入操作。由于单个账户已无法再做分库分表处理,单组节点的性能上限就决定了单个账户的交易量上限。因此,我们需要实施单组节点压测,以了解单个账户的限制情况。
两地三中心结果
在这个实验中,三个数据节点部署在两个异地数据中心,通过基于 Raft 共识算法的强一致性协议进行实时数据同步。这是最常见的一种部署方案,兼顾了容灾和业务处理响应时间。从上图的测试结果看,1KB 的请求,每秒最高能处理的交易量是 7,800 笔。
值得一提的是,当达到峰值后,由于请求接收阶段的处理能力已到上限,P99(第 99 个百分位数)下单个转账请求的延时明显增大。而批处理日志写入阶段单次打包的日志数量明显上升,部分抵消了海量请求带来的影响,P90 下延时的增大幅度则相对较小。
在整个测试过程中发生了一次 Raft 选主,导致了 TPS 的降低。这是因为新主上台前需要将自身的业务状态追到最新,才能处理新的转账请求。和传统基于 Raft 协议的应用相比(例如 KVStore),这是支付应用较为独特的地方。
同城三中心结果
本次实验为同机房三节点部署。从上图可以看到,系统最高每秒可处理 9,500 笔交易。由于数据复制过程中只要有一个从节点(Raft follower)成功写入日志即可认为交易完成,而同数据中心内主从之间数据复制的时延仅为跨数据中心的 1/12 到 1/8,所以每秒交易数相应增加。该效果与异地三中心部署时主从在同一数据中心的效果一致。另一点值得指出的是,由于同数据中心内网络和跨数据中心相比更稳定,在整个实验过程中没有出现选主,TPS 更加平稳。
单中心结果
本次实验采用单节点部署,来测试数据复制对交易吞吐量的影响。和前两次相比,该部署由于只要主节点(也是唯一的一个 Raft 节点)本地写入成功即可认为交易完成,省去了数据复制的时间。从上图可以看到,该部署下每秒交易能稳定在 9,700 笔,最高能处理将近 1 万 1 千笔。可见,单节点部署和同机房三节点部署的系统吞吐能力几乎一致,这在一定程度上归功于我们在提升网络吞吐量方面作的优化。
多组节点测试
在实际应用中,单组节点往往无法满足大型电商平台对总交易量的实时性要求,因此需要部署多组节点,只让每组节点承担一部分账户的交易请求。我们测试了系统在两个典型的业务场景下的表现。
流量阶梯上升,节点数量动态调整
该实验模拟的业务场景是主站日常流量涨跌,交易量会随着某些外部事件而发生动态变化,例如全站大促时,网站流量会随着人们作息而发生缓慢震荡。
从上图可以看出,实验开始只有 222 组节点,当系统监测到单组节点的平均吞吐量开始上升并超过阈值时(意味着交易量上升,即将达到并超过单组节点的处理能力),系统会自动部署新的节点来分担总体交易量。伴随着节点数量的上升,平均吞吐量开始缓慢下降到阈值以下,同时保证总体交易量能被及时处理。该弹性扩容过程示意如下:
系统能动态调整节点数以自适应流量的变化,保证业务不受影响的同时也可以提高硬件的利用率。例如当交易量较低时,可以通过合并节点来减少资源。
瞬时超高流量,节点数量不变
该实验模拟的是主站大促,例如每年双 11 的零点,流量洪峰会在极短的时间内涌向支付系统。我们分别测试了系统在瞬间收到 50 万和 100 万交易量的极端情况下的表现。
50 万
100 万
从图中可以看出,在这两种情况下,系统均能从容稳定应对,虽然其中部分节点出现了换主,但对总体流量的影响微乎其微。测试过程中甚至还发现延时有短暂下降,这主要是由于批处理阶段打包效率提升。
在达到峰值流量时我们还随机查看了部分节点的资源使用情况,发现 CPU 并没有被打满,部分线程并未满负荷运作。这说明流水线上各阶段的处理能力强弱不一,后续调优后系统应该会有更好的表现。
系统架构分析
整体架构设计
核心设计原则
在解决超大流量时,电商系统及支付系统目前最流行的解决方案是通过分库分表来做流量切分,通过分布式事务解决分库分表带来的分布式一致性的问题。这是一个经过了实践检验的解决方案。但是近十年来经过开源社区和各大互联网厂商的不断努力,数据系统的设计有了长足的发展,涌现出了更多更好的解决方案。由于我们是从零开始设计一个新的系统,没有历史负担,所以决定应用更为领先的系统设计来解决核心账务系统所面临的业务和系统复杂度问题。
业务复杂度处理
金融系统是一个典型的业务复杂系统。我们计算过,对于信用卡业务来说,一共有 573,099,840 种不同的业务场景。传统解决方案是所有系统直接基于数据库表来进行开发。传统方案的问题在于数据库存储的是数据的二进制格式,没有业务逻辑,系统在使用数据库的时候需要基于二进制形式来重构业务逻辑,正确性难以保障。
所幸随着本世纪初领域驱动设计(Domain Driven Design)模式的出现,金融系统复杂度得到了系统性的解决。我们首先使用了领域模型来对金融业务建模,使得数据的业务表现层和存储层分离。其次使用 Event Sourcing 来保证业务处理的正确性和对历史状态的百分百可追溯可还原。再者使用 CQRS 来实现系统读写分离。最后用流处理的方式自上而下、从外及内地统一所有设计。
多机事务问题
系统在做水平扩容时需要处理数据正确性问题。传统的方式一般采用分布式事务。我们基于账务系统的特殊业务逻辑提出了基于消息最终一致性和业务补偿的解决方案。
多机方案除了正确性外还有一些其它需要注意的问题。首先是多了至少一次的网络开销,增加了请求延时。其次,系统内部流量翻番,单机提供的业务 TPS 减半。最后由于基于消息最终一致性的实现无法同步返回结果给调用者,系统需要增加异步转同步的组件。
单机事务问题
多机事务需要每台机器都具备本地事务的能力。传统数据库为了提高事务的执行性能普遍采取了多线程的并发执行机制。但是本世纪初的一些研究发现,对于有些业务场景,特别是金融行业,多线程并非最优解决方案。因此在核心账务系统的设计过程中,我们确保核心账务业务逻辑只在单线程执行,以便充分利用单线程执行所带来的事务保证和性能优势。
单线程带来的另一个好处是线性一致性(Linearizability)。事务只能保证多任务按照某种顺序来串行化执行(Serializability),在有多种可选顺序的情况下,事务调度算法并不保证每次均能选择同一种确定顺序。和事务的一致性不同,线性一致性能确保每次调度的顺序完全一致。账务系统要求所有记账均按照一个确定的顺序记录,在发生系统回滚及重放后需要恢复至相同顺序,因此线性一致性是一个对于账务系统正确性来说非常重要的保证。
系统性能
电商及支付系统通常采用基于SOA的节点间服务调用和基于 Java Bean 的节点内组件调用。但是在出现秒杀等场景时需要做一些特殊的削峰填谷工作。削峰填谷通常使用消息队列来做缓冲,因此消息队列也成为了一个必不可少的组件。
与经典设计中所采用的部分异步实现不一样,我们在设计时全面使用了基于消息的异步解决方案,包括系统间和系统内的功能调用。异步方案带来的挑战是没有同步接口,提高了调用方复杂度,因此需要利用前文提到的异步转同步组件来降低调用方复杂度。
系统容灾
金融系统对容灾有极高的要求。首先是数据需要支持同城和异地备份,确保数据有区域级灾备能力。再者需要支持业务的快速自动主备切换,时间就是金钱。
传统方案是基于数据库自带的异地备份,分为异步和同步两种备份方式:
如果是异步备份,由于数据并没有被及时同步至备份节点,主节点出问题时会有数据丢失。
如果是同步备份,则主备机必须同时在线,任何一台出现问题都会导致业务中断。备机越多,当中任何一台出问题的概率越大,业务中断的概率也越大,因此同步备份的问题在于数据容灾能力越强,系统越不稳定。
我们采用了新的方式来达到同时提高数据容灾和系统稳定性的效果。新的方法和同步备份一样需要备机同步返回备份结果,但只要求大部分返回即可,不需要全部返回。这个方法称为共识算法族(consensus)。我们选取了其中的 Raft 算法,从零开始实现自己的高效定制化的强一致性算法。
账务系统的核心数据会通过强一致性算法实时同步至 3 个数据中心的 5 个存储节点。系统只要有多于一半的节点能正常工作便能整体正常运行,因此该部署计划允许 5 个节点中任意 2 个节点同时出现问题。由于单个数据中心出现问题时最多只能影响 2 个节点,因此该部署计划能支持数据中心级别的灾备要求。该部署俗称三地五中心部署。三地五中心的简化版为两地三中心,也是我们在此次压力测试中所采用的模型,如下图所示:
我们地采用了双层(核心业务层和基础架构层)一体的设计来实现业务的快速自动主备切换。当数据出现灾备切换时,定制化的 Raft 算法会同时切换数据和业务逻辑至灾备节点。
为了能更好地证明系统的容灾能力,我们在生产环境部署了定时自动容灾演练系统。该系统会不定时的对系统进行随机的可用性攻击,从而验证生产环境中面对网络、硬盘、内存等出现异常情况时系统的容忍能力。
分层设计
核心账务系统从上至下分为 4 层:
账务业务层
基础账务层
基础架构层
存储层
另外还有一个纵向贯穿始终的监控层,负责收集监控数据和跨层监控。架构示意图:
最上层的账务业务层提供外部访问接口,负责处理最上层的业务逻辑,有一定的对外 I/O 请求,内部无状态。业务会被分解为多个条件转账请求发给下层的基础账务层。基础账务层负责记账,对外无 I/O 请求,内部有状态。该状态由基础架构层提供的状态机来维护。基础架构层负责提供一个基于 Raft 算法的高效稳定的状态机实现,其性能由单线程保证,稳定性由强一致性算法来保证。基础账务层处理完的结果为会计账本,会输出给存储层。完整的分层架构图如下图所示:
Event Sourcing
原理
基础账务层和基础架构层是根据 Event Sourcing 的原则来设计的,这也是核心账务系统最重要的架构设计。Event Sourcing 有 4 个主要组成部分:
Command:外部请求
Event:事件
State:状态
State Machine:状态机
所有外部请求会先被发送给状态机。状态机会结合当前状态生成事件。事件被存入事件仓库(Event Store)后会被状态机执行,进而改变状态机内的状态。如下图所示:
下图展示了多个事件处理时的时序图:
举一个账务系统的例子。账务系统接收到的一个命令是「从甲转乙 100 美金,但甲不能出现余额不足的情况」。当前状态为「甲有 150 美金,乙有 50 美金」。状态机在收到命令后检查当前状态情况,确认转账后不会出现甲余额不足的情况,因此生成事件「甲转出 100 美金,乙转入 100 美金」。
注意,状态机对于同一个命令可能生成不同的事件,完全取决于当前状态。还是刚才的例子,如果此时甲余额只有 50 美金,转账 100 美金的命令将无法被成功执行,此时状态机会生成一个转账失败事件。
一旦事件被生成并存入事件仓库,该事件就一定会被状态机执行,进而改变状态机内的状态。对于本例子来说,甲开始状态为 150 美金,在转出 100 美金后的结束状态为剩余 50 美金。
命令和事件会按顺序记录并处理。这个过程是一个经典的数据库回滚日志(WAL,Write Ahead Log)案例,也是账务系统的本地数据存储格式。由于单机机器故障可能会导致日志丢失,我们采用 Raft 强一致性算法来增强数据容灾能力。日志首先会被存储为临时状态,只有当日志被安全备份到多于一半的机器后才会被更改为已提交状态,允许外部访问和处理。
数据存储格式为数据库回滚日志,当系统出错后会进行标准的数据库回滚和恢复过程。如下图所示:
CQRS
CQRS(Command Query Responsibility Segregation),即俗称的读写分离,也是 Event Sourcing 提出来的一个概念。命令和事件会改变状态机的状态,是一个写的过程。事件一旦生成便不可修改,同时事件之间有严格的先后顺序关系,用户可以通过顺序监听事件来重构状态的备份,甚至增加自己的特殊逻辑来生成定制化的视图,这便是一个读的过程。
例如核心账务系统的所有事件为记账消息,我们通过监听消息来生成余额状态,并将其保存至关系型数据库和 Kafka 等数据流系统做实时聚合。
流处理
我们在 CQRS 的基础上更进了一步。鉴于命令和事件都是通过消息系统来传输,加之 CQRS 设计模式中写和读两个操作之间也是通过消息系统来传输,一个自然的整体解决方案即为:用流处理的架构来设计整个账务系统的消息传送和处理。
各个处理节点,比如处理事件的节点或者是生成视图的节点,均可以看作流处理的计算节点。流处理的最上层节点通过强一致性算法来保证数据和状态的高可用及一致性。但这些通过了强一致性同步的数据在向下游流动的过程中依然可能会出现丢失。但因为最上游数据有线性一致性保证,下游能通过检测线性一致性来判断是否出现了数据丢失,进而通过查询上游来进行数据补偿。这意味着下游数据并不一定需要强一致性算法,我们可以选用一致性稍弱的消息系统,比如 Kafka,来作为下游的消息管道。如下图所示:
系统实现
实现
前几章提到了系统的分层。业务层主要处理账务业务逻辑,为无状态节点,容易横向扩展,由 Java 实现。核心业务层和基础架构层负责处理和维护业务状态,为有状态节点,有稳定性、强一致性及性能表现等需求,由 C++实现。其余辅助性组件由 go 实现。
我们选取了 C++17 标准,利用了新标准下的一些新功能,并在实现过程中大量使用了函数式编程的思想。使用函数式编程思想的主要目的是确保核心逻辑的可组合性及行为可重现性,同时也确保核心数据结构在多线程情况下的正确性。
优化
优化主要集中在C++部分,主要优化两点:1. 吞吐量;2. 延时。我们创新性地采用了双层(核心业务层和基础架构层)一体的设计,并结合流水线和批处理,在提高系统吞吐量,降低延时的同时避免了常见的因为双层分离由网络造成的各种状态不一致的问题。为了保证该设计实现的正确性,我们使用 Jepsen 测试框架,并针对性地设计了测试用例,来确保在各种异常情况发生时,系统均能正常工作。
提高系统吞吐量的常用方法为流水线。把对外部请求的处理过程划分成若干个阶段,每个阶段独立运行,阶段之间使用队列连结,上一个阶段的输出是下一个阶段的输入,形成一条流水线。这样设计的好处在于可以针对每个阶段的特点做独立优化。例如在数据接收阶段,使用多线程来提高单位时间内请求接收的数量;在数据同步阶段,采用批处理方式来降低网络开销;在核心处理阶段,每个请求必须顺序执行以保证状态正确,因此我们采用了一些仅在对冲基金高频交易系统中才会使用的特殊优化方法来提高处理速度。
系统延时主要发生在网络请求上。观测到的一次跨数据中心请求延时在几毫秒左右,发生网络抖动时的延时在几十毫秒左右。正常的一次网络请求对用户体验没有大的影响。
但是 Event Sourcing 会带来两个问题:
第一个问题是 Event Sourcing 处理每个业务事件时需要实时同步灾备两次:第一次备份 Command,Command 备份完成后再备份 Event。这两次备份有先后依赖关系,无法同步处理。
另一个问题是多个业务事件彼此之间需要按顺序先后处理,如果乱序处理会带来业务的不正确性。图下图列举了 2 个事件共 4 步顺序网络同步过程:
通过一些特殊的数据结构优化和运行顺序调整,我们成功地优化掉了绝大多数网络请求。最后结果为:对于同一批需要处理的事件,系统只需要执行一次网络同步。我们也论证了在多线程情况下强一致算法在多节点集群换主时,各个状态机均能运行或回滚至正确的状态。优化后的 2 个事件共 1 步顺序网络同步过程如下图所示:
部署
系统部署在基于 docker 和 k8s 的云平台,支持一键云部署。同时由于系统对所有 IO 进行了抽象和封装,能通过分布式配置服务动态切换存储媒介,系统在部署时能选择远程部署,或者完全基于本地单机文件系统的部署,即俗称的单机版本。单机版本包含了完整的业务、数据、存储、监控等系统,极大地方便了系统的开发与上线。
开源计划
我们会分三步来逐步开放系统功能和代码:
对高校等科研单位开放源代码
对商业合作伙伴开放功能和源代码
对所有人开源
目前我们已经和英美几所大学开展了深入的科研合作,以核心账务系统为研究对象进行分布式系统前沿的研究。我们和商业合作伙伴的合作也正在同步进行当中。我们会于明年年底开源所有的代码和文档,敬请期待。
写在最后
核心账务系统在设计和实现过程中遇到了非常多的挑战,也得到了很多帮助。感谢亿贝支付管理层的信任和战略支持。感谢亿贝中国研发中心管理层的跨部门合作。感谢亿贝全球云计算团队和 SRE 团队的大力支持,在实验过程中赞助了所有的云资源,并帮忙实施了大量优化工作。感谢 Raft 作者 Diego Ongaro 对系统架构的建议和意见。这只是亿贝支付的一小步,我们还有更广阔的空间去探索。
作者介绍
任杰,清华大学本硕。2007 年加入摩根士丹利,后作为衍生品风险引擎的全球负责人,负责所有利率、外汇及信用衍生品业务。2017 年加入支付宝核心支付团队,参与支付宝和网上银行等相关业务。2018 年加入 ebay 支付团队,负责核心账务系统。
原文链接
超越“双十一”—— ebay百万TPS支付账务系统的设计与实现
评论