阅读 409

[译] Uber 面向领域的微服务架构

原文:eng.uber.com/microservic…

介绍

最近,围绕面向服务的架构(尤其是微服务架构)的缺点进行了大量讨论。虽然就在几年前,采用微服务架构是比较流行的,因为它们提供了许多好处,例如以独立部署的形式带来的灵活性、职责清晰、系统稳定性和更好的关注点分离,但近年来,人们开始声讨微服务极大地增加了系统复杂性,有时构建微小的特性也会变得困难。

随着 Uber 发展到大约有 2200 多个关键的微服务,期间我们也经历了一些权衡。在过去的两年里,Uber 一直在试图降低微服务的复杂性,同时仍然保持微服务架构带来的好处。通过这篇博客文章,我想介绍我们对微服务架构的一些通用的方法,我们称之为“面向领域的微服务架构”(DOMA)。

由于这些缺点,近年来很流行批评微服务架构,但很少有人主张直接拒绝微服务架构。可维护性太重要了,而且似乎没有其他选择,或者说选择有限。DOMA 的目标是希望在保持微服务架构的灵活性的同时降低系统复杂性。

这篇文章主要解释 DOMA 的概念,以及 Uber 采用此架构的考虑,对平台和产品团队的收益,以及对想要采用此架构的团队的一些建议。

什么是微服务?

微服务是面向服务架构的扩展。与本世纪初的大“服务”不同,微服务是代表一组功能细分的应用程序。这些应用程序暴露定义好的接口。其他应用程序通过创建“RPC”来进行网络调用。

微服务架构的关键特征是托管、调用和部署代码的方式。如果我们考虑大型的单体应用程序,它们通常被拆分成定义好的接口。然后,这些接口直接在进程内调用,而不是通过网络调用。微服务可以视为通过网络来调用函数的一个库,其性能也会受一些影响(网络 I/O 和序列化/反序列化)。

当我们以这种方式思考微服务时,我们可能会问为什么要采用微服务架构。答案通常是独立部署和扩展性。对于大型、一体式的应用程序,通常需要一次部署或发布其所有代码。应用程序的每个版本都可能有很多更改。部署变得既有风险又耗时。很容易就可以破坏整个系统。

换句话说,采用微服务是以牺牲性能为代价来获取更好的可维护性。还须承担维护微服务所需的基础设施的成本。事实证明,在许多情况下,这种权衡是很有意义的,但它也是反对过早采用微服务架构的有力论据。

动机

在 Uber,我们之所以采用微服务架构,是因为我们(大约在 2012-2013 年)主要拥有两个单体服务,并且遇到了许多问题。

  • 可用性的风险。单体代码库内的简单回退可能会导致整个系统(在本例中为整个 Uber)宕机。
  • 高风险、高代价的部署。执行这些部署既痛苦又耗时,而且经常需要回滚。
  • 缺乏关注点分离。由于代码库庞大,很难保持好的关注点分离。在指数级增长的环境中,考虑眼前的利益有时会导致逻辑和组件之间的边界较差。
  • 执行效率低下。这些问题加在一起,使得团队很难自主或独立执行。

换句话说,随着 Uber 从 10 多名工程师发展到 100 多名工程师,多个团队负责技术栈的一部分,单体架构将团队的命运捆绑在一起,使其难以独立运行。

因此,我们采用了微服务架构。最终,我们的系统变得更加灵活,这使得团队更加独立。

  • 系统可靠性。在微服务架构中,总体系统可靠性提高。单个服务可以在不影响整个系统的情况下关机(并回滚)。
  • 关注点分离。在架构上,微服务架构迫使你想一个问题:“为什么存在这个服务?”我们能更清楚地定义不同组件的角色。
  • 职责清晰。代码的归属权变得清晰多了。服务通常被个人、团队或组织维护,从而实现更快的增长。
  • 独立执行。独立部署 + 职责清晰的界限释放了平台团队的自主执行能力。
  • 开发人员速度。团队可以独立部署代码,这使他们能够按照自己的节奏执行开发。

毫不夸张地说,如果没有微服务架构,Uber 就不可能实现我们今天的规模和质量。

然而,随着公司规模的扩大,从 100 名工程师发展到 1000 名工程师,我们开始注意到系统复杂性在不断升高。使用微服务架构,用单体代码库换取黑盒功能,这些黑盒功能随时可能改变,并且很容易导致意外的行为。

例如,工程师必须对 12 个不同团队的大约 50 个服务进行检查,以调查问题的根本原因。

