设计边缘网关(Edge Gateway),一个高可用和高可扩展的自助服务网关,用于配置、管理和监控 Uber 每个业务领域的 API。

Uber 的 API 网关的演进

2014 年 10 月,优步开始了规模之旅,最终将成为该公司最令人印象深刻的增长阶段之一。随着时间的推移,我们每个月都在以非线性方式扩大我们的工程团队,并在全球范围内获得数百万用户。

在本文中,我们将介绍 Uber 的 API 网关演进的不同阶段,该网关为 Uber 产品提供动力。我们将回顾历史,了解伴随这一飞速发展阶段而发生的建筑模式的演变。我们将讨论这三代网关系统的演变,探讨它们的挑战和责任。

第一代:有机进化

2014 年调查 Uber 的架构,会发现有两个关键服务:调度和 API。调度服务负责连接骑手和司机,API 服务是我们用户和行程的持久化存储。除此以外,还有一个个位数的微服务,支持我们消费者应用上的关键流程。

骑手应用和司机应用都使用一个托管在'/'的端点连接到调度服务。端点的主体有一个名为 "messageType "的特殊字段,它决定了调用特定处理程序的 RPC 命令。处理程序用 JSON 有效载荷来响应。

Uber的API生命周期管理平台边缘网关(Edge Gateway)的设计实践-LMLPHP

图 1:简化的高层说明

在这组 RPC 命令中,有 15 个命令是为关键的实时操作保留的,比如允许司机伙伴开始接受车次、拒绝车次和乘客请求车次。一个特殊的消息类型被命名为 "ApiCommand",它将所有请求代理到 api 服务,并附加一些来自调度服务的上下文。

在 API 网关的上下文中,它看起来像'ApiCommand'是我们进入 Uber 的网关。第一代是一个单一的单体服务有机演化的结果,它开始服务于真正的用户,并找到了用额外的微服务来扩展的方法。调度服务作为一个面向公众的 API 的移动接口--但包括一个具有匹配逻辑的调度系统和一个代理,将所有其他流量路由到 Uber 内部的其他微服务。

这个第一代系统的光辉岁月在那之后并没有持续多久,因为它已经在前几年投入生产。到 2015 年 1 月,一个全新的 API 网关(可以说是第一个真正的网关)的蓝图被引导出来,第一个允许 Uber 骑手应用搜索目的地位置的语义 RESTful API 被部署出来,每秒有几千次查询(QPS)。这是向正确方向迈出的一步。

第二代:全能门户

Uber 在早期采用了微服务架构。这个架构上的决定最终导致了 2200 多个微服务的增长,这些服务在 2019 年为 Ubers 的所有产品提供动力。

Uber的API生命周期管理平台边缘网关(Edge Gateway)的设计实践-LMLPHP

图 2:RTAPIs 服务作为整个公司堆栈的一部分的高级说明

API 网关层被命名为 RTAPI,是实时 API 的缩写。它在 2015 年初从一个单一的 RestfulAPI 开始,后来发展成为一个网关,许多面向公众的 API 为 20 多个不断增长的移动和 Web 客户端组合提供支持。该服务是一个单独的存储库,随着它继续以指数级的速度增长,它被分成多个专门的部署组。

这个 API 网关是 Uber 最大的 NodeJS 应用程序之一,有一些令人印象深刻的数据:

  • 跨 110 个逻辑端点分组的多个端点
  • 40%的工程师已经将代码提交给了这一层
  • 峰值 800000 个请求/秒
  • 有120万个翻译服务用于为客户端本地化数据
  • 在 5 分钟内对每个 diff 执行 50000 个集成测试
  • 在最长的一段时间里,几乎每天都有部署
  • 约 100 万行代码处理最关键的用户流
  • 大约 20%的 mobile build 是根据该层中定义的模式生成的代码
  • 与 Uber 100 多个团队拥有的约 400 多个下游服务进行沟通

第二代的目标

公司内部的每一个基础设施都有一个预定的目标来满足。有些目标是在最初的设计阶段开始的,有些是在设计的过程中实现的。

解耦

100 多个团队在并行构建特性。提供由后端团队开发的基础功能的微服务的数量呈爆炸式增长。前端和移动团队正在以同样快的速度建立产品体验。Gateway 提供了所需的去耦功能,并允许我们的应用程序继续依赖于一个稳定的 API 网关及其提供的契约。

