近来,一些关于面向服务架构的话题,特别是针对微服务架构的弊端这个话题上进行了大量的讨论。虽然在几年前,微服务架构受到很多人的青睐,因为它们提供了许多好处,如独立部署的灵活性、明确的所有权、系统稳定性的改善以及更好的分离问题等优点。但是不久,就开始有人吐槽微服务会大幅增加系统复杂性,有时甚至连一些简单的功能都难以构建。
随着Uber发展,我们目前拥有了大约2200个关键的微服务,并且也亲身经历了这些权衡。
在过去的两年里,Uber一直在试图降低微服务的复杂性的同时仍然保持着微服务架构的优势。
我们希望通过这篇博文介绍我们对微服务架构的通用方法,我们称之为“面向领域的微服务架构”(DOMA)。
由于这些缺点,近年来也有一些批评微服务架构的声音,但是却很少有人主张彻底拒绝微服务架构。运营收益太重要了,而且似乎没有有效的替代方案。我们介绍DOMA的目的是为了给那些希望降低整体系统复杂性,同时又保持微服务架构相关灵活性的组织提供一些经验建议。
这篇文章主要解释了什么是DOMA,以及Uber采用这种架构的原因,它对平台和产品团队带来哪些好处。最后,给想要采用这种架构的团队一些建议。
微服务是面向服务架构的延伸。与2000年代相当大的“服务”相比,微服务是代表一组小型功能的应用程序。这些应用通过网络托管,并暴露出一个明确定义接口。其他应用程序通过进行远程过程调用(RPC)方式来调用这个接口。
微服务架构的特点是代码的托管、调用和部署方式。比如大型的单体应用,它们通常会被分割成具有明确定义接口的封装组件。然后,这些接口就可以直接在进程中调用,而不是通过网络。通过这种方法,我们将微服务看作一个性能受到影响的库(网络I/O和序列化/反序列化),以便调用它的任何功能。
当我们以这种方式来思考微服务时,可能会质疑为什么我们会采用微服务架构。答案通常是独立部署和扩展。对于一个大型的单体应用程序,应用被迫一次性部署或发布所有的代码。应用程序的每一个新版本都可能涉及许多更改。部署变得风险大、耗时长。任何人都可以使整个系统瘫痪。
换句话说,业务采用微服务是以牺牲性能为代价来获取运营利益。业务还必须承担维护支持微服务所需的基础设施的成本。事实证明,在很多情况下,这种权衡是很有意义的,但这也是反对过早采用微服务架构的有力论据。
在Uber,我们也采用了微服务架构,因为我们(大约在2012-2013年)主要有两个单体服务,遇到了很多通过微服务来解决的运营问题。
可用性风险。单体代码库内的一次回滚就会使整个系统(在本例中是Uber的全部)瘫痪。
部署风险大,成本高。在需要频繁回滚的情况下,执行这些操作既困难又耗时。
不平滑的关注点分离。在庞大的代码库中,很难保持良好的关注点分离。尤其在一个指数级增长的环境中,权宜之计有时会导致逻辑和组件之间的界限不清。
执行效率低下。这些问题加在一起,使得团队很难独立自主地执行任务。
随着Uber从10多个工程师发展到100多个工程师,多个团队拥有技术栈的碎片时,这种单一的架构将团队的命运捆绑在一起,使得独立运作变得困难。
因此,我们采用了微服务架构。最终,我们的系统变得更加灵活,这使得团队能够更加自主。
系统的可靠性。在微服务架构中,整体系统的可靠性上升。单个服务可以在不影响整个系统的情况下宕机(并被回滚)。
关注点的分离。从架构上来看,微服务架构迫使你去问 "这个服务为什么存在?"更加清晰地定义不同组件的角色。
明确所有权。代码拥有者变得更加清楚。服务通常由个人、团队或组织级别拥有,从而实现更快的增长。
自主执行。独立的部署 更清晰的所属权限,让不同的产品和平台团队能够自主执行。
开发人员的速度。应用团队可以独立部署他们的代码,这使得他们能够按照自己的项目进度来执行。
毫不夸张地说,如果没有微服务架构,Uber不可能达到今天所保持的规模和执行质量。
然而,随着公司规模的进一步扩大,从100多名工程师到1000多名工程师,我们开始注意到一系列与系统复杂性大大增加的相关问题。在微服务架构下,人们用单一的整体代码库换取了黑盒,黑盒的功能随时可能发生变化,很容易造成意外情况。
例如,工程师们不得不通过12个不同团队大约50个服务来调查问题的根本原因。
理解服务之间的依赖关系可能会变得相当困难,因为服务之间的调用可能会深入许多层。第n个依赖关系的延迟峰值可能会导致上游的一连串问题。如果没有合适的工具,就不可能看到实际发生的情况,这让调试变得困难。
Jaeger于2018年中发布的Uber微服务架构
为了构建一个简单的功能,工程师往往需要跨多个服务工作,所有这些服务都由不同的个人和团队所拥有。这就需要跨部门跨团队的合作,在会议、设计和代码审查上花费时间。由于团队在彼此的服务中构建代码,修改彼此的数据模型,甚至代表服务所有者执行部署,早期对服务所有权的明确界限划分受到了影响。网络化的单体可能会形成,看似独立的服务都必须一起部署才能安全地执行任何变更。
大约在2018年Uber的复杂流程示例,在DOMA之前需要10个接触点才能进行简单集成。
这样所带来的结果就是开发进度变慢、服务所属不稳定、迁移更困难等。对于已经采用微服务架构的企业来说,已经没有回头路了。这就变成了“有了它们不能活,没有它们不能活”。
如果我们可以将微服务视为I / O绑定的库,而将“微服务架构”视为大型的分布式应用程序,则可以使用众所周知的架构来思考如何组织代码。
因此,“面向领域的微服务体系结构”大量借鉴了组织代码的既定方法,例如域驱动设计,清晰架构,面向服务的体系架构以及面向对象和面向接口的设计模式。我们认为DOMA仅是创新,因为它是在大型应用的分布式系统中利用既定设计原则的相对新颖的方法。
围绕相关微服务的集合,称为域。
域的集合称之为层。域所属的层确定了允许该域内的微服务承担什么依赖性,称为层设计。
为域提供接口,这些域被视为集合的单个入口点,称为网关。
确定每个域都应该与其他域不可知,一个域不应该具有与其代码库或数据模型内部硬编码的另一个域相关的逻辑。由于团队经常需要在另一个团队的域中包含逻辑(例如,自定义验证逻辑或数据模型上的某些元上下文),因此我们提供了一种扩展架构,以支持该域中定义明确的扩展点。
通过提供系统的体系结构,域网关和预定义的扩展点,DOMA打算将微服务体系结构从复杂的东西转变为可理解的东西:结构化的一组灵活,可重用和分层的组件。
这篇文章的其余部分将深入研究Uber在DOMA中的实施,我们已经看到的好处以及为可能希望采用这种方法的公司提供的实用建议。
Uber域代表一个或多个与逻辑功能分组绑定的微服务的集合。设计域时常见的问题是“域应该有多大?”有些域可以包含数十个服务,有些域只能包含单个服务。重要的任务是仔细考虑每个集合的逻辑角色。例如,我们的地图搜索服务构成一个域,票价服务是一个域,匹配平台(匹配骑手和驾驶员)是一个域。这些也不总是遵循公司的组织结构。Uber Maps组织本身分为三个域,在三个不同的网关后面有80个微服务。
层设计回答了“什么服务可以调用其他什么服务?”的问题。在Uber的微服务架构中,我们可以将层设计视为“规模化的关注点分离”,或者,我们可以将层设计视为“规模化的依赖管理”。
层设计描述了一种机制,用于考虑Uber的故障影响范围和跨服务依赖的产品特异性。当域从底层移到顶层时,它们在中断的情况下会影响较少的服务,并代表更多特定的产品使用案例。相反,底层的功能具有更多的依存关系,因此趋向于具有更大的影响半径,并代表了更通用的业务功能集。下图说明了此概念。
可以将顶层视为具体的用户体验(例如移动功能),将底层视为通用的业务功能(例如帐户管理或市场行程)。层仅取决于其下的层,这为我们提供了一种有用的启发式方法,可以思考影响范围和区域集成等问题。
值得注意的是,功能经常会从这个图表中 "向下 "移动,从具体到更普遍。可以想象,一个简单的功能,随着需求的变多,最终会变成越来越多的平台。事实上,这种向下迁移是意料之中的,Uber的许多核心业务平台一开始都是针对骑手或司机的功能,随着我们开发出更多的业务线,它们也有了更多的依赖性,就会变得越来越通用(比如Uber Eats或Uber Freight)。
基础设施层。提供任何工程项目都可以使用的功能。这是Uber对诸如存储或网络等重大工程问题的处理。
业务层。提供应用可以使用的Uber功能,但并非特定于特定产品类别或业务线(LOB)的功能,例如乘车,进餐或货运。
产品层。提供与特定产品类别或LOB相关但与移动应用程序无关的功能,例如由多个面向应用程序的Rides所利用的“请求乘车”逻辑(Rider,Rider“Lite”,m.uber.com等)。
演示层。提供直接与面向消费者的应用程序(移动/网络)中存在的功能相关的功能。
边缘层。将Uber服务安全地暴露给外界。该层也支持移动应用程序。
每层代表着越来越具体的功能分组,并且影响半径越来越小(或者换句话说,更少的组件取决于该层中的功能)。
在微服务架构中相信大家对“API网关”这个术语并不陌生。而在DOMA中我们的定义的网关其实与大家所熟知的“API网关”的概念相差无几,只是我们倾向于将网关专门视为进入基础服务集合(称为域)的单个入口点。网关的成功取决于API设计的成功。
由于上游使用者仅在单一服务上运行,因此网关在迁移,服务发现以及整体系统复杂度方面提供了许多好处,上游服务仅需一个依赖项,而不是对域中可能存在的几个下游服务的依赖。如果我们从面向对象设计的角度考虑网关,那么它们就是接口定义,它使我们能够根据底层“实现”(在本例中为底层微服务的集合)做我们想做的任何事情。
扩展表示一种扩展域的机制。扩展的基本定义是,它提供了一种扩展基础服务功能的机制,而无需更改该服务的实际实现,也不会影响其整体可靠性。在Uber,我们提供了两种不同的扩展模型:逻辑扩展和数据扩展。扩展的概念使我们能够将架构扩展到能够独立工作的多个团队。
逻辑扩展提供了一种扩展服务的底层逻辑机制。对于逻辑扩展,我们使用提供程序或插件模式的变体,其接口是以服务为基础定义的。这样一来使得扩展团队可以在不修改底层平台核心代码的情况下,以接口驱动的方式实现扩展逻辑。
例如,一个驱动上线。通常,我们会进行各种检查以确保允许驱动上线(安全检查,合规性等)。这些都由一个单独的团队拥有。一种实现方法是让每个团队在同一端点编写逻辑,但这可能会增加复杂性。每次检查都需要自定义且完全不相关的逻辑。
对于逻辑扩展,“上线”端点将定义一个接口,他们希望每个扩展都符合预定义的请求类型和响应。每个团队都将注册一个负责执行此逻辑的扩展。在这种情况下,他们可能简单地获取一些关于驱动程序的上下文况,然后返回布尔值,来判断驱动程序是否可以上线。“上线”端点将简单地遍历这些响应,并确定它们其中是否有问题。
这就将核心代码与每个扩展解耦,并提供了扩展之间的隔离,它不知道其他逻辑在执行什么。围绕这一点,就能很容易建立更多的功能,比如可观察性或者是特征标志等。
数据扩展提供了一种将任意数据附加到接口的机制,来避免核心平台数据模型中的臃肿。对于数据扩展,我们利用Protobuf的Any功能,这样团队可以将任意数据添加到请求中。服务通常会存储这些数据或将其传递给逻辑扩展,这样核心平台就永远不会负责反序列化(从而 "知道")这个任意上下文。Protobuf的任何实现都会有一些基础设施开销,以换取更强的类型化。为了更简单的实现,我们可以直接使用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只是一种在实践中思考这些原则的方法。我们希望其他人觉得它有用,我们也期待着反馈。
特别推荐一个分享架构+算法的优质内容,还没关注的小伙伴,可以长按关注一下:
长按订阅更多精彩▼
如有收获,点个在看,诚挚感谢