理解服务之间的依赖关系可能会变得相当困难,因为服务之间的调用可能会深入很多层。依赖的第 n 个服务的延迟峰值可能会导致上游问题的级联。如果没有正确的工具,就不能看到实际发生的事情,这使得调试变得困难。

上图是 2018 年中旬 Uber 的微服务架构拓扑图

为了构建一个简单的特性,工程师通常需要跨多个服务,而这些服务可能归不同的团队维护。这使花费在会议、设计和代码审查上的时间变得更多。随着团队在彼此的服务中构建代码、修改彼此的数据模型,甚至代替服务所有者进行部署,这又使之前清晰的服务界限遭到破坏。这形成了网络化的整体,让看似独立的服务都必须部署在一起才能安全地执行更改。

上图是 Uber 大约在 2018 年的一个复杂的流程示例,它需要依赖 10 个服务进行简单的集成。

结果是开发人员体验变慢、服务所有者职责不清晰、迁移变得更加痛苦等等。对于已经采用微服务架构的组织来说,没有回头路可走。

面向领域的微服务架构

如果我们可以将微服务看作受 I/O 约束的库,而将“微服务架构”看作一个大型的分布式应用程序,那么我们就可以使用易于理解的架构来考虑如何组织代码。

因此,“面向领域的微服务架构”在很大程度上借鉴了已有的方法,如领域驱动设计清晰的架构面向服务的架构,以及面向对象和面向接口的设计模式。我们认为 DOMA 是创新的,因为它是在大型公司的大型分布式系统中利用既定设计原则的一种相对新颖的方式。

DOMA 相关的核心原则和术语如下:

  1. 我们不是面向单个微服务,而是面向相关微服务的集合。我们称这些为
  2. 我们进一步创建称为层的域集合。域所属的层确定该域中的微服务可以承担哪些依赖关系。我们称之为分层设计。
  3. 我们为域集合的入口点提供清晰的接口。我们称之为网关。
  4. 最后,每个域应该与其他域无关,也就是说,一个域不应该在其代码库或数据模型中硬编码与另一个域相关的逻辑。由于团队经常需要在另一个团队的域中共享逻辑(例如,自定义验证逻辑或数据模型上的一些元上下文),因此我们提供了一个扩展架构使域具有良好的扩展性。

换句话说,通过提供系统化的架构、域网关和预定义的扩展点,DOMA 想让微服务架构变得更容易理解:一组灵活的、可重用的和分层的组件的结构化集合。

这篇文章的其余部分将继续探讨 Uber 实践 DOMA 的情况,我们已经看到的好处,我们想给要采用这种方法的公司提供一些实用建议。

Uber 的实践

Uber 域代表绑定到逻辑功能分组的一个或多个微服务的集合。设计域的一个常见问题是“域应该有多大?”我们在这里不给任何指导。有些域可以包含数十个服务,有些域可能只包含一个服务。重要的是仔细考虑每个域集合的逻辑角色。例如,我们的地图搜索服务是一个域,票价服务是一个域,匹配平台(匹配乘客和司机)是一个域。这些也并不总是遵循公司的组织结构。Uber 地图被分成三个域,三个不同的网关下有 80 个微服务。

分层设计

分层设计回答了“哪些服务可以调用其他哪些服务?”。因此,我们可以将分层设计视为“规模上的关注点分离(separation of concerns at scale)”。或者,我们可以将层设计视为“规模依赖管理(dependency management at scale)”。

分层设计描述了一种机制,用于考虑 Uber 跨服务依赖的故障影响范围和产品特异性。域从底层到顶层,宕机情况下它们对服务的影响逐渐变小,产品的也从通用变为具体。相反,底层的功能具有更多依赖项,因此往往具有更大的影响范围,并代表更一般的业务功能集。下图说明了这一概念。

你可以将顶层视为特定的用户体验(例如移动功能),将底层视为通用的业务功能(例如帐户管理或旅行业务)。上层只依赖于下层,这给了我们一些启示来考虑影响范围和域继承等问题。

值得注意的是,功能通常会从具体“下移”到通用。可以想象,随着需求的发展,一个简单的功能最终会演化为平台。事实上,这种向下迁移是意料之中的,Uber 的许多核心业务平台一开始都是特定于乘客或司机的功能,随着我们开发更多的业务线,这些功能变得更加通用,它们承担了更多的依赖(如 Uber Eats 或 Uber Freight)。

