17611538698
info@21cto.com

每账户每秒更新 30 次以上:Uber 如何构建高吞吐量支付账户处理

架构 0 13 22小时前

图片

导读:Uber 高吞吐量的支付系统架构之最佳实践。

Uber (优步)新近推出了一套高吞吐量的财务账簿处理系统,旨在处理其分布式会计基础设施中单个账户持续的写入争用。

该系统针对的是单个账户经历集中更新高峰的场景,这种高峰可能超出传统按请求交易处理模型的处理能力。

据Uber工程团队介绍,该设计支持每个账户每秒超过30次的更新,同时保持严格的一致性和可审计性要求。

这项功能已经集成在 Uber 的财务账簿平台中,该平台基于复式记账模型构建,用于记录系统内的所有资金流动。该模型提供了强大的正确性保证和端到端的可追溯性,但要求账户级别的更新必须严格序列化。在高度竞争情况下,例如批量调整、对账或操作更正,单个账户可能会经历持续的更新频率,从而暴露传统交易执行流程中的瓶颈。

Uber首席信息官马克·彼得斯(Mark Peters)这样说道

对于 Uber 这样的规模而言,对亚秒级批处理和保持严格一致性的重视对于运营弹性至关重要。

Uber 团队还发表报告称,这种架构显著缩短了高容量工作负载的处理时间,提高了其市场系统中的财务对账和运营工作流程的速度。

背景


Uber 的核心支付平台 Gulfstream 是一个基于复式记账原则构建的集成式、符合 SOX 标准的平台。它采用基于作业/交易的架构,每天处理数百万笔交易。该系统遵循资金在账户之间流动而不创建或销毁的基本会计原则,并通过用户账户变更日志 (UAC) 提供不可篡改的审计跟踪。

然而,随着平台的发展,某些用户账户经历了极端的流量高峰和数据量激增,导致快捷键出现问题,超出了标准处理流程的承受能力。到 2023 年,一些用户账户需要处理的更新操作远远超过了我们系统每秒 3-4 次的上限。

例如,大型平台运营商每天要处理针对单个用户账户的数万笔批量调整,需要 21-24 小时才能完成。车队运营商每天往往产生数十万笔交易,处理时间超过一天。车队运营商在特定时段遭遇的支付高峰会造成系统负载过高,导致处理流程不堪重负。除了车队运营商之外,某些 Uber for Business 客户还会向特定地区的乘客提供补贴。此类活动会导致每个 Uber for Business 客户每天产生数万笔交易。

传统处理方法需要对每个操作进行多次连续往返:

  • 读取用户帐户状态(30-70毫秒) 
  • 检查处理历史记录(30-70毫秒) 
  • 更新用户帐户(60-160毫秒) 
  • 写入审计日志(12-70毫秒) 

由于每次操作的总延迟在 130 到 370 毫秒之间,通过顺序处理实现每秒 30 次以上更新操作的目标在数学上是不可能的。

图片
图 1:用户帐户事件的传统顺序处理

为什么选择批量处理


Uber最后创造了一种截然不同的方法。

人们的核心发现是,大部分处理时间都消耗在数据存储的往返通信上,而不是实际的计算上。这促使我们采用批量更新的方式,以分摊延迟成本。

Uber 团队考虑过传统的批处理方法,例如基于 Apache Kafka® 的方法,但最终放弃了这些方法,因为客户端批处理会增加延迟,消费者端 1-2 秒的延迟是正常的,而 Kafka 针对高吞吐量进行了优化,但并不针对亚秒级延迟。

为了支持信用卡冻结等实时操作,我们设置了 1 秒的软性限制。因此,我们需要一个能够以 250 毫秒为窗口批量处理操作的系统。这个时间窗口既足够短,可以满足实时操作的需求,又足够长,可以显著提高吞吐量。

图片
图 2:用户帐户事件的新批量处理


Uber团队已经否决的其他架构设计方案包括:

  • 具有全局排序的流处理。在分布式系统中维护全局排序过于复杂,并且需要频繁的压缩。
  • 分片用户帐户。使用多个 Amazon DynamoDB® 行来表示单个帐户会使单一余额概念和热帐户检测变得复杂。
  • 地理位置上的共址。将服务迁移到更靠近数据存储的位置仅带来了 2-3 倍的性能提升,这不足以满足我们 10 倍的吞吐量要求。


设计所考虑因素


在系统设计中,需要在每个用户账户每秒执行超过 30 次操作的情况下,保持严格的一致性保证。此外,还需要将操作批量处理在 250 毫秒的时间窗口内,以平衡延迟和吞吐量。 

