eBay支付核心账务系统之直冲云霄
作者|焦勇
编辑|林颖
供稿|Payments Barbosa Team
本文共7058字,预计阅读时间15分钟
更多干货请关注“eBay技术荟”公众号
导读
随着新业务以及新卖家在eBay Payments2.0系统的不断上线,系统的交易量也在不断地上升。在一年多之前,eBay支付核心账务系统——FAS(Financial Accounting System)刚上线的时候处理的请求数是每秒30个左右,而现在这个峰值已经达到了每秒3000个。这就对FAS系统提出了更高的要求——能随时应对不断增长的账务记录。在提升FAS的处理能力上,FAS团队未雨绸缪,自研了一套水平扩展的方案。此方案做到了在无需停服的基础上能够水平扩展(详见eBay支付核心账务系统之“展”翅高飞)。
为了证明FAS的水平扩展功能和处理能力,我们做了一个百万TPS的测试,并且在测试中加入了自动拆分流量的功能。本文将讲述百万TPS的系统设计、测试过程、正确性验证以及在流量提升后带来的新挑战,看看FAS系统如何以较强的稳定性和较高的性能支撑百万TPS的流量,直冲云霄。
1 背景
FAS系统在2019年做过一次百万TPS测试(详见超越“双十一”—— ebay百万TPS支付账务系统的设计与实现),初步证明了FAS系统具有水平扩容的能力。但那时的FAS系统为了支持这么大的流量做了一些定制化。比如,用于发送的请求是自己生成的,每个请求的大小只有1k,而且请求的类型也比较单一;为了保证正确性,FAS的网关(Gateway)在生产环境上用了同步通信的模式,其性能不是很好(在2019年的百万TPS测试中,我们用Golang重写了网关,采用异步通信的模式提高了性能);为了减少检查前提条件(preCondition)带来的性能损耗,测试中的PU(Processing Unit)忽略了对前提条件的检查(生产环境的PU在处理请求时会检查前提条件,即当满足这个前提条件时才会处理这个请求)。
2019年的百万TPS测试旨在证明FAS系统具有处理百万TPS流量的能力。现在,我们的FAS系统已经从1.0演进到5.0了,在性能、稳定性和可用性方面都做了许多优化(详见超越“双十一” | ebay支付核心账务系统架构演进之路)。我们这次百万TPS测试的重点,除了证明FAS系统具有线性扩容的能力,更多地会放在测试场景的真实性和测试结果的正确性验证上。尽量让测试的环境和数据跟生产上保持一致,这样我们的测试结果才会更加可信、可靠。
2 方案设计
2.1
部署模式
我们的部署分为3个子系统(如下图所示):Transfer系统(即核心系统)、Journal系统和对账系统。
(点击可查看大图)
核心系统的核心由多组PU集群组成,其中每组PU集群是由3个PU(Processing Unit)节点组成的Raft集群。其作用为:接受外部的请求并处理,计算每个账户的余额,存储在Raft Log中作为信息源(Source Of Truth,SOT)。
Journal系统也是由若干组PU组成的Raft集群,它会进一步解析核心系统里的Raft Log,生成交易日志(Journal),存于自己的Raft Log中。
对账系统通过Journal系统的读模块拉取Journal系统的Raft Log进一步解析并将结构化后的Journal通过Kafka存入HDFS(Hadoop Distributed File System)。这样我们就能用同样的请求体,把单组PU的处理结果和多组PU的处理结果解析并存入HDFS,利用大数据的方式对结果进行比较,来证明PU自动分组后和单组PU的行为是一致的。
本次测试中,我们的目标是核心系统达到百万TPS,且Journal系统和对账系统可以在小流量的基础上完成。
2.2
环境准备
2.2.1 核心账务系统部署方式
核心账务系统中PU集群在生产环境上采用的是“三地五中心”的部署方式,即在三座城市部署五个节点组成Raft集群。这种部署方式具有极强的容灾能力,只有当5个节点里面超过半数(即3个及以上)的节点不可用时我们的系统才会停止服务。而想要达到3个节点不可用的条件是:至少2个城市的数据中心同时不可用。这种部署方式足以应对一个地点发生哪怕是地震这样的天灾。我们的压测使用了简化版的“三地三中心”,即每个PU集群3个节点,部署在3个不同的城市。
2.2.2 机器数量
本次测试使用了300个Shooter用于发送请求,5400台网关用于请求路由到PU集群,PU集群有350组,每组3个节点,总共1050个PU节点,还有其他若干辅助节点,一共准备了6774个节点、31296个CPU核心(Core)、130T的内存和600T的硬盘存储。跨城带宽为106Gbps,城内带宽为53Gbps。
2.3
请求体的准备
2019年的百万TPS测试的请求体是根据业务逻辑自己生成的,那时的业务逻辑比较简单,请求体的大小也只有1k。为了简化测试,生成的请求体类型也比较单一,并没有覆盖生产环境上的所有案例。这次的百万TPS测试中,为了尽量让测试场景尽量跟生产环境一致,最好的做法就是直接拿生产环境上的请求体作为我们的测试数据。这样既能保证请求体的真实性,也能覆盖生产环境的所有业务。
得益于我们可溯源、可重放的设计,从核心账务系统上线的第一天起,所有处理过的请求我们都存储在Raft Log中。我们可以通过FAS系统的读模块(详见eBay支付账务系统架构解析之“读”一无二)获取任意一段的Raft Log,解析出原始的请求体,并存储起来作为我们百万TPS测试的请求体。最终,我们解析出了20亿最新的请求体,几乎覆盖所有业务场景,每个请求体的平均大小为5k,为2019年百万TPS测试的5倍。
2.4
发送程序
有了请求体,我们如何以每秒钟百万的速度发送?并且万一对生产环境有影响的情况下怎么能动态地调整发送速度或停止发送?下面我们将分享发送程序Shooter的设计与实现来解答上述问题。
我们生成的请求体每10万个组成一个文件,20亿请求总共2万个文件,其文件名上带有请求的Offset范围。300个Shooter部署在3个数据中心,每个数据中心部署100个Shooter。每个Shooter对应一个从0开始递增的编号,下载自己编号与文件名上起始Offset除以300的余数(Mod(Offsetstart,300))相同的文件。这样每个Shooter就会负责66或67个文件,而且互相不重复。另外,Shooter是用Golang语言编写的轻量级程序,能同时开启1000个以上的协程。通过测试可以得出,单个Shooter能达到8000的TPS。
为了能够让Shooter动态地调整发送速度,我们搭了一套ETCD集群作为分布式的存储。先在ETCD上配置一个速度值,Shooter每隔2秒钟会去ETCD上读取配置的最新速度值,然后除以300就得到自己应发的速度值。Shooter内部的速度控制采用的是“令牌桶”原理——发送程序会不停地循环发送,但每次发送请求之前必须获得一个令牌(Shooter会以计算出来的速度值生成令牌),如果获取不到令牌就等待。这样我们就可以通过配置ETCD上的速度值来随时调整发送速度或停止发送。
2.5
减少对生产环境的影响
Shooter以百万TPS的速度发送Http请求给网关,正常情况下我们是走网关的负载均衡器。但这些负载均衡器跟生产环境里的其他应用程序是共同分享的。为了减少影响,我们决定不走生产环境上的负载均衡器,而是自己在客户端实现负载均衡。
每个数据中心有1800台网关,指定它们接受同一数据中心Shooter的请求,即每个Shooter对应18台网关,在这18台网关中随机发送。这样既避免了对生产环境负载均衡器的影响,也减少了跨数据中心的网络流量。
3 测试过程
3.1
单组节点测试
3.1.1 单组PU测试
PU拆分流量是依据账号进行的。一个账号只能出现在一组PU里面,所以单个PU的最大TPS决定了单个账号交易速度的上限。另外,了解单组PU的TPS上限能帮助我们估算达到百万TPS需要的最少PU组数。
从单组PU测试的结果(如下图所示)中可以看出:在我们的测试环境下,单组PU最高能达到4000+的TPS;但TPS达到峰值的情况下,延迟也相应地增加,且延迟时间超过400ms。
(点击可查看大图)
这里有两个因素导致延迟增加。一是单组集群的3个PU中只有领导者(Leader)接受请求,而在Raft协议中,需要多数派节点成功写入Raft Log才算请求完成。为了更高的可用性,单组集群的3个PU部署在了3个不同的城市,两个跟随者(Follower)需要跨城复制领导者的Raft Log,从而增加了延迟。二是为了提高吞吐量,我们后台用了批(Batch)的形式写Raft Log。TPS越高,吞吐量越大,批大小越大,延迟也就相应的增加了。按照4000TPS来算,想要达到百万,我们至少需要250组PU集群。
3.1.2 单组网关测试
网关是用Java实现的,负责接受Shooter的Http请求、解析、验证并重新封装成GPRC请求发送给PU,然后等待PU回应返回,再返回给Shooter。所有的请求都是同步调用的,但同步调用的代价就是性能不高。我们压测了一下网关的性能。如下图所示,可以看到在PU延迟很低的情况下,一个网关节点差不多有700的TPS。这种情况下,若想要达到百万TPS,我们差不多需要1500台网关。
(点击可查看大图)
3.2
多组节点测试
当单组PU集群满足不了业务发展时,我们就需要多组PU集群来分担处理流量。我们的核心账务系统是有状态的服务,包括内存状态和Raft Log,这就给流量拆分带来了比较大的难度。我们的难点是在不停服的基础上对有状态的服务进行流量拆分,同时还要保证数据的一致性。对此,我们已经有了一套完整的解决方案,详见eBay支付核心账务系统之“展”翅高飞。
3.2.1 流量自动拆分
流量自动拆分功能是由另一个模块控制器(Controller)提供的。Controller每隔几秒钟就会检查所有PU集群的流量情况,挑选流量最大的那组PU集群。如果它的流量达到设定的阈值,Controller就会将此集群的流量拆分一半到一组新的PU集群。因为流量拆分的时候,新的PU集群需要复制源PU的所有内存状态和Raft Log,所以会比较耗时。为了提高测试的效率,我们并没有把这个阈值设得很高,而是用小流量发送请求完成所有PU集群的流量拆分。从下图中可以看出,我们以300TPS的速度发送请求,PU集群每隔十几秒就会拉出一根新的线出来,这根新的线代表自动拆分出来的新PU集群。
(点击可查看大图)
下图是自动拆分的全景图,我们花了80多分钟完成了250组PU集群的流量拆分。
(点击可查看大图)
3.2.2 故障排查
我们在测试的过程中并不是一帆风顺,也遇到了一些问题。下面我们来看看测试过程中遇到的问题以及解决方法。
1、DNS 缓存问题
我们在第一次想上百万TPS的时候,发现750台PU只拆分了220组,少了30组。另外,网关不停地收到PU返回来的指示“路由信息错误”的错误码(如下图右下角)。
(点击可查看大图)
1)对于少了的30组PU,我们检查了拆分日志,发现新PU集群里的3个PU中有2个PU收到了拆分指令,还有1个PU没有收到,感觉是因为Controller没有连接过这个PU。
2)对于网关不停地收到PU返回的“路由信息错误”,正常情况应该是:PU集群在拆分一组新PU的情况下,路由信息在老PU和新PU以及ETCD里都会相应地更新。当网关根据老的路由信息发送拆分出去的流量给老PU时,老PU集群发现自己不应该再处理这个请求,就会拒绝请求并返回网关“路由信息错误”。此时,网关会去ETCD更新自己的路由信息,然后重新发送给正确的新PU集群,路由信息的错误应该会越来越少。但这次的路由信息却一直保持在一秒钟十几万的错误数量。
之后,我们进一步检查了错误日志,发现Controller和网关里某些PU的域名和IP不匹配,因此会发送给错误的PU。通过进一步分析,750台PU被重新创建过,重建之前每个PU分配了一个IP。然而,这些域名跟IP的对应关系已经存在于Controller和网关的DNS缓存里了,当这些PU被销毁和重建的时候,其域名不变,但它们的IP会被回收再重新分配。这时候刚刚被回收的IP可能被分配给了新的其他PU。如果Controller和网关想通过老的DNS缓存里的信息发送请求时就会发送到错误的PU里去。这个问题可以通过重启Controller和网关,从而清空DNS缓存得以解决。
2、PU集群和网关路由不均问题
解决了DNS缓存问题以后,PU拆分流量都成功了。我们的PU流量拆分策略是:把所有流量分为1024组,第一组PU集群在开始时负责全部流量,当拆分的时候,把自己的一半流量分出去,变成2组PU集群,每组各负责512组流量。以此类推,想要拆分以后的PU集群流量均衡,PU集群的数量必须是2的N次方。在“1、DNS缓存问题”的图中可以看出:220组PU集群时,每个PU的流量是不均衡的(见图中的“fam pu tps by cluster”),TPS的显示一半在上面,一半在下面。
为此,我们加了6组PU集群,总共256组,为2的8次方。从压测结果(如下图左中部分所示)可以看出“fam pu tps by cluster”的线基本重合,说明PU集群的流量变得均衡了。
(点击可查看大图)
但是从上图中上部分的“shooter tps by cluster”可以看出,57数据中心的Shooter TPS(黄线表示)大幅低于其他两个数据中心,而57数据中心的Shooter只会发送请求到57数据中心的网关。由此,我们怀疑57数据中心的网关有问题,于是查看了网关的监控。
我们统计了网关的忙碌线程数(如下图所示),发现少数几个网关忙碌线程数维持在500(我们设置了每个网关的线程池最大容量为500),而其他绝大部分网关的忙碌线程数都在100以下。说明Shooter把大部分的请求都发送给了这少数几个忙碌线程数为500的网关。这就造成了路由不均,大大降低了网关的性能。
(点击可查看大图)
对于网关的路由,我们用的是Kubeproxy的IPVS功能。Shooter部署在K8s的Pod里,当Shooter发送Http请求给网关的时候,Shooter本地的Kubeproxy如果发现请求目标为本数据中心的服务,就会到IPVS里面查找这个服务对应的后端节点,根据一定的策略挑选一个节点直接发送,这样就可以绕开网关的负载均衡器。但是这种路由策略看起来并不均衡。
为此,我们决定在客户端自己实现负载均衡。我们拿到了所有网关节点对应的地址,平均分给了同数据中心的Shooter,这样每个Shooter在自己对应的十几个网关中随机发送。测试证明这样可以实现简单的负载均衡,而且57数据中心的Shooter TPS跟其他两个数据中心差别不大了。
当然这种做法只限测试使用,在真正的生产环境中不能这么做,如果真有百万TPS的这一天,我们肯定会用专门且强大的负载均衡器来支撑我们蓬勃发展的业务。
3、网关和PU的数量不够
在上一小节“3.1.2”的单组网关的测试中我们可以得出网关最大的TPS是700,若要支持百万TPS,则需要1500台网关,但这是建立在PU的延迟是20多毫秒的情况下。我们假设网关的平均线程数是180,每个线程的TPS约为4(700/180)。那么每处理一个请求,网关所需要的时间就是250ms,减去PU的20ms延迟,网关本身花费的时间为230ms。而单组PU达到4000TPS的情况下延迟能达到将近600ms。这时候单个网关处理的TPS就变成了210(1000/(230+600) *180),百万TPS就需要4760(1000000/210)台网关。为了不让网关成为我们测试的瓶颈,我们多加了点缓冲,最后总共创建了将近5400台的网关。测试结果如下图:
(点击可查看大图)
上图结果可以看到,PU集群最高的TPS瞬时值为999440(接近百万),且能够稳定在90万TPS以上。这里不能达到百万TPS的原因为:PU的路由只能做到尽量均匀,我们没法控制发送的请求,所以不可能做到绝对均匀,总会有些PU集群接受比较多的流量,有些接受比较少的流量,而接受较少流量的PU集群就会拉低整个系统的TPS。以90万的TPS计算,每组PU集群的平均TPS也有3500以上。
后来,我们把PU的数量加到了350组,又做了一次测试,测试结果如下图所示:
(点击可查看大图)
从上图可以看到PU集群的瞬时TPS最大能达到131万以上,最终能够稳定在120万以上。以120万来算,每组PU集群的平均TPS能达到3400以上。这里要说的一点是:由于350不是2的N次方,所以PU的路由是不均衡的(从上图中“fam pu tps by cluster”可看出)。若想要变得均衡,则需要修改拆分策略。
3.3
正确性验证
通过测试,可以看到我们的核心账务系统能够线性扩展,且具有超过百万TPS的性能,只要硬件资源足够多,还能拥有更高的处理能力。另外,核心账务系统是有状态的服务,在通过横向扩展来提高性能的同时,还必须保证其正确性,否则横向扩展就会没有意义。为了验证其正确性,我们搭了两套系统:一套是单组PU集群,一套是多组PU集群。我们向这两套系统发送同样的请求(第二套系统在发送请求的过程中完成流量自动拆分),最后通过比较两套系统的结果是否一致来判断正确性。
3.3.1 正确性验证的程序设计
正确性验证的方法是比较两套系统的结果是否一致,但结果是以二进制的形式加密存储在PU的Raft Log中的。那么,如何才能得到结构化的结果并能方便地进行比较呢?
这就用到了我们的另一个产品:读模块(详见eBay支付账务系统架构解析之“读”一无二)。基于读模块,我们开发了一套对账系统——将从读模块读取到的信息解析成日志(Journal),再通过Kafka存储到了HDFS中,最后通过Spark SQL可以让我们方便地比较两套系统产生的Journal是否一致。部署模式如下图所示:
(点击可查看大图)
3.3.2 正确性验证的挑战
我们要保证两套系统的结果一致,除了发送的请求要相同之外,发送请求的顺序也是有讲究的。eBay核心账务系统的某些请求之间存在着依赖关系,比如退款请求(PU集群根据接受到退款请求的时刻不同,会从不同的账号退款)、给卖家支出(Payout)的请求(当PU接收到支出请求时,需查看卖家的当前余额,余额多少就支出多少,所以支出请求早来跟晚来产生的结果是不一样的)。为了保证两套系统产生的结果是完全一致的,我们就必须保证这些有依赖关系的请求发送顺序保持一致。找出所有这些有依赖关系的请求是对账系统的难点所在。而且保证有依赖关系的请求的发送顺序就意味着不能并发发送,然而不能并发发送会大大降低了TPS。这些都是对账系统独有的挑战,在生产环境上却不会有这样的烦恼,因为我们的系统本身就能适应网络重传,虽然会有收到请求顺序跟实际发生顺序不一致等情况,但请求的顺序一旦定下来,就是SOT。
由于控制发送顺序会大大降低TPS(这也不是生产环境的行为),所以我们没有在百万TPS时做正确性验证,而是在小流量时验证了PU集群的自动拆分,并且拆分以后结果是跟单组PU集群是一致的。
4 总结
为了应对eBay不断增长的业务流量,FAS团队未雨绸缪,研发了流量自动拆分,将一组PU集群拆分成多组PU集群。通过百万TPS的测试,我们证明了eBay核心账务系统具有线性扩容的能力,也验证了流量自动拆分后的行为跟拆分前是一致的。同时,我们也看到了eBay的基础架构平台具有较强的稳定性和较高的性能,能够支撑百万TPS的流量。
5 展望
本次的百万TPS测试只覆盖了FAS系统最核心的部分,之后会覆盖整个生态系统。我们还会加入更多的功能模块,比如说Chaos Monkey、百万TPS的对账、eBay基础架构平台的FSS(Federal Stateful Set)功能等等,敬请期待!
往期推荐
Spark优化之高性能Range Join
eBay支付核心账务系统之“展”翅高飞
eBay支付账务系统架构解析之“读”一无二
超越“双十一”|eBay支付核心账务系统架构演进之路
超越“双十一”|eBay百万TPS支付账务系统的设计与实现
点击阅读原文,一键投递
eBay大量优质职位虚席以待
我们的身边,还缺一个你!