为可靠性而生:美国运通如何大规模处理支付
测试你的授权流程,无需生产(赞助)
认证往往是应用中测试最少的部分。实时环境需要网络访问和真实凭证,而模拟环境则忽略了破坏生产的故障。
@workos/仿真在本地运行 WorkOS API,用于开发和自动化测试。种子用户、组织、RBAC 角色和单点登录连接,然后测试完整的 AuthKit 登录流程、签名的 webhook、令牌刷新和错误处理,且不涉及生产环境。
响应和事件形状来自 WorkOS OpenAPI 规范,所以你的测试运行的是应用在生产环境中使用的相同表面。本地运行,或在 CI 中运行,使用npm、Homebrew、Docker 或独立二进制文件。
客户在进行信用卡支付时,期望几乎能立即获得批准或拒绝。这一单一要求决定了支付的构建方式,因为整个支付处理必须在这短时间内完成。
我们最近与本·凯恩美国运通的一位杰出工程师,了解他们的平台如何处理这些问题。
为了理解事情的运作方式,让我们想象一下当一笔交易到达美国运通时会发生什么。交易会被路由到几个独立的处理单元之一,并通过一系列微服务链条传递。
然而,在过程中,其中一项服务开始失效,结账终端的客户仍在等待。
仅仅在故障单元内重试事务就存在明显风险。此外,将事务迁移到另一台服务器更是难题。这是因为第二个单元需要知道第一笔交易的传输距离。
共享这些知识会产生依赖关系,可能使两个独立单元变成脆弱的系统。
美国运通工程团队负责处理此事基于单元的架构这有助于将中断的影响降到最低。在与美国运通工程团队的交流中,我们了解到大部分工程工作都用于在压力下保持这种隔离性。
本文将尝试理解交易如何通过这种基于蜂窝的架构运行,以及即使某些服务出现故障,支付也是如何处理的。
核心支付生态系统
在典型的支付流程中,美国运通位于链条中间。商户与收单银行有关系,收单银行将交易发送给美国运通,美国运通则将交易转达给持有账户余额的发卡行。
这一中间跳跃是核心支付网络的工作。
请看下面的示意图,展示了这个设置:
2018年,美国运通开始将该平台现代化到云原生基础设施上。立刻,我们就明白了一个假设必须改变。旧系统运行在经过设计以保持运行的硬件上,而云基础设施的表现则不同。
服务器可能因任何一个团队无法控制的原因消失。你必须假设完全无法控制的故障会更频繁地发生,并围绕此构建应用。
考虑了两种熟悉的模式:
- 事件驱动处理并不适合。工作负载需要以低延迟和实时响应大规模运行,几乎没有延迟空间。异步工作存在,但答案本身仍需迅速到来。
- 大多数队伍都是从单体开始的,但它不太符合扩展要求。
美国运通工程团队最终选择的设计部分源于数十年的内部思考。Ben指出,早在SOA时代,也就是微服务概念出现之前,他的团队就已经在努力减少单次故障对特定系统的影响。
这些模式早在更正式的基于单元架构的术语出现之前就已存在。
单元格边界
在基于单元的架构中,单元是支付处理栈的完整、自给自足的复制品。换句话说,处理交易所需的一切都存在于一个边界内。这包括微服务、数据库、DNS及支持基础设施。
单元格有五个关键属性:
- 它独立部署并自行处理支付。
- 它拥有微服务、数据库及其他组件。
- 它形成了一个单一的失效域,因此内部的问题一直存在于内部。
- 它可以因维护或事故发生时退出轮换,平台的其他部分继续运行。
- 它在关键路径中没有同步的跨单元依赖关系。
参考数据仍然可以跨单元复制,可观察数据可以跨单元汇总,路由器实例之间也在单元间通信。在交易处理过程中,单元之间的阻断调用不会进入关键路径。
换句话说,单元是通过其故障边界定义的,而不是由特定的基础设施结构定义。单元仅停留在一个区域内,所有处理所需的本地设备都位于该边界内。
决定单元的合适大小需要判断力。美国运通的工程师认为这是一种平衡艺术。你不想把整个企业都放在一个单元里。理想情况下,最好划定支持的过程边界。
在支付方面,这意味着产生即时答案所需的最低要素。换句话说,就是交易的实时流程。事后流程仍可在另一个单元处理。
需要明确的是,单元不同于微服务。微服务按功能划分系统,而单元则通过故障划分系统。一个单元可能包含多个微服务。边界很重要,因为每当我们离开单元时,就进入了危险领域,需要合适的弹性能力、流程和业务逻辑来应对潜在的故障。
数据区域性
只有当小区所需的数据已经存在于其内部时,细胞才算自给自足。美国运通工程团队有三种处理数据的策略,根据数据变化的频率选择:
- 不可变数据只设置一次,之后保持不变。
- 半静态数据变化从每隔几小时到每年一次不等。例如,汇率、商户类别代码和国家代码。
- 每次交易数据都会变化。
对于前两类,美国运通在任何交易需要之前都会将参考数据分发到每个单元格。另一种选择是“穿透读取”,即查询会错过本地缓存,而是在交易等待期间移动到中央记录系统。
提前数据有多种好处:
- 第一笔交易避免了为冷缓存付费。
- 关键路径避免了同步呼叫离开小区。
- 复制工作完全运行在事务路径之外。
Ben将此与更常见的模式进行了对比。他说,他们倾向于推送分发,而不是传统的拉取后缓存,后者避免了第一个交易积累缓存。
然而,这种方法对动态数据则不适用。复制运行快速且异步。但当交易到达时,单元可能保持过期状态。将事务发送到数据过时的单元会增加延迟并冒处理失败的风险。
为了解决这种情况,美国运通颠倒了这个问题。平台不是将数据转移到交易中,而是将交易转移到数据中。
这通过确定性路由实现,意味着决策基于交易本身的内容,而非单元负载或可用性。该交易内容的一些示例属性包括合作伙伴、市场和支付类型。
一个称为全局交易路由器的组件在前门做出决策。我们将在下一节中更详细地探讨。
确定性路由是两种模式之一。路由器还支持基于优先级的路由,即小区承载特定排序,流量流向最高优先级的健康小区。所采用的模式选择取决于具体的使用场景。
然而,这种路由并非通用规则。美国运通选择性地应用确定性路由,交易之间需要强一致性。这取决于交易类型。部分类型数据需求极少,可以自由路由。
基于消息的单元间复制贯穿始终,因此故障切换数据存在多个地点。时序是关键,因为每次在运行中事务都无需等待复制完成即可进行。
全局事务路由
全局事务路由器同时路由流量并执行小区边界。
每笔交易都通过路由器进入一个小区,任何移动到不同小区的交易也会通过该小区返回。小区之间没有直接通信的能力,这使得路由器成为它们之间的单一路径。
这种设置的结果是形成了一个支付网格,能够将小区全球连接起来,同时保持它们的独立性。
请看下面的示意图:
路由器以相同条件处理外部流量。当小区完成工作且交易需要到达发卡机构时,小区会将其退回给路由器。路由器随后发起该外呼。小区不在自身边界外通信,无论目的地是其他小区还是外部机构。
将如此多的责任集中在一个组件上,显而易见的问题是:一个每笔交易都依赖的组件,如何避免成为平台最薄弱的环节?
美国运通工程师的回答可以分为几个部分。
第一是故意让路由器保持简单。它们不包含业务逻辑,只赋予路由器足够的消息解析能力,提取特定值并路由。这些值代表什么的知识超出了其职责范围。
输入因交易类型而异,有时是头部值,有时是卡片交易消息内的字段。
保持这种约束至关重要,因为与支付逻辑深度集成的路由器会积累业务规则。这些规则需要数据,而这些数据需要查找。组件将逐渐变成集中式系统,而基于小区的架构正试图避免这种情况。
第二个方向是减少路由器依赖的设备。以下是一些需要注意的事情:
- 依赖关系保持最小。组件越靠近边缘,携带的依赖越少。
- 状态不存在于持久存储中。路由器状态存储在非持久存储中,这使实例尽可能接近无状态。
- 剩余的依赖以异步方式运行。日志使用带有缓冲区截断策略的异步日志记录器,因此满缓冲区会丢弃日志记录,而不是阻断事务处理。
- 配置加载到内存并异步更新,因此无法访问的配置服务会让路由器运行在最后已知的良好值上。
- 实例在不同区域并行运行。多个实例、多个区域和多个连接意味着不可用路径总有备份。
在讨论中,Ben 也明确表达了部署态势的偏好。他说他会尽量避免主动待命,并尽可能让这种状态运行,因为主动待命主要在有州需要管理时才会占用。
这个教训不仅适用于支付问题。有些瓶颈是结构的一部分,因为架构需要一个地方进行跨界决策。解决这个问题的最佳方法是让瓶颈足够简单且易于信任,并用工程预算保持它。
信用卡授权流程
现在让我们看看美国运通团队提供的卡片授权流程,以更清楚地理解其运作方式。
交易流程从美国运通系统之外开始。一张卡在商户的销售点终端使用。接下来,流程如下:
- 全局事务路由器首先接收请求。
- 路由器采用确定性路由或基于优先级的路由。选择取决于使用场景。
- 在单元内,一组微服务执行验证、丰富、转换和发行者确定。
- 小区将交易返回给路由器。
- 路由器会将请求发送给相应的发卡机构以获得授权。
- 发卡机构的回复会回到路由器。
- 路由器采用确定性路由,访问持有交易上下文的小区以便授权。
- 细胞会验证反应。
- 交易会被传回商家的收单银行。
- 付款的最终确认会被发回POS终端。
需要注意的是,小区从不直接联系发卡机构。这保持了小区不应与其边界之外的任何设备通信的规则。
请看下面的详细流程图:
交易中途失败
现在让我们回到文章开头提到的交易。
美国运通的支付处理采用编排微服务架构。这意味着一个编排微服务负责管理工作流程,并依次调用其他微服务。该编排器还持续监控这些微服务的健康状况并检测故障。
以下是当所需服务开始失败,或运行的该服务实例数量不足以处理时,发生的完整事件顺序:
- 编排器检测到故障并停止处理。
- 它会将交易返回到全局事务路由器。
- 路由器选择一个健康的小区。
- 处理在该单元格中重新开始,使用原始的交易数据。
设计分歧在于最后一步。美国运通放弃了部分工作。失败单元完成的每个微服务调用都会被丢弃,第二个单元从头开始,输入相同。这种重定向涵盖了新交易到达故障单元和已在运行中的交易。
这种方法背后的理由可以追溯到边界问题。恢复事务需要第二个单元格读取第一个单元格的状态。然而,该链路会在单元之间创建共享状态。
共享状态会带来故障切换时的同步问题和一致性风险,而这正是该架构想要避免的问题。
这种选择的结果是单元保持松耦合。每个单元运行自己的数据库集群,单元内的微服务仅与本地集群通信,重定向事务处理时完全不依赖前一个单元的状态。
编排器还会对服务网格的准备端点运行应用健康检查。当整体单元健康度超过可接受阈值时,编排器报告该单元不可用。
重做已完成的工作听起来很浪费。但这种权衡是可行的,因为付款很短,所以重做几百毫秒的成本远低于每对单元之间永久结构依赖的替代方案。
在一个工作单元运行数分钟的系统中,检查点作为一种更有利的做法可能更为可取。
恢复语义学
在其他地方重新开始交易可以做到某个特定时刻。可以把它想象成一个不可回头的点。那个时刻的具体位置是团队需要做出的设计决策。
例如,只要交易仍处于核心支付生态系统内,重新路由是安全的,一旦交易进入了外部系统(如发卡机构),它就会保持原位。
卡片授权的结构使得不可回头点落在处理结束时。美国运通在支付流程允许的最晚时间放置不可逆步骤,使可恢复的窗口尽可能宽。
对于无法延迟重定向交易的支付类型,安全性来自幂等性。
每笔交易都携带一个唯一标识符,每次重试和重路由都一致,下游系统利用该标识符抑制重复。备用回拨功能通过百分比控制。它们避免在小区返回时立即将所有数据退回。
此外,按百分比调整流量让美国运通能够逐步耗尽小区,验证部分负载下的恢复小区,或在事件发生时谨慎响应。
但这又引出了另一个问题:平台如何防止恢复的单元在其交易转移到其他地方后写入过期状态?
这里有三种机制可以缩小窗口:
- 速度:信用卡支付速度极快,以至于当故障的手机返回时,交易通常已经在别处完成。
- 幂零标识符:重复抑制会在下游发生,使用随交易一起传输的标识符。
- 有节奏的恢复:基于百分比的流量控制控制恢复单元的工作时间和量。
具体细节可能有所不同。金融领域的恢复可能意味着等待某项同步、接受最终一致性,或根据交易类型和失败发生地点创建补偿性交易。
设计权衡
现在让我们看看美国运通在架构上的权衡。
- 重复服务:强制执行边界有时会导致同一服务在小区间重复实现。美国运通接受这种成本,因为它保留了小区独立性并消除了跨小区网络跳跃。自然地,随着平台规模的扩大,防止跨小区依赖变得更难,因此成本随时间增加。
- 丢失日志记录:缓冲区截断策略意味着在持续压力下,平台会继续处理事务,同时丢失部分记录。需要说明的是,这些是用于一般可观察性的应用日志,而非关键事务事件。
- 全球可见度延迟:每个单元首先在本地写入日志、指标和追踪,并异步地汇聚到全局仪表盘。丢失部分堆栈会降低单个单元的可见性,而非整个平台。全局视图总是延迟。
- 被拒交易:单元格之间的数据同步是异步的。一致性要求取决于事务类型和业务规则。当事务需要强一致性,但所需数据无法验证或不一致时,美国运通可能会拒绝该交易以维护数据完整性。
另外,关于单元需要注意的是,它们改变的是同时故障的数量,而不是故障发生的频率,因为运行更多独立单元可能会带来更多单独故障。
换句话说,管理开销和复杂性被用来减少故障的影响。
结论
由美国运通工程团队打造的支付平台能够幸存下来,是因为设计不允许一笔交易同时依赖两个小区。
以下是使它奏效的关键点:
- 两种数据策略,一个目标:变化的参考数据很少会提前推送到每个单元格,而不断变化的数据则保持原位,交易会传送到那里。
- 瓶颈处的薄部件:全球事务路由器承担着巨大的责任,逻辑却很少,这使得它可靠。
- 用简历重新开始:丢弃部分功会花费几百毫秒,并且会移除连接每个单元格的共享状态。
- 带有选定位置的恢复窗口:将不可回头点设在序列后期,会扩大失败存活的范围。
- 域恢复语义:模式提供了结构,业务逻辑则提供了部分完成交易所需的规则。
参考文献: