聚焦未来十年发展 开启重构之路 Platform Uber Fulfillment (聚焦未来发展)

聚焦未来十年发展 开启重构之路 Platform Uber Fulfillment (聚焦未来发展)

Uber Fulfillment 简介

Uber 的使命是帮助我们的消费者轻松到达世界上成千上万个城市的任何地方,获取任何东西。关键在于,我们捕捉到了消费者的意图,并通过将其与一组合适的供应商匹配来实现。

Fulfillment 是“向客户提供产品或服务的行为或过程”。Uber 的 Fulfillment 组织开发平台来协调和管理正在进行的订单和用户会话的生命周期,并拥有数百万的活跃参与者。

关键性与规模

Fulfillment Platform 是 Uber 的一项基础能力,它能迅速扩展新的垂直领域。

对 Fulfillment Platform 的最后一次重大改写是在 2014 年,当时 Uber 的规模要小得多,Flufillment 场景非常简单。Uber 的业务已经扩展到了移动和交付等多个垂直领域,能够满足各种不同的 Flufillment 体验。例如,预先确认司机的预订流程、同时提供多个行程的批处理流、机场的虚拟排队机制、Uber Eats 的三方市场、通过 Uber Direct 交付包裹等等。

两年前,我们大胆下注,开始重写 Fulfillment Platform,因为在 2014 年构建的架构不能适应 Uber 未来十年的发展。

本文是一系列文章的第一篇,介绍了 Uber 的基础业务平台的重构的过程。通过对存储和应用架构、域数据建模、API 和事件的重构,在超过 30 个团队的 100 多名工程师的支持下,将 Uber 的每一个产品和城市都成功迁移到新的技术栈。

Fulfillment 的早期架构

本节介绍了我们在之前 Fulfillment 栈中用于实现简单 UberX 流的数据模型和架构。

图 2:用于简单 UberX 流的高级架构

域建模

整个 Fulfillment 模型围绕着 2 个实体:Trip(行程)和 Supply(供应)。rt-demand 服务管理 Trip 实体,而 rt-supply 服务管理 Supply 实体。Trip 表示工作单元(例如,从 A 点发送一个包裹到 B 点),而 Supply 实体代表了能够完成该工作单元的现实世界的人。

Trip Entity

Trip 实体包括至少两个航路点。一个航路点代表了一个地点以及在该地点可执行的一系列任务。简单的 UberX 行程通常有一个上客点和下客点,而多目的地的行程有一个上客点和下客点,中间还有额外的“途径”航路点。

Supply Entity

Supply 实体对司机/送货员正在进行中的会话的状态进行建模。Supply 实体可以在一次或多次的行程中有一个或多个航路点,并按时间顺序完成。

读写模式

从高的层面来说,基于传入的请求进行读取-修改-写入,服务可以使用三种读/写模式:

并发读取-修改-写入同一实体:例如,一个试图下线的司机和一个想把新的行程报价与他联系起来的匹配系统。涉及多个实体的写入:若司机接受了行程报价,则必须修改 Trip 实体和 Supply 实体,并向 Supply 实体的计划中添加行程的航路点。涉及多个实体的多个实例的写入:如果一个司机接受了一个具有多个行程的批量报价,所有相关的实体都需要以全有或全无的方式更新。

应用架构

rt-demand 和 rt-supply 服务是与存储在 Apache Cassandra® 和 Redis 键值表中的实体无共享的微服务。接下来的小节,我们将介绍构成应用和存储架构的关键组件。之前的架构将可用性置于一致性之上。

图 3:基于 Ringpop 的服务架构剖析

用 Pod 隔离故障

尽管 Uber 的大多数架构运行在 2 个独立区域的 2 个故障域上,但是为了进一步减少爆炸半径,我们创建了一个 Pod 的概念,它包含了一些必要的基础服务来执行 Flufillment 流。每一个地区都有多个 Pod,而城市则根据不同的标准映射到 Pod。

市场存储网关

Pod 内的所有服务都利用了市场存储网关(Marketplace Storage Gateway,MSG)提供的 KV 存储 API。MSG 对底层存储进行抽象,并利用了 Cassandra 集群。为达到更高可用性服务水平的目标,MSG 采用了冗余的存储集群(即,一个应用程序的写入会导致 Cassandra 写入区域内的两个不同的集群)。在每个集群中,都有 3 个数据副本。区域集群支持跨区域的异步复制。

使用 Ringpop 和串行队列的应用层锁定

(已归档项目)是一个协作和协调分布式应用的库。它根据成员协议维护一个一致的哈希环,并提供请求转发作为一种路由方式。

Ringpop 在读取-修改-写入周期实现了应用层面的序列化,因为每个键都有一个唯一的工作器。Ringpop 以一种“尽力而为”(可用性高于一致性)的方式,将与某个键有相关的请求转发给其所属的工作器。每一个基于 Ringpop 的服务实例都有一个单线程的执行环境(由于 Node.js),一个根据到达顺序排列传入请求的序列队列,以及一个对象的内存锁。

将 Ringpop 和 Redis 用于缓存