无论批次大小,我们都需要将数据存储交互次数降至最低,每个批次仅允许一次读取和一次写入。此外,我们还必须确保审计跟踪的不可篡改性,以符合SOX合规性要求。 

最后,还需要一个能够处理数据持久性的系统,其中 Redis® 数据丢失只会影响单个操作,而不会影响整个批次。

架构图


用户账户批量处理系统采用三服务架构:批量创建服务、批量处理服务和批量后处理服务。

图 3 展示了这些服务如何协同工作。 


图片
图 3:用户帐户批量处理系统的架构。 


批量创建器处理传入的操作,并将它们分组到按时间划分的批次中。它使用 Redis 进行协调,因为 Redis 具有亚毫秒级的延迟和高 RPS 能力。

批量处理服务是核心引擎,它通过一次性读取账户状态、在内存中应用所有操作并执行一次原子更新来处理批量数据。它利用跨不同区域的多个处理器来实现容错。 

批量后处理服务与主路径解耦,并处理异步审计日志的生成和发布。

该系统采用 Redis 集群、用户账户存储和事务数据库。Redis 集群是批处理协调、任务队列和结果缓存的中央协调层。用户账户存储是账户余额的权威数据存储,设计为最小化交互(每个批次一次读取,一次写入)。事务数据库存储用于合规性的不可变审计跟踪(UAC)。

该架构旨在分摊数据存储往返的延迟,使其分摊到多个操作中。每个用户帐户的最大吞吐量超过每秒 30 次操作,批次内每次操作的有效延迟为 8-20 毫秒。整个批次的总耗时约为 400-650 毫秒。

端到端的流程


用户账户批量处理系统的整体流程包括:

  1. 客户端层。该系统接受来自交易处理器和授权保留服务的操作,这两项服务都需要实时处理能力来进行金融操作。
  1. 批处理创建器服务。作为所有传入操作的入口点和协调中心。它遵循以下四个步骤:
    1. 接受操作。通过 RPC 调用接收来自客户端服务的单个操作。
    2. 将操作分组到批次中。使用基于 Redis 的协调机制,按用户帐户将操作分组到时间限定的批次中(250 毫秒窗口)。
    3. 将任务放入队列。将批处理任务放入全局 Redis 任务队列中,供处理器领取。 
    4. 返回结果。当处理结果可用时,从 Redis 结果缓存中检索处理结果并返回给客户端。
  1. 一个 Redis 集群。它作为中央协调层,包含三个关键数据结构:
    1. 批量协调。使用原子 Lua 脚本管理实体到批次的映射、批次元数据和批次操作。
    2. 任务队列。维护一个需要由批处理处理器执行的任务列表。
    3. 结果缓存。保存处理结果以供客户端检索,从而在结果可用时实现快速响应。
  1. 批处理服务:负责处理繁重批处理执行工作的核心处理引擎。
    1. 选择任务。与其他区域的处理器竞争,以获取批量处理任务。
    2. 读取用户帐户。从数据存储中读取当前用户帐户状态一次。
    3. 处理批处理。将批处理中的所有操作应用于内存中的帐户状态。
    4. 存储暂定审计日志。使用批处理后处理服务,将批处理过程中生成的审计日志暂定存储。
    5. 更新用户帐户。使用乐观锁执行原子更新,以确保一致性。
    6. 持久化并发布暂定审计日志:触发批量后处理服务以永久存储暂定审计日志。
  1. 用户账户存储库。用户账户余额和状态的权威数据存储。系统旨在最大限度地减少与该存储库的交互——无论批次大小,批量处理服务每个批次仅执行一次读取和一次写入操作。
  1. 批量后处理服务:异步处理审计跟踪生成,以避免阻塞关键处理路径。
    1. 存储审计数据。根据请求存储暂定审计日志。
    2. 获取审计数据。检索生成审计日志所需的数据。
    3. 写入 UAC。在事务数据库中创建用户帐户变更日志条目。
    4. 发布用户活动控制 (UAC)。通过消息传递将审计事件发布到下游系统。
    5. 更新内部状态。将审计处理标记为已完成,以避免重复工作。
  1. 事务数据库。  存储符合 SOX 合规性和监管要求的不可变审计跟踪(UAC)。它与主处理路径分离,以确保审计操作不会影响性能。


优势


Uber设计的关键的架构优势和创新之处包括:

  • 亚秒级批处理。250毫秒的批处理窗口兼顾了实时操作的延迟和规模化所需的吞吐量。
  • 异步审计处理。将审计跟踪生成与关键处理路径解耦,可防止合规性要求影响性能。
  • 乐观并发。多个处理器竞争同一批次的处理任务,但通过乐观锁实现的原子更新确保了只处理一次,从而保证了处理结果的一致性。
  • 数据存储交互极少。无论操作数量多少,每个批次仅需一次读取和一次写入,从而大幅降低总延迟。
  • 多区域冗余策略。批处理处理器在多个可用区中运行,使用区域名称作为执行器标识符,这显著降低了中断或部署期间的批处理失败率。


