由 Uber 开发的边缘网关是一个高可用、可扩展的自助式网关,用于配置、管理和监视 Uber 的每个业务域 API。
本文最初发表于 Uber 官方博客网站,经授权,由 InfoQ 中文站翻译并发布。
Uber API 网关演变史
自 2014 年 10 月起,Uber 走上了规模化扩张之旅,这段旅程最终成为公司最令人印象深刻的增长阶段之一。随着时间的推移,我们每个月都在非线性扩大工程团队的规模,并在全球获得了数以百万计的用户。
在本文中,我们将为读者介绍 Uber API 网关演变的不同阶段,这个网关为 Uber 产品提供了支持。我们将通过回顾历史来了解架构的演变史,这些演变是伴随着高速增长阶段而发生的。我们将阐述这三代网关系统的演变史,探讨它们的挑战和责任。
第一代 API 网关:有机演变
如果你在 2014 年调查 Uber 架构的话,就会发现有两个关键服务:调度和 API。调度服务负责连接乘客(Rider)和司机(Driver),API 服务是我们用户和行程的长期存储库。除此之外,还有不到 10 个微服务,用来支持我们客户应用程序上的关键流程。
乘客应用程序和司机应用程序都使用位于“/”的单一终结点连接调度服务。端点的主体有一个名为“messageType
”的特殊字段,该字段决定了调用特定处理程序的 RPC 命令。该处理程序以 JSON 有效负荷进行响应。
图 1:简化的高级图示
在这组 RPC 命令中,有 15 个命令是为关键的实时操作保留的,比如,允许司机伙伴开始接受行程、拒绝行程和乘客请求行程。一个特殊的messageType
被命名为 “ApiCommand
”,它将所有请求代理到 API 服务,并提供一些来自调度服务的附加上下文。
在 API 网关的上下文中,看起来ApiCommand
是我们进入 Uber 的门户。第一代网关是单一的单体服务有机演变的结果,它开始服务于真实的用户,并找到了利用附加的微服务进行扩展的方法。调度服务作为面向公众的 API 移动接口,包括一个具有匹配逻辑的调度系统和代理,用于将所有其他流量路由到 Uber 内的其他微服务。
但是,第一代系统的辉煌日子并没有持续很长时间,因为它在前几年就已经投产了。到 2015 年 1 月,一个全新的 API 网关(可以说是第一个真正的网关)蓝图已经启动,第一个语义 RESTful API 被部署,允许 Uber 乘客应用程序搜索目的地位置,每秒查询数(Queries per second,QPS)可达几千次,这是朝着正确方向迈出的第一步。
第二代 API 网关:无所不包的网关
Uber 在早期就采用了微服务架构。这一架构决策最终导致了 2200 多个微服务的增长,到 2019 年,这些微服务为 Uber 所有产品提供了动力。
图 2:RTAPI 服务作为整个公司技术栈的一部分的高级图示
API 网关曾被命名为 RTAPI,是 Real-Time-API 的缩写。它在 2015 年初以一个单一的 RESTful API 开始,并发展成为拥有许多面向公共的 API 网关,并为 20 多个不断增长的移动和 Web 客户端组合提供支持。该服务是单一的存储库,随着它继续以指数级的速度增长,被分解为多个专门的部署组。
这个 API 网关是 Uber 最大的 NodeJS 应用程序之一,有一些令人印象深刻的统计数据:
跨 110 个逻辑端点分组的多个端点;
40% 的工程师已将代码提交到这一层;
峰值时高达 800000 个请求/每秒;
为客户提供 120 万次翻译,以实现数据本地化;
在 5 分钟内对每个差异执行 50000 次集成测试;
有很长一段时间,几乎每天都会进行一次部署;
大约有 100 万行代码处理最关键的用户流;
大约 20% 的移动构建是由这一层定义的模式生成的代码;
与 Uber 100 多个团队拥有的约 400 多个下游服务进行通信;
第二代网关的目标
公司内部的每一个基础设施都有一组预定的目标要满足。有的目标是最初设计时就开始的,有些则是在设计过程中逐步实现的。
解耦
100 多个团队并行构建功能。由后端团队开发的提供基础功能的微服务数量呈爆炸式增长。前端团队和移动团队正在以同样快的速度构建产品体验。网关提供了所需的解耦,并允许我们的应用程序继续依赖稳定的 API 网关和它提供的合约。
协议转换
所有移动客户端到服务器的通信主要使用 HTTP/JSON。在内部,Uber 还推出了一种新的内部协议,旨在提供多路复用双向传输协议。曾经有一段时间,Uber 的每个新服务都采用了这个新协议。这就使得后台系统的服务在这两种协议之间变得支离破碎。这些服务的某些子集也允许我们只能通过对等网络来解决这些问题。当时的网络协议栈也处于非常早期的阶段,网关保护我们的产品团队不受底层网络变化的影响。
横切关注点
公司使用的所有 API 都需要一定的功能集,这些功能应该保持通用且稳定。我们关注的重点是身份验证、监控(延迟、错误、有效负荷大小)、数据验证、安全审核日志、按需调试日志、基线警报、SLA 测量、数据中心粘性、CORS 配置、本地化、缓存、速率限制、负荷消减和字段模糊处理。
流式有效负荷
在此期间,许多应用功能采用了从服务器向移动应用推送数据的功能。这些有效负荷被建模为 API 和上面讨论的相同的“横向关注点”。最后推送到应用程序的过程是由我们的流媒体基础设施管理的。
减少往返
过去十年,互联网在解决 HTTP 协议栈的各种缺点方面取得了进展。减少 HTTP 上的往返是前端应用程序使用的一项众所周知的技术(请记住图像精灵、用于资产下载的多个域等)在微服务架构中,减少访问微服务功能的往返次数在网关层结合在一起,网关层从各种下游服务中“分散收集”数据,以减少我们的应用和后端之间的往返。这对于我们在拉美、印度等国家的低带宽蜂窝网络上的用户来说尤为重要。
开发速度
对任何成功的产品而言,开发速度都是非常关键的特征。在整个 2016 年,我们的新硬件基础设施并未实现 Docker 化,虽然提供新服务很容易,但硬件配置稍显复杂。网关为团队提供了一个这样奇妙的地方,让团队可以在一天之内开始并完成他们的功能。这是因为它是我们的应用程序调用的系统,服务有一个灵活的开发空间来编写代码,并且可以访问公司内部的数百个微服务客户端。Uber Eats 的第一代产品完全是在网关内开发的。随着产品的成熟,部分产品被移出了网关。在 Uber,有很多功能完全是利用其它现有微服务的现有功能在网关层构建出来的。
面临的挑战
技术挑战
我们最初的网关目标主要是受 I/O 限制的,并且有一个团队致力于支持 Node.js。经过一轮又一轮的评审,Node.js 成了这个网关的首选语言。随着时间的推移,拥有这样一种动态的语言,以及为 Uber 架构中如此关键的层中 1500 名工程师提供自由编码空间,都面临着越来越大的挑战。
在某些时候,每一个新的 API/代码变更都要运行 5 万个测试,因此,要可靠地创建一个增量测试框架是很复杂的,因为这个框架基于依赖项,且具有一些动态加载机制。当 Uber 的其他部分转向 Golang 和 Java 作为主要支持的语言时,随着新的后端工程师加入网关及其异步 Node.js 模式,工程师的速度慢了下来。
网关变得相当大了。它被贴上了 monorepo(单体式代码仓库)的标签(该网关被部署为 40 多个独立服务),并将 2500 个 npm 库升级到较新版本的 Node.js,继续以指数级方式增加工作量。这就意味着我们无法采用众多库的最新版本。这时,Uber 开始采用gRPC作为首选协议,而我们的 Node.js 版本在这方面却毫无建树。
在代码审查和影子流量期间,指针异常(Null Point exception,NPE)是无法避免的,这会导致关键网关部署停滞几天,直到 NPE 在一些不相关的、新的、未使用的 API 上得到修复。这样一来,就进一步拖慢了我们的工程速度。
网关中代码的复杂性与受 I/O 限制背道而驰。由一些 API 引入的性能回归可能会导致网关变慢。
非技术性挑战
网关的两个特定目标。“减少往返”和“开发速度”,给这个系统带来了很大的压力。这两个目标是导致大量业务逻辑代码泄漏到网关的原因。有时,这种泄露是故意的,有时是无意的。由于代码超过一百万行,因此“减少往返”和大量的业务逻辑之间是相当难以辨别的。
随着网关成为保证客户持续移动的关键基础设施,网关团队开始成为 Uber 产品开发的瓶颈。我们通过 API 分片部署和分散审查缓解了这一问题,但成为瓶颈的问题没有解决到令人满意的程度。
这时,我们不得不重新考虑下一代 API 网关的策略。
第三代网关:自助式、分散式和分层式
到 2018 年初,Uber 已经拥有全新的业务线,并有了众多新的应用。业务线的数量再次增加:Freight(优步货运)、ATG(先进技术团队)、Elevate(优步航空)、Groceries(杂货配送)等。在每条业务线中,团队管理者的后端系统和应用需要互相独立,以便快速开发产品。网关必须能够提供正确的功能集,这些功能实际上可以对它们进行加速,并避免上面提到的技术性和非技术性挑战。
第三代网关的目标
第三代网关与第二代网关的设计有很大不同。在回顾所有技术性和非技术性的挑战后,我们开始设计第三代网关,并制定了一套新目标。
关注点分离
这种新的架构鼓励公司遵循分层的方法进行产品开发。
边缘层:真正的网关系统,它提供了第二代系统的网关部分目标所描述的所有功能,除了“开发速度”和“减少往返”。
表示层:专门为前端的功能和产品提供后端标记的微服务。这种方法导致产品团队管理自己的表示和编排服务,这些服务可以满足消费应用程序所需的 API。这些服务中的代码是针对视图生成和来自许多下游服务数据的聚合。有单独的 API 来修改以适应特定消费者的响应。例如,与标准的 Uber 乘客应用程序相比,Uber Lite 应用程序可能需要更少的与接送地图相关的信息。其中每一个都可能涉及不同数量的下游调用,以使用某些视图逻辑计算所需的响应有效负荷。
产品层:这些微服务被专门标记,以提供功能性的、可重用的 API 来描述他们的产品/功能,这些可能会被其他团队重用来组合和构建新的产品体验。
域层:包含作为叶节点的微服务,为产品团队提供单一的优化功能。
图 3:分层架构
减少边缘层的目标
造成复杂性的关键因素之一是第二代网关中的特殊代码,它由视图生成和业务逻辑组成。在新的架构下,这两个功能已被移出到其他微服务,由独立团队在标准 Uber 库和框架上拥有和运营。边缘层是作为纯边缘层运行的,没有定制代码。
关键是要注意,一些刚起步的团队可以拥有一个单一服务来满足表示层、产品层和服务层的职责。随着功能的发展,它可以被解构到不同的层。
这种架构提供了极大的灵活性,可以从小规模开始,最终形成一个贯穿我们所有产品团队的北极星架构。
技术构建块
在我们努力转移到新设想的架构时,我们需要关键的技术组件就位。
边缘网关
端到端用户流
最初由我们的第二代网关系统服务的边缘层被一个单独的 Golang 服务和一个用户界面所取代。“边缘网关”是内部开发的 API 生命周期管理层。所有 Uber 工程师现在都可以访问其用户界面来配置、创建和修改面向产品的 API。用户界面既可以进行简单的配置(如身份验证),也可以进行高级配置(如请求转换和标头传播)。
服务框架
鉴于所有的产品团队都要维护和管理一组微服务(可能是在这个架构的每一层,用于他们的功能/产品),边缘层团队与语言平台团队合作,商定了一个名为“Glue”的标准化服务框架,将在整个 Uber 中使用。Glue 框架提供了一个建立在Fx依赖注入框架之上的 MVCS 框架。
服务库
网关中的代码类别属于“减少往返”和“用于前端的后端”这两个范畴,需要在 Golang 建立一个轻量级 DAG 执行系统。我们在 Golang 建立了一个内部系统,称为控制流框架(Control Flow Framework,CFF),它允许工程师在服务处理程序中为业务逻辑编排开发复杂的无状态工作流。
图 4:CCF 任务流
组织一致性
将一家在过去几年里以特定方式运作的公司转移到一个新的技术系统中,始终是一个挑战。这个挑战尤其巨大,因为它影响了 40% 的 Uber 工程部门的常规运作方式。进行这样的努力,最好的方式是建立共识和对目标的认识。有几个方面需要重点关注。
建立信任
集中式团队将一些高规模的 API 和关键端点迁移到新的栈中,以验证尽可能多的用例,并验证我们可以开始让外部团队迁移他们的端点和逻辑。
查找所有者
由于有许多 API,我们必须清楚地确定所有权。要做成这件事并不简单,因为大量 API 具有跨团队拥有的逻辑。对于清晰映射到某个产品/特性的 API,我们会自动分配它们,但对于复杂的 API,我们逐个处理并协商所有权。
承诺
在将其分成团队之后,我们将端点团队分成许多组(通常按较大的公司组织结构,例如乘客、司机、支付、安全等),并联系工程领导,以期在 2019 年全年找到一个工程部和项目部的 POC 来带领他们的团队。
培训
集中式团队对工程和项目负责人进行了培训,让他们了解“如何”迁移、“需要注意什么”、“何时”迁移等。我们建立了支持渠道,来自其他团队的开发人员可以在迁移过程中寻求问题和帮助。为了保证团队承担责任,提供进度的可视性,并更新领导能力,我们还部署了一个自动化的集中跟踪系统。
迭代策略
在迁移过程中,我们遇到了一些边缘情况,并对假设提出了质疑。有好几次,我们引入了新的功能,而在其他时候,我们选择不使用与某层无关的功能来污染新的架构。
在迁移过程中,我们的团队不断思考技术组织的未来和发展方向,并确保在这一年中对技术指导进行调整。
最终,我们能够有效地执行我们的承诺,并朝着自助式 API 网关和分层服务架构的方向迈进。
结论
在 Uber 耗费时间开发和管理了三代网关系统之后,下面是一些关于 API 网关的高层次观察。
如果有选择的话,请坚持为你的移动应用和内部服务采用单一协议。因为采用多种协议和序列化格式最终会导致网关系统的巨大开销。拥有一个单一协议为你提供了选择,你的网关层可以有多丰富的功能。它可以是简单的代理层,也可以是极其复杂和功能丰富的网关,可以使用自定义 DSL 实现 graphQL。如果有多个协议要迁移,网关层就会不得不变得复杂,以实现将 http 请求路由到另一个协议的服务的最简单过程。
设计你的网关系统,使其能够横向扩展是非常关键的。对于像我们第二代和第三代这样复杂的网关系统来说,更是如此。为 API 组构建独立二进制的能力是我们第二代网关能够横向扩展的一个关键特性。单一的二进制文件过于庞大,无法运行 1600 个复杂的 API。
基于用户界面的 API 配置对于现有 API 中的增量更改非常有用,但是创建新的 API 通常是一个多步骤的过程。作为工程师,有时用户界面可能会感觉比直接在检查过的的代码库上工作更慢。
我们从第二代到第三代的开发和迁移时间长达 2 年。随着工程师在项目内外的过渡,持续投资至关重要。最后,每一个新系统不需要支持来自旧系统的所有技术债务功能。有意识地选择放弃支持对长期可持续性至关重要。
回顾我们网关的演变史,人们会想,我们是否可以跳过一代,到达目前的架构?任何一家还没有开始该历程的公司可能也会想,是否应该从自助式 API 网关开始?这是一个很难做出的决定,因为这些演变并不是独立的决定。很多事情取决于整个公司的支持系统的演变情况,比如基础设施、语言平台、产品团队、增长、产品规模等等。
在 Uber,我们发现了这一最新架构成功的有力指标。我们在第三代系统中每天的 API 变化已经超过了第二代的数字,这直接关系到节奏更快的产品开发生命周期。转移到基于 Golang 的系统后,我们的资源利用率和请求/核心度量已经显著地提高了。我们大多数 API 上的延迟数都已经显著地减少了。随着新架构的成熟和旧系统在其自然重写周期中被重写到较新的分层架构中,还有很长的一段路要走。
作者介绍:
Madan Thangavelu 是 Uber 高级工程经理。在过去的 6 年里,他见证了 Uber 令人兴奋的高速增长阶段,并为此做出了贡献。他花了 4 年的时间领导 Uber 的网关平台团队。目前,他担任 Uber 实现平台的工程主管,该平台为全球实时购物和物流系统提供支持。
Uday Kiran Medisetty 是 Uber 的高级工程师。。他在第二代 API Gateway 发布不到 10 个 API 时加入了团队,帮助扩展到 1600 个 API,并协助制定了第三代网关的策略,还领导了基于服务器到客户端的实时移动体验推送消息的开发。在过去的几年里,他正在领导 Uber 核心实现平台的重新架构。
Pavel Astakhov 是 Uber 的高级技术项目经理。在过去的两年里,他领导了整个公司的多个大型跨功能基础设施项目,并与 Madan 和 Uday 一起开发和领导了从边缘层的第二代到第三代过渡的执行策略/迁移。
原文链接:
https://eng.uber.com/gatewayuberapi/
评论