在 Uber 内部,我们建立了以下五层。

  1. 基础设施层。提供任何工程组织都可以使用的功能。这是 Uber 大的工程问题的解决手段,如存储或网络等。
  2. 业务层。提供可以使用的功能,但不特定于特定的产品类别或业务线,如顺风车、餐饮或运费。
  3. 产品层。提供与特定产品类别或业务线相关但与移动应用无关的功能,例如面向多个骑行的应用(Rider、Rider-Lite、m.uber.com 等)使用的“request a ride”逻辑。
  4. 展示。提供面向消费者的应用(移动 app/web)。
  5. 边缘层。安全对外公开 Uber 服务。这一层也是移动应用感知的。

如你所见,后续的每个层代表越来越具体的功能分组,并且具有越来越小的影响范围(换句话说,更少的依赖于上层的功能)。

网关

术语 “Gateway API” 是微服务架构中广泛认知的概念。我们的定义与已有的定义没有太大差别,只是我们倾向于将网关排他性地认为是进入一组底层服务的单一入口点,我们称之为域。网关的成功取决于 API 设计的成功。

上图展示了一个网关。它抽象了域的内部细节-多个服务、数据表、ETL 管道等。只有接口 - RPC 、消息传递事件和查询等接口向其他域公开。

由于上游消费者只对单个服务进行操作,因此网关可以为可迁移性、可发现性系统复杂度带来很多好处,上游服务只有一个依赖关系,而不是可能存在的对多个下游服务的依赖关系。如果我们从面向对象设计的意义上考虑网关,它们是接口定义,使我们能够在底层“实现”(在本例中是底层微服务的集合)方面做任何我们想做的事情。

拓展

扩展代表了一种扩展域的机制。扩展的基本定义是,它提供了一种机制,用于扩展底层服务的功能,而不会修改服务的核心实现,也不会影响其可靠性。在 Uber,我们提供两种不同的扩展模式:逻辑扩展数据扩展。扩展的概念使我们能够将我们的架构扩展到多个团队,从而能够彼此独立地工作。

逻辑拓展

逻辑扩展提供了一种扩展服务底层逻辑的机制。对于逻辑扩展,我们使用提供者模式或插件模式的变种,并逐个在服务的基础上定义接口。这可以以接口驱动的方式实现扩展逻辑,而无需修改底层平台的核心代码。

例如,一个司机上线。通常,我们会进行各种检查,以确保司机可以上线(安全检查、合规性等)。这些项目中的每一个子项都属于一个单独的团队。实现这一点的一种方法是让每个团队在同一端点中编写逻辑,但这会带来复杂性。因为每个检查都是自定义的、完全无关的逻辑。

在逻辑扩展的情况下,“去上线” 端点将定义一个接口,约束每个扩展都符合预定义的请求类型和响应。每个团队将注册一个负责执行此逻辑的扩展。在这种情况下,它们可能会简单地获取有关驱动程序的一些上下文,并返回一个布尔值,说明驱动程序是否可以上线。“去上线” 端点将简单地迭代这些响应,并确定其中是否有任何响应是不符合要求的。

这将核心代码与每个扩展解耦,并将逻辑与扩展之间提供隔离。围绕这一点很容易构建更多功能,例如可观测性或特性标记。

数据拓展

数据扩展提供了一种将数据插拔到接口的扩展方式来避免核心平台数据模型膨胀。对于数据扩展,我们利用 Protobuf 的 Any 功能,以便可以任意添加数据。服务通常会存储此数据或将其传递给逻辑扩展,因此核心平台不负责反序列化上下文。Protobuf Any 的实现通过一些基础设施的开销来换取更强的类型。如果要更简单的话,也可以使用 JSON 字符串来表示任意数据。

自定义扩展

除了逻辑和数据扩展之外,Uber 的许多团队都引入了适合其领域的自定义扩展模式。例如,表示架构相关的许多集成都使用基于 DAG 的任务执行逻辑。

收益

几乎 Uber 的每个主域都在一定程度上受到了 DOMA 的影响。在过去的一年里,我们主要关注 Uber 的业务层,它为我们的不同业务线提供了通用的逻辑。

DOMA 在 Uber 还很年轻,我们很高兴能在未来分享更多的数据和架构示例。然而,在简化开发人员体验和降低总体系统复杂性方面目前是非常积极的。

产品与平台

DOMA 是 Uber 产品和平台团队共识努力的结果。平台支持成本通常会下降一个数量级。使得产品团队开发更为迅速。

