扫描下方海报
试读
2018年,ebay全面展开了下一代百万TPS支付账务系统的设计与实现。本文主要介绍核心账务系统的性能和容灾能力,将从
账务系统简介、百万TPS压测实验、系统架构分析、开源计划
四个方面进行阐释。希望能给同业人员一定启发和借鉴。
亿贝 (www.ebay.com) 于
2018年
全面展开了下一代支付系统的设计和实现。支付宝的双十一和微信财付通的春节红包已经向世界展示了支付行业当前的工程成果。那么作为下一代支付系统,如何才能做得更好?
支付宝的支付系统能支撑双十一期间高达几十万的并发量,我们的支付系统有没有可能支持得更高、更稳定?
俗话说擒贼先擒王,
支付系统最重要的组件之一是账务系统,有着极其严格的业务正确性和稳定性要求,我们便以此为突破点,开展了攻坚项目。
我们支付团队在
2018年底
接到项目后对支付行业做了充分的调研,首先确定了下一代账务系统的设计目标:
1. 业务目标:
a. 支持所有支付业务功能
b. 扩展接口支持所有互金功能
a. 三地五中心部署,数据中心级别容灾
b. 数据毫秒级实时异地同步备份,业务秒级异地主备切换
a. 自动线性扩容
b. 有能力承接全球所有支付公司峰值流量之和
a. 实时数据中台
b. 具备一定的区块链所具有的数据防篡改能力
接下来我们开展了为期半年的系统设计和原型实现。
2019年
,第一季度原型系统通过验收,下一代核心账务系统正式立项。
经过半年紧锣密鼓的开发,系统于今年第三季度正式灰度上线,并通过了性能和容灾验收。
本文主要介绍核心账务系统的性能和容灾能力,包含以下内容:
-
核心账务系统简介
-
百万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共识算法的强一致性协议进行实时数据同步
。
这是最常见的一种部署方案,兼顾了容灾和业务处理响应时间。从图3.1的测试结果看,
1KB
的请求,每秒最高能处理的交易量是
7,800笔
。
值得一提的是,当达到峰值后,由于请求接收阶段的处理能力已到上限,
P99
(第99个百分位数)下单个转账请求的延时明显增大。而批处理日志写入阶段单次打包的日志数量明显上升,部分抵消了海量请求带来的影响,
P90
下延时的增大幅度则相对较小。
在整个测试过程中发生了一次Raft选主,导致了TPS的降低。
这是因为新主上台前需要将自身的业务状态追到最新,才能处理新的转账请求。和传统基于Raft协议的应用相比(例如KVStore),这是支付应用较为独特的地方。
本次实验为同机房三节点部署。从图3.2可以看到,系统最高每秒可处理
9,500笔
交易。
由于数据复制过程中只要有一个
从节点(Raft follower)
成功写入日志即可认为交易完成,而同数据中心内主从之间数据复制的时延仅为跨数据中心的
1/12
到
1/8
,所以每秒交易数相应增加。该效果与异地三中心部署时主从在同一数据中心的效果一致。
另一点值得指出的是,由于同数据中心内网络和跨数据中心相比更稳定,
在整个实验过程中没有出现选主,TPS更加平稳。
本次实验采用单节点部署,来测试数据复制对交易吞吐量的影响。和前两次相比,该部署由于只要主节点(也是唯一的一个Raft节点)本地写入成功即可认为交易完成,省去了数据复制的时间。
从图3.3可以看到,该部署下每秒交易能稳定在
9,700笔
,最高能处理将近
1万1千
笔。可见,
单节点部署和同机房三节点部署的系统吞吐能力几乎一致
,这在一定程度上归功于我们在提升网络吞吐量方面作的优化。
在实际应用中,单组节点往往无法满足大型电商平台对总交易量的实时性要求,因此需要部署多组节点,只让每组节点承担一部分账户的交易请求。
我们测试了系统在两个典型的业务场景下的表现。
该实验模拟的业务场景是主站日常流量涨跌,交易量会随着某些外部事件而发生动态变化,例如全站大促时,网站流量会随着人们作息而发生缓慢震荡。
从图3.4可以看出,实验开始只有
222组
节点,当系统监测到单组节点的平均吞吐量开始上升并超过阈值时(意味着交易量上升,即将达到并超过单组节点的处理能力),系统会自动部署新的节点来分担总体交易量。伴随着节点数量的上升,平均吞吐量开始缓慢下降到阈值以下,同时保证总体交易量能被及时处理。
该弹性扩容过程示意如下:
(
点击
可观看视频)
系统能动态调整节点数以自适应流量的变化,保证业务不受影响的同时也可以提高硬件的利用率。例如当交易量较低时,可以通过合并节点来减少资源。
该实验模拟的是主站大促,例如每年
双11的零点
,流量洪峰会在极短的时间内涌向支付系统。我们分别测试了系统在瞬间收到
50万
(图3.5)和
100万
(图3.6)交易量的极端情况下的表现。
从图中可以看出,在这两种情况下,
系统均能从容稳定应对,虽然其中部分节点出现了换主(图3.5),但对总体流量的影响微乎其微。
测试过程中甚至还发现延时有短暂下降,这主要是由于批处理阶段打包效率提升。
在达到峰值流量时我们还随机查看了部分节点的资源使用情况,发现CPU并没有被打满,部分线程并未满负荷运作。这说明流水线上各阶段的处理能力强弱不一,后续调优后系统应该会有更好的表现。
在解决超大流量时,电商系统及支付系统目前最流行的解决方案是通过
分库分表
来做流量切分,通过
分布式事务
解决分库分表带来的分布式一致性的问题。这是一个经过了实践检验的解决方案。
但是近十年来经过开源社区和各大互联网厂商的不断努力,数据系统的设计有了长足的发展,涌现出了更多更好的解决方案。
由于我们是从零开始设计一个新的系统,没有历史负担,所以决定应用更为领先的系统设计来解决核心账务系统所面临的业务和系统复杂度问题。
金融系统是一个典型的业务复杂系统。我们计算过,对于信用卡业务来说,一共有
573,099,840种
不同的业务场景。传统解决方案是所有系统直接基于数据库表来进行开发。
传统方案的问题在于数据库存储的是数据的二进制格式,没有业务逻辑,系统在使用数据库的时候需要基于二进制形式来重构业务逻辑,正确性难以保障。
所幸随着本世纪初
领域驱动设计(Domain Driven Design)模式
的出现,金融系统复杂度得到了系统性的解决。
我们首先使用了
领域模型
来对金融业务建模,使得数据的业务表现层和存储层分离。
其次使用
Event Sourcing
来保证业务处理的正确性和对历史状态的百分百可追溯可还原。再者使用
CQRS
来实现系统读写分离。最后用
流处理
的方式自上而下、从外及内地统一所有设计。
系统在做水平扩容时需要处理数据正确性问题。传统的方式一般采用分布式事务。
我们基于账务系统的特殊业务逻辑提出了基于消息最终一致性和业务补偿的解决方案。
多机方案除了正确性外还有一些其它需要注意的问题。首先是多了至少一次的网络开销,增加了请求延时。
其次,系统内部流量翻番,单机提供的业务TPS减半。
最后由于基于消息最终一致性的实现无法同步返回结果给调用者,系统需要增加异步转同步的组件。
多机事务需要每台机器都具备本地事务的能力。传统数据库为了提高事务的执行性能普遍采取了多线程的并发执行机制。
但是本世纪初的一些研究发现,对于有些业务场景,特别是金融行业,多线程并非最优解决方案。
因此在核心账务系统的设计过程中,我们确保核心账务业务逻辑只在单线程执行,以便充分利用单线程执行所带来的事务保证和性能优势。
单线程带来的另一个好处是
线性一致性(Linearizability)
。事务只能保证多任务按照某种顺序来
串行化执行(Serializability)
,在有多种可选顺序的情况下,事务调度算法并不保证每次均能选择同一种确定顺序。
和事务的一致性不同,线性一致性能确保每次调度的顺序完全一致。账务系统要求所有记账均按照一个确定的顺序记录,在发生系统回滚及重放后需要恢复至相同顺序,因此线性一致性是一个对于账务系统正确性来说非常重要的保证。
电商及支付系统通常采用基于SOA的节点间服务调用和基于Java Bean的节点内组件调用。但是在出现秒杀等场景时需要做一些特殊的削峰填谷工作。削峰填谷通常使用消息队列来做缓冲,
因此消息队列也成为了一个必不可少的组件。
与经典设计中所采用的
部分异步
实现不一样,我们在设计时
全面使用了
基于消息的异步解决方案
,包括系统间和系统内的功能调用。异步方案带来的挑战是没有同步接口,提高了调用方复杂度,因此需要利用前文提到的异步转同步组件来降低调用方复杂度。
金融系统对容灾有极高的要求。首先是数据需要支持同城和异地备份,确保数据有区域级灾备能力。再者需要支持业务的快速自动主备切换,时间就是金钱。
传统方案是基于数据库自带的异地备份,分为异步和同步两种备份方式:
-
如果是
异步备份
,由于数据并没有被及时同步至备份节点,主节点出问题时会有数据丢失。
-
如果是
同步备份
,则主备机必须同时在线,任何一台出现问题都会导致业务中断。备机越多,当中任何一台出问题的概率越大,业务中断的概率也越大,因此同步备份的问题在于数据容灾能力越强,系统越不稳定。
我们采用了新的方式来达到同时提高数据容灾和系统稳定性的效果。新的方法和同步备份一样需要备机同步返回备份结果,但只要求大部分返回即可,不需要全部返回。
这个方法称为
共识算法族(consensus)
。我们选取了其中的Raft算法,从零开始实现自己的高效定制化的强一致性算法。
账务系统的核心数据会通过强一致性算法实时同步至
3个
数据中心的
5个
存储节点。系统只要有多于一半的节点能正常工作便能整体正常运行,因此该部署计划允许
5个
节点中任意
2个
节点同时出现问题。
由于单个数据中心出现问题时最多只能影响
2个
节点,因此该部署计划能支持数据中心级别的灾备要求。
该部署俗称
三地五中心部署
。三地五中心的简化版为
两地三中心
,也是我们在此次压力测试中所采用的模型,如图4.1所示:
我们创新性地采用了
双层(核心业务层和基础
架构层)
一体的设计来实现业务的快速自动主备切换。当数据出现灾备切换时,定制化的Raft算法会同时切换数据和业务逻辑至灾备节点。
为了能更好地证明系统的容灾能力,我们在生产环境部署了
定时自动容灾演练系统
。该系统会不定时的对系统进行随机的可用性攻击,从而验证生产环境中面对网络、硬盘、内存等出现异常情况时系统的容忍能力。
另外还有一个纵向贯穿始终的
监控层
,负责收集监控数据和跨层监控。架构示意图见图4.2:
最上层的账务业务层
提供外部访问接口,负责处理最上层的业务逻辑,有一定的对外I/O请求,内部无状态。业务会被分解为多个条件转账请求发给下层的基础账务层。
基础账务层
负责记账,对外无I/O请求,内部有状态。该状态由基础架构层提供的状态机来维护。
基础架构层
负责提供一个基于Raft算法的高效稳定的状态机实现,其性能由单线程保证,稳定性由强一致性算法来保证。基础账务层处理完的结果为会计账本,会输出给
存储层
。
完整的分层架构图如下图4.3所示:
基础账务层和基础架构层是根据
Event Sourcing
的原则来设计的,这也是核心账务系统最重要的架构设计。
Event Sourcing有
4
个
主要组成部分:
-
Command:外部请求
-
Event:事件
-
State:状态
-
State Machine:状态机
所有外部请求会先被发送给状态机。状态机会结合当前状态生成事件。事件被存入
事件仓库(Event Store)
后会被状态机执行,进而改变状态机内的状态。如图4.4所示:
举一个账务系统的例子。账务系统接收到的一个命令是「从甲转乙
100美金
,但甲不能出现余额不足的情况」。当前状态为「甲有
150美金
,乙有
50美金
」。状态机在收到命令后检查当前状态情况,确认转账后不会出现甲余额不足的情况,因此生成事件「甲转出
100美金
,乙转入
100美金
」。
注意,状态机对于同一个命令可能生成不同的事件,完全取决于当前状态。
还是刚才的例子,如果此时甲余额只有
50美金
,转账100美金的命令将无法被成功执行,此时状态机会生成一个转账失败事件。
一旦事件被生成并存入事件仓库,该事件就一定会被状态机执行,进而改变状态机内的状态。对于本例子来说,甲开始状态为
150美金
,在转出100美金后的结束状态为剩余50美金。
命令和事件会按顺序记录并处理。这个过程是一个经典的
数据库回滚日志(WAL,Write Ahead Log)
案例,也是账务系统的本地数据存储格式。
由于单机机器故障可能会导致日志丢失,我们采用
Raft强一致性算法
来增强数据容灾能力。
日志首先会被存储为临时状态,只有当日志被安全备份到多于一半的机器后才会被更改为已提交状态,允许外部访问和处理。
数据存储格式为数据库回滚日志,当系统出错后会进行标准的数据库回滚和恢复过程。如图4.6所示:
CQRS(Command Query Responsibility Segregation),即俗称的读写分离
,也是Event Sourcing提出来的一个概念。命令和事件会改变状态机的状态,是一个写的过程。事件一旦生成便不可修改,同时事件之间有严格的先后顺序关系,用户可以通过顺序监听事件来重构状态的备份,甚至增加自己的特殊逻辑来生成定制化的视图,这便是一个读的过程。
例如核心账务系统的所有事件为记账消息,我们通过监听消息来生成余额状态,并将其保存至关系型数据库和Kafka等数据流系统做实时聚合。