Uber 如何构建高吞吐支付账户处理系统:用 250ms 实时批处理解决“热账户”难题
本文是对 Building High Throughput Payment Account Processing 的整理与翻译。内容结构概览本文会按照以下脉络展开Uber 支付账户系统为什么会遇到“热账户”问题传统逐条处理为什么无法满足吞吐要求为什么 Uber 选择了 250ms 实时批处理而不是 Kafka 批处理、分片账户或地理共址新系统的整体架构Batch Creator、Batch Process、Batch Post-Processing一次账户更新从进入系统到完成审计的完整流程系统在一致性、审计、容错、延迟和吞吐之间如何权衡开发过程中遇到的两个关键问题时间同步和账户体积膨胀如何通过影子流量和压力测试验证金融系统的正确性这个系统在 Uber 内部带来的实际效果对后端高吞吐系统设计的工程启发一、问题背景支付账户系统里的“热账户”Uber 的核心支付平台叫 Gulfstream。它是一个集成式、符合 SOX 合规要求的支付平台底层遵循复式记账原则也就是每一笔资金流动都必须有来源账户和目标账户钱不会凭空产生也不会凭空消失。系统采用 job/transaction 形式的架构每天处理数百万级交易并通过 UAC也就是 User Account Changelog维护不可变的审计轨迹。(Uber)从业务上看这类系统最重要的目标不是单纯“跑得快”而是既要快又要保证金融数据正确。账户余额、交易记录、审计日志、幂等处理、失败恢复这些都不能出错。对于普通账户来说传统处理链路已经足够。但随着 Uber 业务规模增长一些特殊账户开始出现极端流量突发这些账户在短时间内会产生大量连续更新形成所谓的 hot-key 问题。所谓 hot-key在这里可以理解为大量请求集中打到同一个用户账户上导致这个账户对应的数据行或数据项成为瓶颈。系统整体也许还能承受很高的 QPS但某一个账户如果被频繁更新就会受到单账户顺序一致性、数据库读写延迟、锁冲突、审计写入等因素限制。到 2023 年Uber 发现有些用户账户需要承载远远超过系统原有限制的更新频率。原来的系统大约只能支持每个账户每秒 3 到 4 次更新但部分业务场景已经明显超过这个范围。(Uber)典型场景包括大型 marketplace operator 会对单个用户账户发起每天数万次批量调整这些更新在旧系统中可能需要 21 到 24 小时才能处理完。车队运营商每天可能产生数十万笔交易处理时间甚至超过一天。某些车队会在特定时间产生巨大的付款突发瞬间压垮处理管线。Uber for Business 的某些客户会在特定区域给乘客提供补贴这类活动也可能让单个企业账户承受每天数万笔交易。(Uber)这类问题的本质是单个账户的写入吞吐变成了系统瓶颈。普通的水平扩容对它帮助有限因为同一个账户的数据更新必须维持严格一致性不能简单把同一个余额拆散到多个地方随便写。二、传统逐条处理为什么不行在旧的处理方式中每一个账户更新操作都要经过多个顺序步骤单个操作进入系统 ↓ 读取用户账户状态 ↓ 检查处理历史 ↓ 更新用户账户 ↓ 写入审计日志 ↓ 返回结果原文给出的延迟范围大致如下读取用户账户状态30 到 70ms检查处理历史30 到 70ms更新用户账户60 到 160ms写入审计日志12 到 70ms这样算下来每个操作的总延迟大约是 130 到 370ms。(Uber)如果完全逐条顺序处理那么理论吞吐上限很快就会撞墙。130ms 一次操作理想情况下每秒也只能处理约 7.7 次370ms 一次操作每秒只有约 2.7 次。目标如果是每个账户每秒超过 30 次更新那么靠顺序处理根本不现实。更关键的是这里慢的并不是计算。账户加减、状态变更、校验逻辑本身通常很快真正耗时的是和数据存储之间的多次网络往返。每次操作都读一次、查一次、写一次、记一次审计相当于把数据存储的访问延迟重复支付了很多遍。这就引出了 Uber 这套方案的核心思路既然瓶颈是数据存储往返那么就把多个账户更新合并起来让一批操作共享一次读取和一次写入。三、核心选择250ms 实时批处理Uber 最终选择的方向是 batching也就是批处理。但这里的批处理不是传统意义上“等一段时间攒一批再慢慢处理”而是面向实时金融操作的短窗口批处理。原文中提到Uber 需要支持信用卡预授权等实时操作因此系统有一个约 1 秒的软限制。如果批处理窗口太长就会破坏实时体验如果窗口太短又攒不出足够多的请求吞吐提升有限。最终他们选择了 250ms 的批处理窗口。这个窗口足够短可以满足亚秒级实时处理要求同时又足够长可以在热账户场景下积累一批更新从而摊薄数据存储读写成本。(Uber)可以把这个思路理解为传统方式 操作 1读账户 → 写账户 → 写审计 操作 2读账户 → 写账户 → 写审计 操作 3读账户 → 写账户 → 写审计 操作 4读账户 → 写账户 → 写审计 新方式 250ms 内收集操作 1、2、3、4 ↓ 读一次账户 ↓ 在内存中依次应用 4 个操作 ↓ 原子写一次账户 ↓ 异步完成审计处理这样做的收益非常直接数据库访问次数从“每个操作多次访问”变成“每个批次一次读、一次写”。批次越大单个操作分摊到的存储往返成本越低。四、为什么不是 Kafka、分片账户或地理共址在设计新系统时Uber 并不是一开始就直接选择 Redis 加实时批处理。他们也评估过其他方案但最后都没有采用。1. 为什么不是基于 Kafka 的传统批处理Kafka 适合高吞吐流式处理但不一定适合这种亚秒级金融更新场景。原文提到客户端侧批处理会引入等待延迟消费者侧出现 1 到 2 秒延迟也很常见。Kafka 的优势是高吞吐但它不是为“低于 1 秒完成实时账户更新”这个目标专门设计的。(Uber)Uber 的目标是 250ms 级别的批处理窗口同时整体流程仍要满足实时业务。因此传统消息队列式的批处理不够合适。2. 为什么不是全局有序流处理另一种思路是用流处理系统维护全局顺序。但在分布式系统中维护全局顺序非常复杂而且需要频繁 compaction。对于金融账户系统来说顺序确实重要但真正需要严格顺序的是同一个账户内部的更新顺序而不是整个系统里所有账户的全局顺序。如果为了所有账户强行维护全局顺序会让系统复杂度和性能成本都变得很高。3. 为什么不是把单个账户分片还可以考虑把一个用户账户拆成多个 DynamoDB 行用多个分片分摊写入压力。但这个方案会破坏“单一余额”的概念。金融账户最核心的抽象就是账户余额。如果一个账户被拆到多个分片里那么余额查询、热账户检测、幂等控制、审计追踪都会变复杂。对于支付平台来说这种复杂度非常危险。4. 为什么不是地理共址还有一个优化方向是让服务和数据存储在地理位置上更接近减少网络延迟。这个方案确实能带来提升但原文提到它大约只能带来 2 到 3 倍改善而 Uber 需要的是 10 倍级别的吞吐提升。(Uber)所以地理共址只能作为优化手段不能解决根本问题。五、设计目标吞吐、一致性、审计、容错都要兼顾Uber 这套系统的目标不是简单把请求攒起来批量执行。因为它处理的是金融账户所以设计约束非常多。首先系统要支持每个用户账户每秒超过 30 次更新操作并且保持严格一致性。其次批处理窗口要控制在 250ms 左右在吞吐和实时延迟之间取得平衡。再者不管批次里有多少操作系统和账户数据存储之间都应该尽量只有一次读和一次写。(Uber)同时系统还必须保留不可变审计轨迹。对于符合 SOX 要求的支付平台来说审计日志不是可选项而是系统正确性的一部分。每次账户变化都要能够追溯不能因为性能优化而牺牲审计完整性。最后系统还要考虑 Redis 层的数据可靠性问题。Redis 被用作协调层和高速缓存层但它不能成为金融数据的最终可信来源。设计上必须做到即使 Redis 出现数据丢失影响范围也应尽量限制在单个操作而不是整批金融变更更不能破坏最终账户数据和审计数据的完整性。六、整体架构三个服务组成实时批处理系统Uber 新建的 User Account Batch Processing 系统采用三服务架构核心组件包括Client ↓ Batch Creator ↓ Redis Cluster ↓ Batch Process ↓ User Account Store Batch Process ↓ Batch Post-Processing ↓ Transaction DB / Audit Events这三个服务分别是Batch CreatorBatch ProcessBatch Post-Processing此外系统还依赖 Redis Cluster、User Account Store 和 Transaction DB。Redis 负责协调、任务队列和结果缓存User Account Store 是账户余额和账户状态的权威存储Transaction DB 用来保存不可变审计轨迹。(Uber)1. Batch Creator负责接收请求并组批Batch Creator 是整个系统的入口。它接收来自上游服务的单个账户操作然后按照用户账户维度把这些操作组织进有时间边界的 batch 中。它使用 Redis 来做协调原因是 Redis 具备亚毫秒级延迟和很高的 RPS 能力适合作为这种短窗口实时批处理系统的协调层。Batch Creator 主要做四件事第一接收来自客户端服务的 RPC 请求。第二按照用户账户维度把操作放进 250ms 时间窗口内的批次中。第三把批处理任务放入全局 Redis 任务队列等待 Batch Process 服务领取。第四从 Redis 结果缓存中获取处理结果并返回给调用方。这里有一个很重要的点Batch Creator 不直接完成账户更新它更像是入口和协调中心真正的账户状态变更由 Batch Process 执行。2. Redis Cluster协调层、任务队列、结果缓存Redis Cluster 在架构中承担三类职责。第一类是 batch coordination也就是批次协调。它需要维护 entity-to-batch 的映射关系也就是某个账户当前属于哪个 batch还要维护 batch 元数据以及 batch 中包含的操作列表。这些操作需要原子性原文提到这里使用了 Lua 脚本。第二类是 task queue也就是任务队列。Batch Creator 生成批处理任务后把任务放到 Redis 队列里Batch Process 服务再去竞争领取任务。第三类是 results cache也就是结果缓存。Batch Process 完成处理后把结果写入 RedisBatch Creator 再从这里读取结果并返回给客户端。Redis 在这里不是最终账本而是高速协调层。真正的账户状态仍然在 User Account Store真正的不可变审计轨迹仍然在 Transaction DB。3. Batch Process真正执行批处理的核心引擎Batch Process 是系统的核心处理引擎。它从 Redis 任务队列中领取 batch然后只读取一次当前账户状态把 batch 中的所有操作在内存中依次应用到账户状态上。处理完成后它通过乐观锁进行一次原子更新把最终账户状态写回 User Account Store。(Uber)这个流程可以概括为领取 batch ↓ 读取一次账户当前状态 ↓ 在内存中依次应用 batch 内所有操作 ↓ 生成临时审计数据 ↓ 使用乐观锁原子更新账户 ↓ 触发后处理服务完成审计持久化和发布这里的关键是“只读一次、只写一次”。不管 batch 中有 5 个操作、10 个操作还是更多操作与 User Account Store 的交互都尽量保持为一次读取和一次写入。4. Batch Post-Processing把审计从主路径中拆出去Batch Post-Processing 负责异步处理审计日志。它不阻塞主账户更新路径而是在 Batch Process 生成临时审计数据后负责后续的持久化和发布。它主要做五件事存储 batch 处理过程中生成的临时审计数据在需要时取回审计数据向 Transaction DB 写入 UAC也就是 User Account Changelog通过消息系统向下游发布审计事件更新内部状态标记审计处理已经完成避免重复处理这样做的好处是合规审计仍然完整保留但不会把审计写入延迟直接压到每次账户更新的关键路径上。七、端到端流程一次金融更新如何被处理把所有组件串起来看一次金融账户更新大致会经历以下流程。首先请求来自客户端层。原文提到的上游包括 Transaction Processor 和 Authorization Hold service它们都需要实时处理金融操作。然后请求进入 Batch Creator。Batch Creator 接收单个操作并根据用户账户将它加入对应的 250ms batch。这里会通过 Redis 管理账户到 batch 的映射、batch 元数据以及 batch 内的操作列表。当 batch 达到时间窗口或满足处理条件后Batch Creator 会向 Redis 任务队列中放入一个处理任务。接着多个 Batch Process 实例会跨可用区竞争领取任务。某个处理器拿到任务后会读取一次 User Account Store 中的账户当前状态。然后它会在内存中依次应用 batch 里的所有操作。这个顺序很重要因为账户变更通常不是简单可交换的。比如先扣款再退款、先授权再确认、先冻结再释放这些顺序都会影响最终状态和审计语义。处理过程中系统会生成临时审计日志并交给 Batch Post-Processing 处理。之后Batch Process 使用乐观锁对账户进行原子更新。如果账户版本没有被其他处理器改掉就提交成功如果发生冲突就通过一致性机制处理避免重复或乱序写入。账户更新成功后Batch Post-Processing 负责把审计数据正式写入 Transaction DB并发布 UAC 事件给下游系统。最后处理结果被写入 Redis 结果缓存Batch Creator 从缓存中取回结果并返回给调用方。通过这种方式系统把多次账户更新压缩成一次账户读取和一次账户写入同时仍然保留了完整的审计路径。八、性能收益单账户超过 30 ops/s原文给出的结果是这套架构可以支持每个用户账户超过 30 次更新操作每秒。由于多个操作共享一次数据存储往返batch 内每个操作的有效延迟可以降到 8 到 20ms而整个 batch 的总处理时间大约为 400 到 650ms。(Uber)这里要注意两个不同概念一个是单个操作的有效摊销延迟。另一个是整个 batch 从创建到完成的端到端时间。如果只看一次 batch它仍然需要经历收集窗口、读取账户、内存计算、原子写入、结果返回等步骤所以整体可能是几百毫秒。但由于一个 batch 中包含多次操作每个操作分摊到的数据存储成本明显下降。这就是吞吐提升的来源。这也是为什么批处理在高频热账户场景下非常有效它不是让单次数据库访问变快而是减少了数据库访问次数。九、架构上的几个关键创新点原文总结了这套架构的几个重要收益。1. 亚秒级批处理250ms 的 batch window 是整个系统的关键参数。它既不像离线批处理那样等待太久也不像完全逐条处理那样浪费存储往返成本。这个窗口让系统可以同时兼顾实时体验和高吞吐。2. 审计异步化金融系统不能不要审计但审计不一定要阻塞主处理路径。Uber 把审计日志生成和发布从关键路径中拆出来通过 Batch Post-Processing 异步完成。这是一种典型的工程权衡主路径负责账户状态的正确更新后处理路径负责审计数据的最终持久化和发布。只要设计好幂等、状态标记和失败恢复就可以既保证合规又降低主路径延迟。3. 乐观并发控制多个处理器可能会竞争同一个 batch 或账户更新任务。系统通过乐观锁和原子更新保证一致性。乐观并发的思路是先读取当前版本在内存中计算新状态写入时检查版本是否仍然匹配。如果匹配就提交如果不匹配说明有并发修改需要按系统策略重试或处理。对于金融账户来说这一点非常关键。批处理提升吞吐但不能以牺牲一致性为代价。4. 最小化数据存储交互不管 batch 中有多少操作都尽量只对 User Account Store 做一次读和一次写。这是架构层面的根本优化也是系统能从每秒 3 到 4 次更新提升到超过 30 次更新的核心原因。(Uber)5. 多可用区冗余Batch Process 运行在多个可用区。原文提到系统使用 zone name 作为 executor identifier这可以在可用区故障或部署期间显著降低 batch 失败率。(Uber)也就是说某个可用区出现问题时其他可用区的 executor 仍然可以继续处理 batch从而提高整体容错能力。十、开发挑战一分布式时间不同步第一个关键问题是时间同步。这套系统依赖 250ms 的时间窗口来定义 batch 边界。但在分布式系统中不同机器对“当前时间”的理解并不完全一致。即使使用 NTP也不能假设所有服务实例的时钟完全同步。如果每个服务都用自己的本地时间判断 batch 边界就可能出现同一个账户的操作被错误地分到不同 batch或者 batch 时间窗口不一致的问题。Uber 的解决方案是把承载某个 batch 的 Redis 实例作为权威时间源。也就是说batch 边界不由各个应用实例自己的机器时间决定而是由对应 Redis 实例提供统一时间判断。这样可以消除服务节点之间的时钟差异让 batch 的时间窗口定义更加一致。(Uber)这个设计很有启发。分布式系统里经常会遇到“谁说了算”的问题。对于这类需要统一时间边界的系统与其试图让所有机器时间完全一致不如明确指定一个权威时间来源。十一、开发挑战二账户体积膨胀与 MicroUAC第二个问题是用户账户大小管理。传统审计日志会占用较多空间。在高吞吐批处理场景下如果每次操作都把较大的审计相关数据挂到账户状态附近就可能导致用户账户数据体积超过存储限制。这类问题在低频场景下不明显但在热账户批处理里会被快速放大。一个账户如果每天有数万甚至数十万次变更任何“每次多写一点”的设计都会变成大问题。Uber 的解决方案是设计 MicroUAC 格式。原文提到MicroUAC 小于 100 字节比传统审计日志小得多但仍然可以提供相同的幂等性保证。这个发现来自生产测试是控制高频 batch 场景下账户体积的关键。(Uber)这里的思路可以理解为账户主路径上只保留最小必要信息完整审计数据交给后处理和审计存储系统。这样既能保留幂等和恢复能力又不会让热账户本身变得越来越大。十二、容错保证Redis 快但不能成为最终账本这套系统大量使用 Redis但 Uber 并没有把 Redis 当作最终可信账本。Redis 的定位是高速协调层用于 batch 协调、任务队列和结果缓存。在失败场景下系统需要满足几个要求。首先单个操作失败应该被隔离不能拖垮整批操作更不能导致整个账户状态损坏。其次Redis 故障可能会造成临时不可用但不能破坏数据完整性。也就是说系统可以短暂处理不了但不能处理错。第三审计轨迹的数据持久性要通过 Redis 和 Docstore 等机制结合保障。Redis 提供速度持久化存储提供可靠性。第四多可用区冗余要保证单个可用区故障时其他可用区的 executor 仍然可以继续 batch 处理。(Uber)这也是金融系统和普通高吞吐系统的区别。普通系统很多时候可以接受缓存丢失、消息重放、最终一致。但金融账户系统必须明确区分哪些数据可以临时丢哪些状态必须持久可靠哪些操作必须具备幂等和可恢复能力。十三、验证策略影子流量与压力放大对于金融系统来说新系统上线前最可怕的不是“性能不够”而是“跑得很快但算错了”。高吞吐管线里一个小 bug可能带来系统性数据污染。所以 Uber 在验证这套系统时关注两个目标completeness 和 correctness。completeness 可以理解为完整性也就是所有该做的工作都做了。系统要验证每一笔交易对应的审计日志和交易记录都被发布并持久化所有请求都被处理并正确反映在成功指标里所有必要的副作用或后续交易都被生成系统没有留下悬挂状态比如没有清理掉的临时 hold 或 lock。(Uber)correctness 可以理解为正确性也就是所有账户计算和状态变更都完全正确。Uber 使用一个专门的 Validation service对比新旧两套系统的输出。旧系统处理 account A新系统处理 account B然后比较两边的最终账户余额、账户状态以及审计日志中的状态变化是否和原始输入完全对应。最终通过标准很直接account A 和 account B 的最终状态以及所有相关日志必须一致。(Uber)除了普通流量下的正确性Uber 还做了压力测试。方法不是简单复制生产流量而是对影子流量进行放大比如放大到 10 倍、15 倍用来模拟远超生产峰值的突发流量。这样可以找出系统最大可持续吞吐、极端负载下的延迟表现以及隐藏瓶颈比如数据库连接池、CPU 饱和、资源争用等。(Uber)这个验证策略非常典型也非常值得借鉴生产流量 ↓ 复制一份影子流量 ↓ 旧系统正常处理真实结果 ↓ 新系统处理影子结果但不影响生产 ↓ 验证服务对比两套系统结果 ↓ 再将影子流量按 10x / 15x 放大做压力测试这比单纯压测接口 QPS 更严格因为它不仅关注“能不能扛住”还关注“扛住以后结果是不是完全正确”。十四、实际业务效果从一天缩短到几分钟这套系统主要服务于那些出现极端流量突发的账户比如大型 marketplace operator 的批量调整、车队运营商的大量交易、激励处理和支付突发等。原文提到以前需要 21 到 24 小时才能处理完的金融操作现在可以在几分钟内完成。这个提升不只是性能数字变好而是释放了新的业务能力。以前因为账户处理吞吐不足而无法承载的活动现在可以在保持一致性保证的前提下完成。(Uber)此外系统能力提升还减少了重试和相关噪声。对于大客户来说这意味着更稳定的处理体验对于平台来说也意味着更低的系统压力和更少的故障传播。十五、这篇文章真正值得关注的地方这篇文章表面上是在讲 Uber 的支付账户批处理系统但它背后其实有几个非常通用的后端工程原则。1. 热点问题不能只靠横向扩容很多系统遇到吞吐瓶颈时第一反应是加机器。但 hot-key 问题往往不能靠加机器解决因为压力集中在同一个 key 上。同一个账户、同一个用户、同一个订单、同一个商户、同一个库存 SKU都可能成为热点。当瓶颈集中到单个实体时系统要做的不是盲目扩容而是重新设计单实体的处理模型。Uber 的方案就是典型例子不是让更多机器同时写同一个账户而是把同一个账户短时间内的多次更新聚合起来用一次读写完成多个变更。2. 真正的瓶颈经常不是计算而是往返从延迟构成看账户状态计算本身不是问题多次访问数据存储才是问题。这个结论在很多后端系统里都成立。如果一个流程中包含多次 RPC、多次数据库读写、多次同步日志写入那么优化单个函数的 CPU 通常没有太大意义。更有效的方式往往是减少往返次数、合并请求、批量写入、异步化非关键路径。3. 批处理不等于离线处理很多人一听 batch就以为是分钟级、小时级的离线任务。但 Uber 这篇文章展示的是另一种思路实时批处理。250ms 的 batch window 本质上是一种工程折中。它牺牲了极小的等待时间换来了显著的吞吐提升。对于很多业务来说这种折中是完全可以接受的。比如风控事件、账户流水、积分变更、优惠券扣减、库存更新、消息聚合都可能适合类似的短窗口批处理模型。4. 金融系统优化不能绕开审计这套系统最难的地方不只是“批量更新账户”而是“批量更新账户的同时仍然保留完整、不可变、可追溯的审计轨迹”。如果只是普通计数器把多个更新合并写回很容易。但金融系统必须回答每次变更有没有记录失败后能不能恢复重试会不会重复扣款审计日志和账户状态是否一致下游系统能否看到正确事件合规要求是否满足Uber 的做法是把审计从关键路径中异步拆出但没有放弃审计本身。这是高吞吐系统里很重要的设计思想异步化不代表弱化只是改变执行时机和路径。5. 新旧系统迁移必须重视结果对比对于关键系统尤其是支付、账户、订单、库存这类系统迁移时不能只看新系统压测通过。更重要的是新旧系统在同一批输入下是否产生完全一致的结果。影子流量加结果对比是这类系统非常重要的上线手段。先让新系统处理真实流量的副本但不影响生产结果再用验证服务对比最终状态和日志。只有当完整性和正确性都满足要求才逐步切流。十六、总结Uber 这套 User Account Batch Processing 系统解决的是一个典型但很棘手的问题单个金融账户在极端业务场景下出现高频更新传统逐条处理无法满足吞吐要求而简单扩容、Kafka 批处理、账户分片、地理共址都无法同时满足实时性、一致性和审计要求。最终方案的核心是 250ms 实时批处理。系统通过 Batch Creator 收集并组织操作通过 Redis 做高速协调通过 Batch Process 对同一账户的一批操作执行一次读取、内存计算和一次原子写入再通过 Batch Post-Processing 异步完成审计日志持久化与发布。这套架构把单账户吞吐提升到每秒超过 30 次更新操作并把原本需要 21 到 24 小时处理的金融操作缩短到几分钟完成。同时它仍然保持严格一致性、不可变审计轨迹、幂等保证和多可用区容错。从工程角度看这篇文章最大的启发是当系统瓶颈集中在单个热点实体上时最有效的优化往往不是继续加机器而是改变处理模型。通过短窗口批处理把多次状态变更合并到一次权威读写中再把非关键路径异步化同时用严格验证保障正确性这才是高吞吐金融系统真正可落地的设计方式。需要的话我也可以继续把这篇改成更像“技术公众号爆款”的版本标题更抓人开头更有冲突感但内容仍保持技术严肃性。