例如,采用扩展架构,能减少代码审查、设计和学习的时间,能够使集成新功能的时间从三天减少到三个小时。

降低复杂性

以前,产品团队必须调用多个下游服务才能利用一个域;现在他们只需调用一个服务。通过降低接触点的数量,平台能够减少 25-50% 的时间消耗。此外,我们能够将 2200 个微服务分类为 70 个域。其中大约 50% 已经实施,剩下的大多数都有未来的计划。

方便的迁移

在 Uber,我们计算出微服务的半衰期是 1.5 年,这意味着我们每 1.5 年 就有 50% 的微服务流失。没有网关,微服务架构很容易因此而陷入“迁移地狱”。不断变化的微服务需要不断向上游迁移。网关使团队能够避免对底层域服务的依赖,这意味着这些服务可以在不要求上游迁移的情况下进行更改。

Uber 去年两次最大的平台重写都发生在网关后面。这些平台被数百项服务依赖,这些服务将不得不迁移现有的消费者。在这种场景下,迁移成本将非常高,使得完全重写平台变得不可行。

新的业务线和产品线

事实证明,使用 DOMA 设计的平台更具扩展性,也更易于维护。Uber 的大多数团队之所以采用 DOMA,是因为支持新的业务线已经变得过于昂贵。

实用的建议

本节为可能想要采用 DOMA 的公司提供一些实用的建议。这里的指导原则是,根据我们的经验,成熟的微服务架构源于在正确的时间朝着正确的方向进行推进。现实情况是,对于一个整体的微服架构,真正的“重写”是永远不可能的。

因此,我们认为发展微服务架构更像是“修剪树枝”,以使其正确增长,而不是自上而下或一次性架构(或重构)。这是一个动态进步的过程。

初创型公司

主要问题应该是“我们应该在什么时候采用微服务架构?”以及“这对我们有意义吗?”正如我们在上面看到的,虽然微服务为拥有大量工程师的组织提供了维护性优势,但这也增加了复杂性,可能会使功能更难构建。

在小型组织中,可维护性可能无法抵消架构带来的复杂性。此外,微服务架构通常需要专门的工程资源来支持,这可能超出创业公司的预算,或者从优先级的角度来看不是最优的。

考虑到这一点,完全搁置微服务一段时间也是合理的。如果组织选择采用微服务,应该需要考虑到“微服务是大型分布式应用程序”,以及需要考虑微服务之间的关注点分离。此外,要认识到第一批微服务可能是最重要、持续时间最长的服务,因为它们描述了业务的核心。

中型公司

一旦公司变成拥有多个团队的中型公司,不同功能和平台之间的职责边界变得模糊不清,微服务体系结构就会变得更加有用。

正是在这个阶段,可以开始考虑微服务之间的分层结构。依赖管理可能会变得更加重要,因为一些服务随着越来越多的团队依赖它们而变得越来越重要。

对平台化的早期投资可能会为未来带来红利。如果能够创建完全与产品无关的业务平台,并避免核心平台服务中耦合产品逻辑,这里就有可能避免技术债。在这一点上采用扩展来实现该目标可能是有意义的。

鉴于微服务的数量可能仍然比较低,将它们集中在一起可能没有意义。然而,Uber DOMA 的域也可以包含单个服务,因此以“面向域”的方式思考可能仍然有用。

大型公司

较大的工程组织可能有数百名工程师和微服务以及一些依赖项。在这一点上,DOMA 会发挥比较大的作用。这些公司可能会有微服务集群,这些微服务可以很容易地分组到有网关的域中。遗留服务通常需要重构或重写,然后再进行迁移,这意味着如果网关已经就位,那么迁移会变得更加便利。

清晰的层次结构也将变得越来越重要,一些服务将作为特定功能的“产品”进行服务,而其他服务将越来越多地支持多个产品线,并被视为“平台”。在这个阶段,保持任意的产品逻辑与平台解耦是至关重要的,以避免平台团队繁重的操作负担以及系统边界的不稳定性。

最后的想法

随着 Uber 越来越多的团队开始采用 DOMA,我们仍在积极发展 DOMA。DOMA 的重要见解是,微服务架构实际上只是一个大型的分布式程序,你可以将适用于任何软件的相同原则应用于其发展。DOMA 只是在实践中思考这些原则的一种方法。我们希望其他人认为它有用,我们也期待反馈!

DOMA 本身就是跨职能部门努力的结果,Uber 每个组织都有近 60 名工程师参与其中。对于在过去两年中在这方面投入巨资的人,致以感谢,…