协议转换

所有移动到服务器的通信都主要使用 HTTP/JSON。在内部,Uber 还推出了一种新的内部协议,该协议旨在提供多路复用的双向传输协议。优步的每一项新服务都会采用这一新协议。这使得后端系统在两个协议之间的服务变得支离破碎。这些服务的一些子集也允许我们只通过对等网络来处理它们。当时的网络堆栈还处于非常早期的阶段,网关保护我们的产品团队不受底层网络变化的影响。

横切关注点

公司使用的所有 API 都需要一定的功能集,这些功能应该保持通用性和健壮性。我们专注于身份验证、监控(延迟、错误、有效负载大小)、数据验证、安全审计日志记录、按需调试日志记录、基线警报、SLA 测量、数据中心粘性、CORS 配置、本地化、缓存、速率限制、负载削减和字段混淆。

流式有效载荷

在此期间,许多应用程序功能都采用了将数据从服务器推送到移动应用程序的功能。这些有效负载被建模为 API 和上面讨论的相同的“横切关注点”。应用程序的最终推送是由我们的流媒体基础设施管理的。

减少往返

在过去的十年里,互联网已经在不断发展,以解决 HTTP 协议栈的各种缺点。减少 HTTP 上的往返是前端应用程序使用的一种众所周知的技术(记住图像精灵、用于资产下载的多个域等)。在微服务架构中,减少访问微服务功能的往返次数在网关层结合在一起,网关层“分散收集”来自各种下游服务的数据,以减少应用程序与后端之间的往返。这对于我们在拉坦、印度和其他国家的蜂窝网络上的低带宽网络的用户特别重要。

前端的后端 BFF(Backend for the frontend)

开发速度是任何成功产品的重要特征。在整个 2016 年,我们的新硬件基础设施没有对接,提供新服务很容易,但硬件配置稍显复杂。网关为团队提供了一个很好的地方,可以在一天内启动和完成他们的功能。这是因为这是我们的应用程序调用的一个系统,该服务有一个灵活的开发空间来编写代码,并且可以访问公司内数百个微服务客户端。第一代 Uber Eats 完全是在 Gateway 内部开发的。随着产品的成熟,产品被移出了网关。Uber 有许多功能完全是在网关层使用其他现有微服务的现有功能构建的。

我们的方法面临的挑战

技术挑战

我们对网关的最初目标主要是 io 绑定,并且有一个团队致力于支持节点.js. 经过一轮又一轮的审查,节点.js 成为这个门户的首选语言。随着时间的推移,拥有这样一种动态语言并在 Uber 体系结构的关键层为 1500 名工程师提供自由格式的编码空间,面临着越来越大的挑战。

从某种意义上说,每次新的 API/代码更改都会运行 50000 个测试,因此,使用一些动态加载机制可靠地创建基于依赖关系的增量测试框架非常复杂。随着 Uber 的其他部分转向 Golang 和 Java 作为主要支持的语言,新的后端工程师加入到网关及其异步上节点.js 模式减慢了我们工程师的速度。

大门变得相当大。它采用了 monorepo 的标签(网关被部署为 40 多个独立服务),并将 2500 个 npm 库升级到更新版本的节点.js 继续以指数级的速度增加努力。这意味着我们不能采用许多库的最新版本。此时,Uber 开始采用 gRPC 作为首选协议。我们的版本节点.js 在这方面没有帮助。

在代码检查和影子流量期间,经常会出现无法阻止的空指针异常(NPE)的情况,从而导致关键网关部署暂停几天,直到 NPE 在一些不相关的新的未使用的 api 上得到修复。这进一步降低了我们的工程速度。

网关中代码的复杂性偏离了 IObound。由几个 api 引入的性能回归可能导致网关速度减慢。

非技术性挑战

网关的两个具体目标给这个系统带来了很大的压力。“减少往返”和“前端的后端BFF”是导致大量业务逻辑代码泄漏到网关中的秘诀。有时这种泄漏是出于自愿,有时是毫无理由的。由于有超过一百万行的代码,很难区分“减少往返”和繁重的业务逻辑。

