vlambda博客
学习文章列表

面向领域的微服务架构设计

最近围绕面向服务的架构,特别是微服务架构的缺点进行了大量的讨论。在几年前由于微服务架构带来的众多好处,如独立部署的灵活性、明确的所有权、系统稳定性的改善和更好的关注点分离等,很多人都欣然采用了微服务架构,但近年来人们开始抨击微服务大大增加了复杂性,有时甚至使微服务变得更加复杂了。

随着 Uber 发展到大约 2200 项关键的微服务,我们都亲身经历了这些事情。在过去两年里,Uber 试图在降低微服务复杂性的同时,仍然保持着微服务架构的一些优势。我们希望通过这篇文章介绍我们对微服务架构的通用方法,我们称之为 "面向领域的微服务架构"(DOMA)。

虽然近年来流行批评微服务架构,但很少有人主张彻底拒绝微服务架构。因为目前似乎没有或有限的替代方案。我们使用 DOMA 的目标是为那些希望降低总体系统复杂性,同时保持微服务架构灵活性的组织提供一条前进的道路。

本文解释了 DOMA,导致 Uber 采用这种架构的原因,以及它对平台和产品团队的好处,最后,给想采用这种架构的团队提供一些建议。

什么是微服务?

微服务是面向服务架构的扩展。与2000年代较大的“服务”相比,微服务表示一组范围更小的功能的应用程序。这些应用程序通过网络托管提供,并暴露一个良好定义的接口,其他应用程序通过创建一个 RPC 来调用这个接口。

微服务架构的关键特点是代码被托管、调用和部署的方式。如果我们考虑大型的单体应用,它们一般会被分割成具有明确定义接口的封装组件,然后,这些接口将直接在进程中调用,而不是通过网络调用。通过这种方式,我们可以开始将微服务看成一个库,但是为了调用它的功能函数,它的性能会受到一定影响(网络 I/O 和序列化/反序列化)。

当我们以这种方式思考微服务时,我们可能会质疑为什么我们还要采用微服务架构。答案往往也很明确,那就是独立部署和扩展。对于一个大型的单体应用,组织会被迫同时部署或发布所有代码。但是我们知道应用程序的每个新版本都可能涉及许多变更,这样部署就会变得风险大、耗时长,任何人都可能使整个系统瘫痪。

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

动机

在 Uber,我们采用了微服务架构,因为我们大约在2012-2013年主要有两个单体服务,然后我们也遇到了微服务解决的大部分问题。

  1. 可用性风险:单体代码库内的一次回归就会使整个系统瘫痪。

  1. 风险大、成本高的部署:在频繁需要回滚的情况下,执行这些操作既痛苦又费时。

  1. 糟糕的关注点分离:在庞大的代码库中,很难保持良好的关注点分离。在一个指数级增长的环境中,一些临时的解决方法有时会导致逻辑和组件之间的界限不清。

  1. 执行效率低下:这些问题结合在一起,使得团队难以自主或独立地执行。

当 Uber 从10多个工程师发展到100多个工程师的时候,多个团队还拥有不同的技术栈时,单体架构将整个团队都捆绑在一起了,很难独立运作了。所以,我们采用了微服务架构,最终,我们的系统变得更加灵活,使得团队也更加自主了。

  1. 系统可靠性:在微服务架构中,整体系统的可靠性会上升,单个服务可以在不影响整个系统的情况下宕机或回滚。

  1. 关注点分离:从架构上看,微服务架构迫使你去思考 "这个服务为什么存在?",这就可以更清晰地定义不同组件的角色。

  1. 明确所有权:谁拥有什么代码变得更加清晰,一个服务通常由个人、团队或组织级别拥有,从而实现更快的增长。

  1. 自主执行:独立部署+更清晰的所有权线,解除不同产品和平台团队的自主执行。

  1. 开发速度:团队可以独立部署他们的代码,这使他们能够以自己的速度执行。

可以毫不夸张地说,如果没有微服务架构,Uber 不可能达到今天的规模和执行质量。

然而,随着公司规模的进一步扩大,工程师人数增加到1000人,我们开始注意到一系列与系统复杂性增加相关的问题。在微服架构下,我们可以将单个功能的代码库换成一个黑盒(独立的微服务),黑盒的功能随时可能发生变化,很容易导致意外行为。

面向领域的微服务架构设计
为了调试一个 pick-up 问题,工程师们不得不通过12个不同团队的约50个服务进行调试