有两种缓存形式:Redis 为应用程序管理的 MSG 提供后备,而内存缓存用于减少 Cassandra 集群的负载。读取操作主要由内存中的缓存提供。

提交后操作和计时器

大部分的事务都需要保证操作在提交之后被执行,并且在适当的时候(比如报价到期)触发调动定时器。这是用框架实现的,这个框架提供了一种方法,当 hashring 中的一个节点宕机时,或另一个节点取得键空间的所有权时,可以移交某些对象的所有权。

使用 Saga 的多实体事务

Saga 提供了跨多个服务实现业务事务的模式。我们利用这种模式实现了跨多 Trip 实体和 Supply 实体的交易。为实现多数据存储、多服务操作,它提供了应用层的事务语义。Saga 协调器将首先触发所有参与的实体的提议操作,如果它们都成功,就会提交;否则,它将触发取消操作来执行补偿操作。

使用独立的 Cassandra 表的二级索引

为查找给定的乘客的行程,rt-demand 维护了一个单独的表,该表将乘客映射到行程标识符的列表中。由于 rt-demand 是用标识符分片的,对于一个给定的行程标识符的所有请求都被转到包含该行程的工作器中,而对于某个给定的乘客标识符的所有请求将被转到拥有该乘客的工作器那里。在创建行程时,该行程首先被保存到乘客索引表中,然后保存到行程表中,并向拥有乘客的工作器发送一条请求,以便使缓存失效。

先前架构的问题

基础设施问题

一致性问题

整个架构是以一致性为交换可用性和延迟为前提的,因此一致性只能通过尽力而为的机制来实现。缺少原子性意味着我们必须在第二个操作失败时协调操作。对于“大脑分裂”(部署过程中的区域故障),不一致可能由于并发的写入操作而发生,这种不一致性可能最终会相互覆盖,因为 Cassandra 显示了最后写入的语义。

多实体写入

如果操作需要跨多个实体写入操作,那么应用层可以在任意的基于 RPC 的机制中处理这种协调,并且持续验证预期的状态和当前的状态,以解决任何不匹配的问题。构成逻辑事务的操作之间,系统处于内部不一致的状态。在使用 Saga 模式构建更复杂的写入操作流时,调试跨越多个实体和服务的问题变得更加困难。

可扩展性问题

城市被分散到一个可用的 Pod 中,Pod 的大小取决于 Ringpop 集群的最大环大小。鉴于协议的点对点性质,Ringpop 有物理限制。这就是说,如果任何一个城市都超过了并发行程的阈值,那么将存在一个垂直的限制,以扩大 Pod 的规模。

应用问题

过时语言和框架

2018 年,Uber 不再推荐 Node.js 和 HTTP/JSON 框架。新工程师被迫了解遗留应用程序框架、不同的编程语言,以及雪花般的 HTTP/JSON 协议,以便在技术栈中进行更改,极大的增加了入职成本。

数据不一致

先前的架构采用分层的数据存储方式。分散式内存缓存缓冲区提供了第一层,Redis 和 MSG(使用镜像的 Cassandra 集群)作为第二层。分层方法具有高性能和冗余,但代价是保证缓存的一致性。在本地缓存中的缓冲数据变化,当缓存不一致时,缓存变得更加复杂。

可扩展性模式不清晰

近几年来,有 400 名工程师对核心 Fulfillment Platform 进行了改造。在没有清晰的扩展模型和开发模式的情况下,一个新的工程师很难理解整个流程,并且自信且安全地进行修改。

Fulfillment 的新架构

我们花了 6 个月的时间仔细审核了技术栈中的每一件产品,收集了利益相关者团队的 200 多页需求,用数十种评估标准广泛辩论了架构选项,对数据库选择进行了基准测试,并对应用框架选项进行了原型设计。在完成了一些关键的决定之后,我们为未来十年的需求提出了一个整体的架构。本节对新的体系结构进行了高级概述。

新架构的要求

从 NoSQL 迁移到 NewSQL

存储抽象化解决方案集中在 3 种方法上:

为满足事务一致性、横向可扩展性和低操作费用等要求,我们决定采用 NewSQL 架构。Uber 还没有在此项目之前使用过基于 NewSQL 存储的先例。经过全面的基准以及对可用性 SLA、操作开销、事务能力、模式管理、分片管理、自动扩展和横向可扩展性等方面进行彻底的基准测试和仔细评估后,我们决定以 Google Cloud Spanner 为主要的存储引擎。

Spanner 作为事务数据库

我们使用 Spanner 的北美多区域配置作为 Flufillment 实体的存储引擎。Fulfillment 服务运行于 Uber 北美运营区,每个事务对部署在 Google Cloud 的 Spanner 进行一系列的网络调用。

Flufillment 依赖于 Spanner 所公开的一些核心能力,例如:

图 4:新 Flufillment Platform 的存储拓扑

提交后操作

Spanner 的公开版本目前并不支持开箱即用的变更数据捕获。为了给提交后的操作提供至少一次的保证,我们构建了一个名为 Latent Asynchronous Task Execution(LATE,潜在异步任务执行)的组件。所有的提交后操作和计时器都与读写事务一起提交到一个单独的 LATE 操作表中,该表指出了所有要执行的提交后操作。LATE 应用工作器从这个表中扫描并拾取行,并保证至少执行一次。

