在大规模分布式系统中,将关键系统从一种架构迁移到另一种架构,不仅在技术层面存在挑战性,还需要一个精细的迁移过程。Uber 运营着全球最复杂的实时履约系统之一。本文将介绍 Uber 如何将工作负载无缝地从本地环境迁移到混合云架构,并实现零停机时间和最小的业务影响。
系统复杂性
Uber 的履约系统是一个具备实时性、一致性和高可用性的系统。用户持续不断地与应用程序发生互动,如发起新行程、取消已有行程和修改行程细节。
餐厅不断更新订单状态,快递员在繁忙的城市中穿梭,确保包裹能够及时送达。系统每秒钟处理超过两百万次交易,高效地管理着平台上用户和行程的状态。
我是将这个履约系统从本地架构迁移至混合云架构的技术负责人。除了开发新系统所需的代码并进行验证之外,最具挑战性的部分是设计一个可以实现零停机时间并将对客户影响减少到最小的迁移策略。
旧系统架构
让我来概述一下原有的履约系统。它由多个服务构成,这些服务在内存中维护用户和行程实体的实时。此外还有一些辅助服务负责处理锁定机制、搜索功能和数据存储,确保系统能够跨数据中心进行数据复制。
所有的乘客端交易都被定向到“需求”服务,而所有的司机端交易被定向到“供应”服务。这两个系统通过分布式事务技术来保持同步。
这些服务以 pod 的形式运行。一个 pod 就是一个自给自足的单元,内部的服务相互交互,共同为特定城市的履约活动提供支持。一个请求进入一个 pod,通常会在这个 pod 的内部服务之间流转,除非需要访问 pod 外部的服务数据。
在下图中,绿色和蓝色服务组分别代表两个不同的 pod,每个 pod 包含了所有服务的副本。城市 1 的流量被路由到 pod1,城市 2 的流量被路由到 pod2。
此外,该系统还通过采用 saga 模式促进了 A、B 和 C 服务之间的分布式事务,确保了它们存储的实体数据保持同步。通过内存数据管理和序列化技术,每个服务内部的实体数据一致性得到了有效的保证。
这个系统在 Uber 的早期发展阶段就进行了扩展。它采取了一些架构决策,这些决策可能更贴合 Uber 不断增长的业务需求。首先,系统被设计为优先考虑可用性而非一致性。也就是说,在多个系统中保存了不同的实体,系统最终能够达到一致状态,但并没有通过真正的 ACID 兼容系统来管理跨系统的变更。由于所有数据都存储在内存中,系统在垂直扩展和特定服务的节点数量方面存在固有的限制。
重新设计的系统
新系统减少了服务数量。原本处理“需求”和“供应”等实体的服务被合并到一个由云数据存储提供支持的单体应用程序中。所有的事务管理的责任都被转移到了数据存储层。
新系统和已有的服务消费者
对于这种关键系统的迁移,需要一个全面的多维策略来覆盖迁移的方方面面。在微服务环境中,每个系统都与周围的许多系统进行着复杂的交互。交互主要通过两种方式进行:API 和发布到消息总线中的事件。
新系统修改了所有的核心数据模型、API 契约和事件模式。因为有数百个 API 调用者和系统消费者,所以不可能一次性迁移完毕。我们采取了一种“向后兼容层”的策略,保持现有 API 和事件契约不变。
创建一个向后兼容层使得系统可以在不中断现有消费者使用旧接口的情况下进行重新架构。在接下来的几年中,消费者可以根据自己的节奏逐步迁移到新的 API 和事件模式,不必与整个系统的重新设计紧密耦合。同样,旧系统的事件消费者也将继续通过向后兼容层以旧模式接收事件。
实体的生命周期
这个过程的复杂性在于不断变化的实体状态。以系统中的三个关键实体——乘客、司机和行程为例,它们可以在不同的时间点开始和停止。旧系统将这些实体存储在不同的内存系统中,而新系统必须在数据库中反映这些变化。
迁移过程的一个主要挑战是确保在过渡期间,每个实体的状态及其相互关系都能被保留,并且能够适应高频率的变化。
迁移策略
在迁移的每个阶段——发布之前、发布期间和发布之后——都必须采用多种策略,确保流量能够平稳地从现有系统转移到新系统。我将在文章的后续部分进行详细的讨论。
发布之前
影子验证
确保新旧系统间的 API 契约一致性至关重要,这有助于建立信心,保证新系统与现有系统的一致性。引入向后兼容性验证层可以确保旧系统和新系统之间 API 和事件的一致性。
每个请求都会被发送给旧系统和新系统。两个系统对请求的响应将根据键值进行比较,差异被记录到一个可观测性系统中。在每次发布之前,我们的目标是确保两个系统的响应完全一致。
在实时系统中确实存在一些细微的差别。特别是在即时系统中,并非所有调用都会成功并获得确切的响应。一种策略是将所有成功的调用视为一个具有高匹配率的群组,并确保大多数 API 调用都是成功的。在某些有效的情况下,对新系统的调用可能会失败。例如,当司机请求离线 API,而新系统中没有该用户的上线记录,可能会出现客户端错误。对于这些边缘情况,我们可以暂时忽略一致性不匹配的问题,但同时需要保持警惕。
此外,只读 API 的一致性是最容易实现的,因为它们没有副作用。对于写入 API,我们引入了一个系统,旧系统会在共享缓存中记录请求响应,新系统会重放这些响应。因为我们有跟踪标头,新系统可以透明地获取旧系统最初接收的响应,并将它们从缓存中传输到新系统,而不是进行全新的外部调用。我们因此可以在不受外部依赖影响的情况下更好地匹配新旧系统的行为,在响应和字段方面实现更高的一致性。
端到端集成测试
大规模重构是提高端到端(E2E)测试覆盖率的一个好时机。这样可以确保新系统在流量增长和新代码部署时的稳定性。在此次迁移过程中,我们将测试用例数量增加至 300 多个。
端到端测试应在开发生命周期的多个关键节点执行:在工程师编写代码时、在构建过程的预部署验证阶段、以及在生产环境中持续进行。
金丝雀黑盒测试
为了最小化潜在的负面影响,可以先只将代码暴露给一小部分请求,避免不良代码造成广泛影响。为了进一步优化策略,可以只在预生产环境中部署代码,并在这个环境中持续执行端到端测试。只有在金丝雀测试环境成功通过所有测试后才允许部署系统继续推进到生产环境。
负载测试
在将大量生产负载转到新系统之前,必须进行全面的负载测试。由于我们能够通过向后兼容层将流量重定向到新系统,所以可以将全生产流量转到新系统。此外,我们编写了自定义负载测试脚本来验证包括数据库和网络系统在内的独特集成点。
预热数据库
云服务供应商的数据库和计算资源扩展通常不是即时完成的。在面对突然增加的负载时,系统的扩展可能跟不上需求。在分布式数据存储环境中,数据的再均衡、压缩和分区拆分可能会引发热点问题。为了应对这些挑战,我们通过模拟合成数据负载实现了一定程度的缓存预热和分区拆分。这样可以确保在生产流量流向这些系统时系统能够平稳运行,无需突然进行系统扩展。
回退网络路由
由于应用程序和数据库系统分别部署在两个不同的云平台上,且一个在本地,另一个在云端,因此管理网络拓扑就变得尤为重要。两个数据中心至少建立了三条网络路由,并通过专有网络连接数据库。用超过三倍正常容量的流量对这些网络进行验证和负载测试,确保在生产环境中即使在高负载条件下网络连接也能保持稳定和可靠。
发布期间
流量固定
多个应用程序在相同的行程中相互协作,司机和乘客的行程代表同一个行程实体。当 API 调用从乘客端转移到新系统时,相关的行程和司机状态也必须从旧系统中迁移出来。我们已经实现了路由逻辑,确保行程能够持续进行并在同一个系统中完成。为此,我们在迁移执行前约 30 分钟开始记录所有消费者的标识符,确保相关实体的请求会被锁定在同一系统中。
分阶段发布
我们对选定城市的首批迁移行程进行了端到端测试。随后,我们将非活跃行程中的空闲乘客和司机迁移至新系统。迁移完成后,所有新的行程都只在新系统中启动。在这一过渡期间,两个系统并行提供服务。随着旧系统中的行程逐渐结束,乘客和司机也将逐步迁移至新系统。在一小时内,整个城市的服务完全转移到新系统。若新系统出现故障,我们可以执行相同的迁移流程,反向将服务迁移回旧系统。
发布之后
可观测性和回滚
在两个系统间进行切换时,确保健壮的跨系统可观测性至关重要。为此,我们开发了一个仪表盘,它能够实时显示流量逐渐从旧系统迁移到新系统。
我们的目标是在迁移过程中确保关键业务指标——行程量、可用供应、行程完成率和行程开始率——保持稳定。这些聚合指标应该在迁移过程中保持平稳,尽管在迁移的关键时刻旧系统与新系统的混合比例可能会发生变化。
由于 Uber 在数千个城市运营,并提供众多功能,因此,对这些指标进行城市级别的细致观察至关重要。在迁移数百个城市的过程中,手动监控所有指标是不现实的。我们必须开发专门的工具来自动监控这些指标,并能够突出显示那些显著偏离正常范围的指标。简单的警报系统也是无效的,因为大量没有根据每个城市的流量特点进行适当调整的无效警报会削弱对迁移过程的信心。因此,我们需要一个静态工具来深入分析每个城市的健康状况,确保所有城市在迁移过程中的平稳运行。
如果一个城市显示出不健康的迹象,我们可以选择只将该城市回滚到旧系统,而让其他城市继续运行在新系统中。
生产环境黑盒测试
与“金丝雀黑盒测试”策略类似,我们应该持续在生产系统上运行相同的测试,以便及时发现潜在问题。
成功迁移的关键要素
确保旧系统拥有充足的运行时间至关重要,这有助于我们专注于新系统开发而不受干扰。在着手新系统开发之前,我们花了四个月时间来提升旧系统的稳定性。跨项目规划至关重要,我们先在旧系统中开发功能,随后,在新系统开发过程中,这些功能在两个系统中并行开发,最后在在新系统中开发。为了避免长期同时维护两个系统,这种平衡策略至关重要。
在迁移关键系统时,确保这些系统具备强大的一致性保障,这是保证业务连续性的关键。40% 左右的工程资源应该放在新架构的可观测性、迁移工具和强大的回滚机制上。
关键技术要素包括:对 API 流量的全面控制,精确到单个用户级别;强大的字段级一致性;影子匹配机制;健壮的跨系统可观测性。在迁移过程中,我们需要面对充满竞态条件和特殊处理的情况,可能需要明确的业务逻辑或产品开发策略来应对。
流量向新系统的迁移在开始时保持平稳,并在迭代中逐渐增长。随着对新系统的信心增强,流量迁移往往会呈现出指数级的加速,如下图所示。
提前加速迁移的挑战在于,你可能会陷入同时维护两个同等重要的系统,这需要更长时间的维护。由于新系统规模的扩大,任何中断都可能对生产用户造成重大影响。因此,最佳策略是在将流量迁移到新系统之前,先对最小数量的功能集进行验证,在一段时间内保持稳定,然后在较短时间内快速增加流量迁移。
在 Uber,我们在开发和迁移过程中仅影响了不到几千名用户。本文所讨论的技术是我们从旧系统平稳过渡到新系统的关键。
原文链接:
评论