理解服务之间的依赖关系可能变得相当困难,因为服务之间的调用可能会深入许多层,第 n 个依赖项中的延迟峰值可能会导致上游的一连串问题,如果没有合适的工具,要了解实际发生的事情是不可能的,这就使得调试变得非常困难。

面向领域的微服务架构设计
大约在2018年中期的 Uber 的微服务架构,来自 Jaeger

为了构建一个简单的功能,工程师往往需要跨多个服务工作,所有这些服务都由不同的个人和团队拥有。这就需要广泛的合作,在会议、设计和代码审查上花费时间。由于团队在彼此的服务中构建代码,修改彼此的数据模型,甚至代表服务所有者执行部署,早期对服务所有权的明确界限的承诺受到了影响。网络化的单体应用可能会形成,看似独立的服务都必须部署在一起才能安全地执行一些变更。

面向领域的微服务架构设计
2018年左右 Uber 的一个复杂流程的例子,在 DOMA 之前,一个简单的集成需要10个接触点

结果是开发者体验变慢、服务所有者不稳定、迁移更痛苦等。但是对于已经采用微服务架构的企业来说,已经没有回头路了,这就变成了 "有它们不能活,没有它们也不能活"的窘境了。

面向领域的微服务架构

如果我们将微服务看作 I/O 绑定的库,将“微服务架构”看作一个大型的分布式应用程序,那么我们就可以利用理解良好的架构来思考如何组织我们的代码。

因此“面向领域的微服务架构”大量地借鉴了已有的成熟的代码组织方式,比如Domain-driven Design,Clean Architecture,Service-Oriented Architecture,以及面向对象和接口的设计模式。我们认为 DOMA 的创新之处在于它是在大型组织的大型分布式系统中利用既有设计原则的一种相对新颖的方式。

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

  1. 我们不以单个微服务为导向,而是以相关微服务的集合为导向,我们称之为领域。

  1. 我们进一步创建领域的集合,我们称之为层,域所属的层确定了该领域内的微服务允许承担哪些依赖关系,我们把这叫做层设计。

  1. 我们为领域提供清晰的接口,我们将其视为集合的单一入口点,我们把这些叫做网关。

  1. 最后,我们确定每个领域都应该与其他域无关也就是说,一个域不应该在其代码库或数据模型里面硬编码与另一个域相关的逻辑。但是由于经常还是有团队需要将逻辑包含到另一个团队的领域中去,为此我们提供了一个扩展架构来支持域内定义良好的扩展点。

通过提供系统化的架构、领域网关和预定义的扩展点,DOMA 打算将微服务体系结构从复杂的东西转换为可理解的东西:一组灵活、可重用和分层的结构化组件。

本文其余部分将深入探讨 Uber 对 DOMA 的实施,以及对可能想要采用这种方法的公司一些实用建议。

Uber 的实施

领域

Uber 的领域代表了一个或多个微服务的集合,这些微服务与功能的逻辑分组相联系。在设计一个领域时,一个常见的问题是“一个领域应该有多大?”,我们这里并没有一些指导方案。有些领域可以包含几十个服务,有些领域只包含一个服务。重要的任务是仔细思考每个集合的逻辑作用。例如,我们的地图搜索服务构成一个领域,票价服务是一个领域,匹配平台(匹配乘客和司机)是一个领域。它们也不一定要按照公司的组织结构来设计,Uber 地图组织本身被分成三个领域,在3个不同的网关后面有80个微服务。

层设计

在 Uber 的微服务架构中,层设计回答了 "什么服务可以调用其他什么服务?"的问题。因此,我们可以将层设计视为 "规模化的关注点分离",我们也可以把层设计看作是 "规模化的依赖管理"。

层设计描述了一种考虑 Uber 服务依赖的故障爆炸半径和产品特性的机制。当领域从底层移动到顶层时,它们在故障情况下影响的服务较少,并且代表更具体的产品用例相反,底层的功能具有更多的依赖,因此往往具有更大的爆炸半径,并代表一组更通用的业务功能。如下图所示:

面向领域的微服务架构设计

我们可以把上层看作是具体的用户体验(例如移动端的功能特性)而底层则是通用的业务功能(如帐户管理)。层级只依赖于它们下面的层,这给我们提供了一个有用的启发式方法来思考爆炸半径和领域整合等问题。