在本系列的第二篇文章中,我们将介绍如何选择正确的存储、评估需求和在 Uber 内部操作 Spanner。

程序设计模型

由于 Uber 项目的规模越来越大,产品流程也越来越复杂, Fulfillment Platform 的编程设计模型必须提供简单、模块化、可扩展、一致性和正确性,以确保 100 多个工程师可以在此平台上安全构建。

新的编程设计模型从高层次分为三部分:

图 5:新 Flufillment Platform 的应用架构组件

状态图

我们利用状态图将实体的生命周期表示为一个分层的状态机。我们通过利用与 Protobufs 一致的数据建模方法和建立一个实现状态图的通用 Java 框架,从而正式识别并记录了 Flufillment 实体建模的原则。

什么是状态图?

状态图是一种有限状态机,每一种状态都可以定义它的下级状态机,称为子状态。这些状态可以再次定义子状态。嵌套的状态允许抽象的层次,并提供分层级别,这样就可以放大系统中的特定功能。

一个状态图是由 3 个主要部分组成的。

当触发器发生时,会通知状态图,然后状态图会告知触发器当前的活动状态。当一个相应的转换被注册到该触发器的状态时,转换将执行,从而导致状态图从当前状态转换为目标状态。

怎样把 Flufillment 实体作为状态图构建?

Flufillment 实体是指一个业务对象,它对物理(如送货人或其车辆)或数字抽象(如运输包裹所需的工作)进行建模,以便使用有限状态机模型对消费者(或多个消费者之间的互动)进行建模。

通过状态图配置静态地定义每个 Flufillment 实体,包括状态、状态间的转换以及在每个状态上注册的触发器。通过明确定义的代码组件(Java 类),这些建模组件(状态图、转换、触发器)在应用层中实现。这些代码组件构成了与建模组件相关的功能业务逻辑。除此之外,触发器作为 RPC 暴露出来,允许外部系统(用户应用、周期性事件、事件管道和其他系统)通过 RPC 接口调用触发器。

事务协调器

单个业务流可能涉及到一个或多个 Flufillment 实体触发器,而与消费者应用程序和其他内部系统交互。举例来说,当捕捉到用户要从餐厅送食物的意图时,我们创建了一个订单实体来捕捉用户的意图,一个工作实体来准备食物,另一个工作实体用于将食物从餐厅送到用户的位置。这样就可以协调多个实体在单一事务范围内的转换,从而为消费者提供业务流中每个 Flufillment 实体的一致性视图。成功完成业务流还可能导致副作用(例如,对其他系统的非事务性更新,写入 Apache Kafka,发送通知),在业务流结束后,这些副作用至少需要执行语义一次。

为在一个或多个实体之间实现事务的一致性协调,我们通过网关提供了高级 API。API 可以是触发器或者查询。触发器允许对一个或多个实体进行事务更新,而查询则允许调用者读取一个或多个实体的状态。

这个网关使用两个主要组件来实现触发器和 API:业务交易协调器和查询计划器。业务交易协调器以实体触发器的有向无环图为输入,并通过图中代表单个实体触发器在单个读写事务范围内协调。查询计划器负责提供实体与他们的关系之间的读取访问,具有不同程度的一致性。

ORM 层

ORM 层提供了一个抽象层,用于事务管理、实体访问和实体-实体关系管理的数据库结构。

本系列的第三篇文章将详细介绍这些组件,它们组成了一个应用框架,以及如何让超过 100 名工程师在一个新架构中构建产品流。

挑战和经验

作者介绍:

Ashwin Neerabail,Uber 软件工程师/架构师,在设计和开发大型任务关键性平台和基础设施服务方面具有丰富经验。自从加入 Uber 以来,在过去两年中,领导了下一代 Flufillment Platform 的开发。

Ankit Srivastava,Uber 高级工程师,领导并致力于构建可覆盖全球数百万的 Uber 用户的软件开发项目。领导了 Uber Flufillment Platform 的重构。他的兴趣包括构建分布式系统和可扩展性框架,以及为复杂的业务工作流指定测试策略。

Kamran Massoudi,Uber 工程师,为实现 Flufillment Platform 的技术愿景做出了贡献,并领导了多个项目。他是发起和领导 Fulfillment Platform 重构的技术负责人之一。

Madan Thangavelu,Uber 工程总监。在过去 7 年来,见证并促成了 Uber 令人兴奋的高速增长阶段。花了 4 年时间领导 Uber 的 API 网关和流媒体平台团队。目前是 Uber Flufillment Platform 的工程负责人。

Uday Kiran Medisetty,Uber 首席工程师,领导、引导并推动了 Uber 主要的实时平台项目。

原文链接:

关联阅读:

Uber 如何为近实时特性构建可伸缩流管道?

揭秘 Uber API 网关的架构

声明:本文来自用户分享和网络收集,仅供学习与参考,测试请备份。