面向领域的微服务体系结构

2020-07-24 23:59:38

最近,围绕面向服务的体系结构(尤其是微服务体系结构)的缺点进行了大量讨论。虽然就在几年前,由于微服务体系结构提供的许多好处(如独立部署形式的灵活性、清晰的所有权、系统稳定性的改善和更好的关注点分离),许多人很容易采用它们,但近年来,人们开始谴责微服务极大地增加复杂性的趋势,有时甚至使微不足道的功能很难构建。

随着优步发展到大约2200项关键微服务,我们亲身经历了这些权衡。在过去的两年里,优步一直在试图降低微服务的复杂性,同时仍然保持微服务架构的好处。通过这篇博客文章,我们希望介绍我们对微服务体系结构的通用方法,我们称之为“面向域的微服务体系结构”(DOMA)。

尽管由于这些缺点,近年来批评微服务体系结构很流行,但很少有人主张直接拒绝微服务体系结构。运营效益太重要了,而且似乎没有其他选择,或者说选择有限。我们使用DOMA的目标是为希望降低总体系统复杂性同时保持与微服务架构相关的灵活性的组织提供一条前进的道路。

这篇文章解释了DOMA,导致Uber采用此架构的顾虑,它对平台和产品团队的好处,以及对想要采用此架构的团队的一些建议。

微服务是面向服务体系结构的扩展。与本世纪初相当大的“服务”不同,微服务是代表一组范围狭窄的功能的应用程序。这些应用程序通过网络托管和使用,并公开定义良好的接口。其他应用程序通过进行“远程过程调用”(RPC)来调用此接口。

微服务体系结构的关键特征是托管、调用和部署代码的方式。如果我们考虑大型的单片应用程序,它们通常被拆分成具有定义良好的接口的封装组件。然后,这些接口将直接在进程内调用,而不是通过网络调用。通过这种方式,我们可以开始将微服务视为性能受到影响(网络I/O和序列化/反序列化)的库,以便调用它的任何函数。

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

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

在优步,我们之所以采用微服务架构,是因为我们(大约在2012-2013年)主要拥有两项单一服务,并且遇到了许多微服务解决的运营问题。

可用性风险。单一代码库中的一次回归可能会导致整个系统(在本例中,是整个Uber)瘫痪。

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

关注点分离不佳。对于庞大的代码库,很难保持良好的关注点分离。在指数增长的环境中,权宜之计有时会导致逻辑和组件之间的边界较差。

执行效率低下。这些问题加在一起,使得团队很难自主或独立地执行。

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

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

系统可靠性。在微服务体系结构中,总体系统可靠性提高。单个服务可以关闭(和回滚),而不会影响整个系统。

关注点分离。从体系结构上讲,微服务体系结构迫使您问这样一个问题:“为什么存在这个服务?”更清楚地定义不同组件的角色。

明确所有权。谁拥有哪些代码就变得清晰多了。服务通常在个人、团队或组织级别拥有,从而实现更快的增长。

自主行刑。独立部署+更清晰的所有权界限释放了各种产品和平台团队的自主执行能力。

开发人员速度。团队可以独立部署他们的代码,这使他们能够按照自己的进度执行。

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

然而,随着公司规模的扩大,从100名工程师发展到1000名工程师,我们开始注意到一系列与系统复杂性大大增加相关的问题。使用微服务体系结构,人们可以用单一的整体代码库来换取黑盒,这些黑盒的功能随时可能改变,并且很容易导致意外的行为。

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

为了构建一个简单的特性,工程师通常必须跨多个服务工作,所有这些服务都归不同的个人和团队所有。这需要与花费在会议、设计和代码审查上的时间进行广泛协作。随着团队在彼此的服务中构建代码、修改彼此的数据模型,甚至代表服务所有者执行部署,早先明确的服务所有权线承诺遭到破坏。可以形成网络化的整体,其中看似独立的所有服务都必须部署在一起才能安全地执行任何更改。

结果是开发人员体验变慢、服务所有者变得不稳定、迁移变得更加痛苦等等。对于已经采用微服务架构的组织来说,没有回头路可走。这就变成了“不能和他们一起生活,不能没有他们。”

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

因此,“面向领域的微服务体系结构”在很大程度上借鉴了现有的代码组织方式,如领域驱动设计、干净体系结构、面向服务的体系结构以及面向对象和面向接口的设计模式。我们认为DOMA是创新的,因为它是在大型组织的大型分布式系统中利用已建立的设计原则的一种相对新颖的方式。