图片
图 4:利用多区域冗余实现容错。 


开发中的挑战

开发过程中出现了两个技术难题和解决方案:时间同步和用户帐户大小管理。 

在分布式系统中,机器对时间的感知并不一致,而时间对于定义有时间限制的批次至关重要。为了解决这个问题,我们将托管每个批次的 Redis 实例指定为权威时间源。这消除了时钟同步问题,并确保了批次边界的一致性。

另一个挑战是,传统的审计日志会占用大量空间,这可能导致在高吞吐量处理期间用户帐户大小超出存储限制。为了解决这个问题,我们开发了 MicroUAC 格式,其大小小于 100 字节,远小于传统的审计日志,但同样能够保证幂等性。这一发现对于在高批处理频率下管理用户帐户大小至关重要,而这一发现源于生产测试。

容错保证


Uber针对各种故障场景设计了系统。单个操作的故障会被隔离,Redis 服务中断只会导致暂时性的服务不可用,而不会影响数据完整性。为了确保审计跟踪的数据持久性,我们结合了 Redis 的速度优势和 Docstore 的持久性。多区域冗余机制也保证了即使单个区域发生故障,其他区域的执行器也能继续进行批处理。

验证策略


在迁移或构建用于处理关键任务型、高吞吐量交易(例如批量订单处理或大规模财务更新)的新系统时,出错的可能性非常大。高吞吐量流程中的一个漏洞就可能导致系统性数据损坏。

为了保证完整性(所有工作均已完成)和正确性(所有数据均正确),我们采用了两种强大的技术:影子测试和压力测试。

影子测试是指将生产环境中实时流量的副本路由到新系统,而新系统的输出不会影响生产环境。其目的是将新系统的行为与可信的现有系统的行为进行比较。

验证过程分为两个关键领域:完整性验证和正确性验证。 

完整性验证步骤确保新系统按预期完成所有必要的工作项。我们验证以下内容:

  • 每笔交易都会发布并保存审计日志和交易记录。
  • 所有请求均已处理完毕,并正确反映在成功指标中。
  • 所有必要的副作用或后续交易均已正确生成。
  • 该系统不会留下任何悬空状态(例如,未清理的临时持有或锁定)。

在正确性验证过程中,我们会确认算术运算和状态变更完全正确。一个专门的验证服务会查询两次并行运行的结果。它会将账户 A(旧系统)的最终余额或状态与账户 B(新系统)进行比较。此外,它还会将审计日志中的状态变更与原始输入进行交叉比对,以确保每笔交易都能正确更新账户状态。最终的通过标准很简单:账户 A 的最终状态及其所有相关日志必须与账户 B 完全相同。

正常负载下的正确性还不够。高吞吐量系统必须在最大压力下也能完美运行。为了确定系统极限,我们使用了一种专门的流量放大流程。我们并非简单地镜像实时流量,而是对模拟流量应用一个倍增因子(例如 10 倍、15 倍)。这使我们能够模拟远超峰值生产量的突发活动。

此压力测试有助于我们确定最大可持续吞吐量(每秒请求数)、极端负载下的系统延迟以及隐藏的瓶颈或资源争用点(例如,数据库连接池和 CPU 饱和度)。

Uber 的场景应用案例


该系统旨在处理流量激增的账户,例如大型市场运营商处理数万笔批量调整交易,或者车队运营商产生大量日常交易、支付高峰或处理奖励。

该系统现在只需几分钟即可处理以前需要 21-24 小时才能完成的金融交易。这种性能的显著提升催生了以前无法实现的全新业务功能,并使系统能够在应对极端流量高峰的同时,保持运行的稳定性。

结语


在本篇文章中,我们详细介绍了Uber如何使用实时批处理技术,以每秒每个用户账户 30 次操作的速度处理财务更新,同时确保数据一致性。

Uber采用了三个服务架构,分别用于创建批处理、处理批处理和后处理批处理,从而确保生成审计日志。另外还讨论了如何保证容错能力并控制数据规模。这项新增功能减少了系统中的重试次数和相关噪声,显著提升了客户的最佳体验。

作者:场长

参考:

https://www.uber.com/us/en/blog/high-throughput-processing/

评论

我要赞赏作者

请扫描二维码,使用微信支付哦。

分享到微信