由于网关是保持客户继续移动和进食的关键基础设施,网关团队开始成为优步产品开发的瓶颈。我们通过 API 分片部署和分散审查缓解了这一点,但瓶颈问题并没有得到令人满意的解决。

这使我们不得不重新考虑下一代 API 网关的策略。

第三代:自助式、分散和分层式

到 2018 年初,Uber 已经拥有了全新的业务线,并有了众多新的应用。业务线的数量只会继续增加--货运、ATG、Elevate、杂货等。在每条业务线中,团队管理着他们的后端系统和应用。我们需要系统垂直独立,以便快速开发产品。网关必须提供一套正确的功能,能够真正加速他们的发展,并避免上述的技术和非技术挑战。

我们第三代产品的目标

公司与上次我们设计第二代网关时有很大不同。回顾了所有的技术和非技术挑战,我们带着新的目标开始设计第三代产品。

分离关注点

这种新的架构鼓励公司遵循分层的产品开发方式。

边缘层:真正的网关系统,除了 "为前端提供后台 "和 "减少往返 "之外,提供我们第二代系统网关部分目标中描述的所有功能。"

展示层:专门标记了微服务,为其功能&产品提供前端的后端。该方法的结果是,产品团队管理自己的展现和编排服务,以满足消费应用所需的 API。这些服务中的代码是针对视图的生成和来自许多下游服务的数据的聚合。有单独的 API 来修改迎合特定消费者的响应。例如,与标准的 Uber 骑手应用相比,Uber Lite 应用可能需要更少的与接送地图相关的信息。每一个都可能涉及不同数量的下游调用,以计算所需的响应有效载荷与一些视图逻辑。

产品层:这些微服务被专门标记为提供描述其产品/功能的功能性、可重用的 API。这些可能会被其他团队重用,以组成和构建新的产品体验。

领域层:包含了微服务,这些微服务是为产品团队提供单一精细功能的叶子节点。

Uber的API生命周期管理平台边缘网关(Edge Gateway)的设计实践-LMLPHP

图 3:分层结构

减少我们边缘层的目标

造成复杂度的关键因素之一是第二代中由视图生成和业务逻辑组成的临时代码。在新的架构下,这两个功能已经被移出到其他微服务中,由独立团队在标准的 Uber 库和框架上拥有和运营。边缘层是作为一个纯边缘层运行的,没有定制代码。

需要注意的是,一些起步的团队可以拥有一个服务,满足展示层、产品层和服务层的职责。随着功能的发展,可以将其分解到不同的层中。

这种架构提供了极大的灵活性,可以从小做起,并在我们所有的产品团队中达成一致的北极星架构。

技术构件

在我们向新设想的架构转移的过程中,我们需要关键的技术组件到位。

边缘网关(Edge Gateway)

Uber的API生命周期管理平台边缘网关(Edge Gateway)的设计实践-LMLPHP

端到端用户流

原本由我们第二代网关系统服务的边缘层,被一个独立的 Golang 服务与 UI 搭配取代。"边缘网关 "作为 API 生命周期管理层,由内部开发。现在所有的 Uber 工程师都可以访问 UI 来配置、创建和修改我们面向产品的 API。该 UI 既能进行简单的配置,如身份验证,也能进行高级配置,如请求转换和头传播。

服务框架

鉴于所有的产品团队都要维护和管理一套微服务(可能在这个架构的每一层为他们的功能/产品服务),边缘层团队与语言平台团队合作,商定了一个名为 "Glue "的标准化服务框架,在整个 Uber 中使用。Glue 框架提供了一个建立在 Fx 依赖注入框架之上的 MVCS 框架。

服务库

网关中属于 "减少往返 "和 "前台的后台BFF "这两个属性的代码类别,需要在 Golang 中建立一个轻量级的 DAG 执行系统。我们在 Golang 中构建了一个名为控制流框架(CFF)的内部系统,允许工程师在服务处理程序中开发复杂的无状态工作流来进行业务逻辑协调。

Uber的API生命周期管理平台边缘网关(Edge Gateway)的设计实践-LMLPHP

图4:一个 CFF 任务流

组织一致性

将一家过去几年以特定方式运营的公司转移到一个新的技术系统中,始终是一个挑战。这个挑战特别大,因为它影响了 40%的 Uber 工程的常规运作方式。进行这样的努力,最好的方式是建立共识和对目标的认识。有几个维度需要关注。