我们不是面向单个微服务,而是面向相关微服务的集合。我们称这些域为域。

我们进一步创建域的集合,我们称之为层。域所属的层确定该域中的微服务可以承担哪些依赖关系。我们称之为层设计。

我们为我们视为进入集合的单个入口点的域提供干净的接口。我们称这些为网关。

最后,我们确定每个域应该与其他域无关,也就是说,一个域不应该在其代码库或数据模型中硬编码与另一个域相关的逻辑。由于团队经常需要在另一个团队的域中包含逻辑(例如,数据模型上的自定义验证逻辑或一些元上下文),因此我们提供了一个扩展体系结构来支持域中定义良好的扩展点。

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

这篇文章的其余部分深入探讨了优步实施DOMA的情况,我们已经看到的好处,以及对可能想要采用这种方法的公司的实用建议。

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

层设计回答了“什么服务可以调用其他什么服务?”在优步的微服务架构中。因此,我们可以将层设计视为“规模上的关注点分离”。或者,我们可以将层设计视为“规模依赖管理”。

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

您可以将顶层视为特定的用户体验(例如移动功能),将底层视为通用的业务功能(例如帐户管理或市场旅行)。层只依赖于它们下面的层,这给了我们一个有用的启发式方法来考虑爆炸半径和区域积分等问题。

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

基础设施层。提供任何工程组织都可以使用的功能。这是优步对存储或网络等重大工程问题的答案。

业务层。提供优步作为一个组织可以使用的功能,但不特定于特定的产品类别或业务线(LOB),如顺风车、餐饮或货运。

产品层。提供与特定产品类别或LOB相关但与移动应用程序无关的功能,例如面向多个骑行应用程序(Rider、Rider“Lite”、m.uber.com等)利用的“请求搭车”逻辑。

介绍。提供与面向消费者的应用程序(移动/网络)中存在的功能直接相关的功能。

边缘层。将优步服务安全地暴露给外界。这一层也是移动应用感知的。

如您所见,后续的每个层代表越来越具体的功能分组,并且具有越来越小的爆炸半径(换句话说,较少的组件依赖于该层中的功能)。

术语“网关API”在微服务体系结构中已经是一个广泛确立的概念。我们的定义与已建立的定义没有太大不同,只是我们倾向于将网关视为进入底层服务集合(我们称为域)的单个入口点。网关的成功取决于API设计的成功。

由于上游用户仅对单个服务进行操作,因此网关在未来的迁移、可发现性和系统复杂性的总体降低方面提供了许多好处,上游服务仅具有单一依赖关系,而不是依赖于域中可能存在的多个下游服务。如果我们从OO设计的意义上考虑网关,它们是接口定义,使我们能够在底层“实现”(在本例中是底层微服务的集合)方面做任何我们想做的事情。

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

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

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

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

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

数据扩展提供了一种将任意数据附加到接口以避免核心平台数据模型膨胀的机制。对于数据扩展,我们利用Protobuf的任何功能,以便团队可以向请求添加任意数据。服务通常会存储此数据或将其传递给逻辑扩展,因此核心平台永远不会负责反序列化(并因此“知道”)此任意上下文。Protobuf的任何实现都会带来一些基础设施开销,以换取更强的类型。对于更简单的实现,可以同样容易地使用JSON字符串来表示任意数据。

除了逻辑和数据扩展之外,优步的许多团队都引入了适合其领域的自己的扩展模式。例如,与我们的表示体系结构相关的许多集成都使用基于DAG的任务执行逻辑。

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

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

DOMA是优步产品和平台团队共识努力的结果。平台支持成本通常会下降一个数量级。产品团队受益于护栏和加速开发。例如,我们扩展体系结构的早期平台使用者通过采用扩展体系结构,减少了代码审查、规划和学习曲线的时间,从而能够将区分优先顺序和集成新功能的时间从三天减少到三个小时。

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

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

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

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

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

因此,我们认为发展微服务体系结构更像是“修剪篱笆”,以使其最终正确增长,而不是自上而下或一次性体系结构(或重新体系结构)的努力。这是一个动态和进步的过程。

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

在小型组织中,运营优势可能无法抵消架构复杂性的增加。此外,微服务体系结构通常需要专门的工程资源来支持

.