值得注意的是,一些功能特性经常会从图上的 Specific(具体)下移到 General(一般)的位置。我们可以想象,随着需求的发展,一个简单的特性最终会变得越来越像一个平台。事实上,这种向下的迁移是预料之中的,Uber 的许多核心业务平台一开始时都是针对骑手或司机的功能,随着我们开发了更多的业务线,它们也有了更多的一依赖性(比如 Uber Eats 或 Uber Freight),这些功能也变得越来越通用了。

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

  1. 基础设施层:提供任何工程组织都可以使用的功能,这是 Uber 对大型工程的解决方式,比如存储或网络。

  1. 业务层:提供了 Uber 作为一个组织可以使用的功能,但这些功能并不针对特定的产品类别或业务线,如乘车、饮食或货运。

  1. 产品层:提供与特定产品类别或业务相关的功能,但与移动应用程序无关,例如“请求乘车”的逻辑,该逻辑被多个面向乘车的应用程序(Rider、Rider“Lite”、m.uber.com 等)所使用。

  1. 显示层:提供与面向消费者的应用(移动/web)中存在的功能直接相关的功能。

  1. 边缘层:安全地将 Uber 服务暴露给外部世界,。

从上面我们可以看出后续的每一层都代表着越来越具体的功能分组,并且爆炸半径越来越小(或者说依赖该层内部功能的组件越来越少)。

网关

在微服务架构中,"网关 API "这个术语已经是一个广为人知的概念。我们的定义与既定的定义没有很大的区别,只是我们倾向于把网关完全看作是进入底层服务集合的一个单一入口点,一个网关的成功依赖于 API 设计的成功。

面向领域的微服务架构设计
该网关 抽象出了领域的内部细节-多个服务、数据表、ETL管道等,只有接口-RPCAPI、消息传递事件和查询暴露给其他领域。

由于上游消费者只在一个服务上操作,网关在未来的迁移、可发现性和整体降低系统复杂性方面提供了许多好处,上游服务只采取单一的依赖,而不是依赖一个域内可能存在的多个下游服务。如果我们从面向对象设计的角度来考虑网关,那么它们就是接口定义,它使我们能够在底层的“实现”(在本例中是底层微服务的集合)方面做我们想做的任何事情。

扩展

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

逻辑扩展

逻辑扩展提供了一种机制来扩展服务的底层逻辑。对于逻辑扩展,我们使用的是提供者或插件模式的变体,其接口是以服务为基础定义的。这使得扩展团队可以在不修改底层平台核心代码的情况下,以接口驱动的方式实现扩展逻辑。

比如,一个司机上线,通常情况下,我们会进行各种检查以确保司机被允许上线(安全检查、合规性等),其中的每一检查项都是由各个团队负责的,一种实现方式是让每个团队在同一个端点中编写逻辑,但这可能会引入复杂性。每个检查都需要自定义、完全不相关的逻辑。

在逻辑扩展的情况下,“上线”端点将定义成一个接口,他们希望每个扩展都符合预定义的请求类型和响应,每个团队都会注册一个扩展,负责执行这个逻辑。在这种情况下,他们可能只需了解一些有关司机的上下文,并返回一个 bool,说明司机是否可以上线即可。上线的端点将简单地遍历这些响应,并确定其中所有响应是否为 false。

这就将核心代码与每个扩展解耦,并提供了扩展之间的隔离,它不知道其他逻辑在执行什么。围绕这一点,很容易构建更多的功能,比如可观察性或特征标志。

数据扩展

数据扩展提供了一种将任意数据附加到接口的机制,以避免核心平台数据模型的臃肿。对于数据扩展,我们利用 Protobuf 的 Any 功能,这样团队就可以将任意数据添加到请求中,服务通常会存储这些数据或将其传递给逻辑扩展,这样核心平台永远不会负责反序列化这个 Any 的上下文,Protobuf的任何实现都会有一些基础设施开销,以获取更强的类型,对于更简单的实现,可以使用 JSON 字符串来表示任意数据。

自定义

除了逻辑和数据扩展之外,Uber 的许多团队都推出了适合自己领域的扩展模式。例如,与我们的展示架构层相关联的许多集成都使用了基于 DAG 的任务执行逻辑。

优点

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

在 Uber,DOMA 还很年轻,我们很高兴将来能分享更多的数据和更深入的例子来了解我们的架构,不过,在简化开发人员工作和降低整体系统复杂度方面,现阶段的表现是非常积极乐观的。

产品和平台

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 只是一种在实践中思考这些原则的方法。

这项工作引入了业内现有的多种设计模式来解决 Uber 的问题,同时也提出了一些新的模式,比如扩展。希望本文对大家有所帮助。

原文链接:https://eng.uber.com/microservice-architecture/