建立信任

中心化团队将一些高规模的 API 和关键端点迁移到新的技术栈中,以验证尽可能多的用例,并验证我们可以开始让外部团队迁移他们的节点和逻辑。

确定所有者

由于有许多 API,我们必须明确识别所有权。这并不简单,因为大量的 API 都是跨团队拥有的逻辑。对于明确映射到某一产品/功能的 API,我们自动分配给他们,但对于复杂的 API,我们逐个进行协商,确定所有权。

团队承诺

分解成团队后,我们将终端团队分成了很多组(通常是按照公司较大的组织结构,比如骑手、司机、支付、安全等),并联系了工程部的领导,希望找到一个工程部和项目部的 POC 来带领他们的团队,贯穿整个 2019 年。

人员培训

中间件团队对工程和项目负责人进行了培训,让他们了解 "如何 "迁移,"需要注意什么","何时 "迁移。我们建立了支持渠道,其他团队的开发人员可以在迁移过程中寻求问题和帮助。我们建立了一个自动化的集中跟踪系统,以保证团队的责任,提供进度的可视性,并向领导层汇报。

迭代策略

在迁移过程中,我们遇到了一些边缘情况,对假设提出了质疑。有好几次,我们引入了新的功能,而在其他时候,我们选择不使用与某层无关的功能来污染新的架构。

在迁移过程中,我们的团队继续思考未来和技术组织的发展方向,并确保在这一年中对技术指导进行调整。

最终,我们能够有效地执行我们的承诺,并顺利地朝着自助式 API 网关和分层服务架构的方向发展。

结论

在 Uber 花时间开发和管理了三代网关系统之后,以下是一些关于 API 网关的高层次洞察。

如果有选择的话,请为你的移动应用和内部服务坚持使用单一协议。采用多种协议和序列化格式最终会导致网关系统的巨大开销。拥有一个单一协议为你提供了选择,你的网关层可以有多丰富的功能。它可以是简单的代理层,也可以是极其复杂和功能丰富的网关,可以使用自定义 DSL 实现 graphQL。如果有多个协议需要翻译,那么网关层就不得不变得复杂,以实现将 http 请求路由到另一个协议的服务的最简单过程。

设计你的网关系统以横向扩展是极其关键的。尤其是对于像我们第二代和第三代这样复杂的网关系统来说,更是如此。为 API 组构建独立二进制的能力是使我们的第二代网关能够横向扩展的一个关键功能。单一的二进制将过于庞大,无法运行 1600 个复杂的 API。

基于 UI 的 API 配置对于现有 API 的增量变化是非常好的,但是创建一个新的 API 通常是一个多步骤的过程。作为工程师,有时 UI 可能会觉得比直接在检查出来的代码库上工作更慢。

我们从二代到三代的开发和迁移时间线长达 2 年。当工程师在项目中过渡和退出时,持续的投入是至关重要的。保持可持续发展的势头对这种以北极为目标的长期项目的成功极为关键。

最后,每个新系统不需要支持旧系统的所有技术债务的功能。在放弃支持的问题上做出有意识的选择,对于长期的可持续发展至关重要。

回顾我们网关的发展历程,人们会想,我们是否可以跳过一代人,到达目前的架构。任何一个还没有开始这个历程的公司,可能也会想是否应该从自助式 API 网关开始。这是一个很难做出的决定,因为这些进化不是独立的决定。很多事情都取决于整个公司的支持系统的进化,比如基础设施、语言平台、产品团队、增长、产品规模等等。

在 Uber,我们已经找到了最新架构成功的有力指标。我们在第三代系统中每天的 API 变化已经超过了第二代的数字。这直接关系到更快节奏的产品开发生命周期。转移到基于 golang 的系统后,我们的资源利用率和请求/核心指标得到了显著改善。我们大多数 API 的延迟数据已经显著降低。随着架构的成熟和旧系统在自然重写周期内被重写到新的分层架构中,还有很长的路要走。

作者

Madan Thangavelu

Uday Kiran Medisetty

Pavel Astakhov

原文

https://eng.uber.com/gatewayuberapi/

09-16 22:52