原文标题: Untangling Microservices, or Balancing Complexity in Distributed Systems翻译:时序
微服务的蜜月期已经结束了。Uber正在将成千记的微服务重构成一个更加可管理的方案[1];Kelsey Hightower正在预测单体架构将是未来[2]; Sam Newman甚至声明微服务不应该是第一选择,而应该是最后一个选择[3]。
这是怎么回事?尽管微服务承诺了简单和灵活,为什么这么多的项目变得难以维护?或者难道最终单体架构更好?
在这篇文章中,我想要讨论这些问题。你会看到一些将微服务编程一团分布式大泥球的常见设计问题 - 当然,也会看到如何避免他们。
但最开始,让我们先了解下什么是单体架构。
单体架构
微服务一直是被认为是单体应用代码的解决方案。但是单体应用是不是一个问题呢?根据维基百科的定义[4],一个单体应用是自包含且与其他计算应用独立的。与哪些其他应用独立呢?这不是我们在设计微服务时追求的吗?David Heinemeier Hansson[5]指出了单体应用的缺陷。他
因此,微服务不是“修复”单体应用。微服务需要解决的真正问题是交付业务目标的无力。一般,团队是由于指数级增长的 - 或更糟的不可预测性 - 进行变更的成本才交付不了业务目标的。换一句话说,系统不能满足业务的需要。不可控的变更成本不是单体应用的特性,而是大泥球的特性[6]:
大泥球是杂乱的结构,无序,泥泞,缠在一起的电线和胶带,面条代码的丛林。系统显示出无节制增长,重复,临时修复的明显迹象。系统中混乱的将信息在很多极长链路的系统部分中共享,这表示大部分重要信息都变成了全局的或被重复复制的。
对大泥球的复杂性的修改和进化可以由于多个原因引起:协调众多团队的工作,非功能性需求的冲突,或一个复杂的业务域。无论怎样,我们经常试图将这种复杂问题分解成微服务来解决。
微什么?
文字“微服务”指明了服务的一部分可以被度量并且它的价值应该是最小化的。但微服务到底意味着什么?我们看下一些常见的用法。
微团队
第一个工作在服务上的团队大小。而这个尺度可以按披萨来度量。你没听错。 他们说如果工作在服务上的团队可以被2个披萨喂饱, 那么这就是微服务。 我发现这很有启发,我曾经做一个项目而团队可以被一个披萨喂饱... 而我敢对任何人说这团大泥球是微服务。
微代码库
另一种广泛使用的方法时基于它的代码库来设计微服务。有些人将这个概念发挥到了极致,将服务的大小限制到了某些确定的代码行数。就是说,可以构成一个微服务的确切代码行数还没被找到。当这个软件架构的圣杯被发现,我们会进入下一个问题 - 构建微服务团建的编辑器宽度是多少?
有个更严重的问题,这个方法一个没那么极端的版本更流行。代码库的大小常被用来决定它是否是一个微服务。
某些时候,这个方法管用。更小的代码库,更小的业务域。因此,这容易理解,实现,发展。而且,更小的代码库不太可能变成一个大泥球 - 如果发生了,也比较容易重构。
不幸的是,前面提到的简单只是一个错觉。当我们开始基于服务本身来评估服务的设计时,我们忽略了系统设计的核心部分。我们忘记了系统自己,服务作为系统的组成。
“有很多有用和有启发性的方法来定义一个服务的边界。大小是最不重要的部分。” - Nick Tune
我们开发系统!
我们开发系统,而不是服务的集合。我们使用基于微服务的架构来优化系统的设计,而不是设计独立的服务。无论别人怎么说,微服务不能,也永远不会完全解耦,和独立。 你不能打造用完全独立的组件来打造系统! 现在我们看下“系统”的定义[7]:
- 一组连接在一起并可一起操作的物件或设备
- 一组为了一个特定目的一起使用的计算机设备或程序
服务会与其他服务进行不断交互来形成系统。如果你通过优化服务来设计一个系统,却忽略了他们之间的交互,最终你可能是这样的结局:
这些“微服务”可能自身很简单,但系统却变成了复杂性的地狱!
所以我们如何不只是处理了服务的复杂性,而是也考虑了整个系统的复杂性来进行微服务设计呢?
这是个困难的问题,但幸运的是,在很早以前就有答案。
系统视角的复杂性
四十年前,还没有云计算,没有全球规模的需求,不需要每11.7秒部署一次系统。但工程师仍然需要控制系统复杂度。尽管这些工具与现在不一样,但挑战 - 更重要的是, 解决方案 - 都是类似的,也可以被用于基于微服务设计的系统。
在他的书里,“组合/结构设计”[8],Glenford J. Myers讨论了如何用结构化的过程代码来降低复杂度。在书的第一页,他写到:
关于复杂性的主题中有比简单的尝试最小化程序中一部分的本地复杂度更重要的事。一个更重要的复杂度类型是全局复杂度:程序或系统的全局结构的复杂度(比如,程序主要部分的关联或独立程度)。在我们的语境里,本地复杂度就是每个独立微服务的复杂度,而全局复杂度是整个系统的复杂度。 本地复杂度以来与一个服务的实现部分;全局复杂度是被服务间的交互和依赖所定义的。
所以哪一个复杂度更重要 - 本地还是全局?让我们看看当只有一种复杂度被关心时的情况。
要将全局复杂度降到最小实际非常简单。我们只要评估下任何系统组件间的交互 - 即,将所有功能在一个单体服务中实现。就像我们早前看到的,这个策略在某些特定场景是有用的。而在其他场景,它会导致恐怖的大泥球 - 可能是最高级别的本地复杂度。
从另一方面,我们很清楚当你只优化本地复杂度而忽视系统全局复杂度时会发生什么 - 更大的分布式大泥团。
因此,当我们只关注复杂度的某一种,选哪一个并不重要。在一个复杂分布式系统,对向的复杂度都会暴涨。所以,我们不能只优化一个。相反,我们要平衡本地和全局复杂度。
有意思的是,在“组合/结构设计”一书中描述的复杂度平衡不仅与分布式系统有关,其也提供了如何设计微服务的见解。
微服务
让我们先从精确定义什么是服务和微服务来开始。
什么是服务?
根据OASIS标准 9 ,一个服务是:
通过规定好的接口提供能访问一种或多种能力的机制
规定好的接口这部分很重要。服务的接口定义了它暴露给外界的功能。根据Randy Shoup 10的说法,服务的公共接口简单来说就是任何让数据进出服务的机制。它可以是同步化的,如简单的请求/响应模型,或者异步化的,一个生产事件一个消费事件。不管怎么说,同步或异步化,公共接口只代表让数据进出一个服务。Randy也表达了服务的公共接口就跟前门是一样的。
服务是被公共接口定义的,这个定义对于定义什么服务是微服务也足够了。
什么是微服务?
如果一个服务是被它的公共接口定义的,那么 -
一个微服务是指一个用了微型公共接口的服务 - 微型前门
这条在过程式编程中被遵守的规则,如今在分布式系统领域更加有关联性。你暴露的服务越小,它的实现越简单,它的本地复杂性越小。从全局复杂度来看,更小的公共接口会在服务间产生更少的依赖和连接。
微接口的概念也解释了广泛使用的微服务不暴露数据库的实践。没有微服务可以访问另一个微服务的数据库,只能通过其提供的公共接口。为什么?因为,数据库实际是一个巨大的公共接口!只要想想你可以在一个关系数据库上能执行多少种操作。
因此,再重申下,在分布式系统中,我们通过将服务的公共接口最小化的方式来平衡局部与全局复杂度,然后服务就变成了微服务。
警告
这听起来很简单但其实不然。如果一个微服务只是有微型公共接口的服务,那我们可以直接将公共接口限制到只有一个方法。由于这个“前门”已经小的不能再小了,这应该是完美的微服务,对吗?为了解释为什么不这么做,我会使用我另一篇博文11里的一个例子:
加入我们有如下库存管理服务:
如果我们将它拆成八个服务,每个只有一个简单的公共方法,我们可以得到完美的低本地复杂度的服务:
但我们能将它们连入系统来真正管理库存吗?并不行。要形成系统,服务需要与其他服务交互并共享对于每个服务的状态。但它们不行。服务的公共接口不支持。
因此,我们要继承这个“前门”并让这些公共方法可以支持服务间的集成:
完了!如果我们通过将每个服务完全独立的方式来优化复杂度,那么解耦的是很彻底。但是,当我们将服务连入系统,全局复杂度又升高了。不只是导致系统卷入了一团乱麻;为了集成 - 继承公共接口也超出了我们原来的意图。引自Randy Shoup,除了建设了一个小“前门”,我们也建了一个巨大的“员工专用”入口!这告诉我们一个重要的观点:
一个服务有比业务方法更多的集成方法有成长为分布式大泥球的巨大可能!
因此,一个服务的公共接口可以被最小化到什么程度不只是依赖于服务本身,也取决(主要)于它在系统中是哪一部分。一个微服务何时的解耦应该同时考虑系统的全局复杂度和服务的局部复杂度。
设计服务边界
“要找到服务边界太难了... 完全没有流程图!” -Udi Dahan
上面Udi Dahan说的话对于基于微服务的系统来说也很对。设计微服务的边界很难,基本上第一次很难做对。折让设计一个合适复杂度的微服务变成了一个迭代流程。
因此,从更大的边界开始是比较安全的 - 从上下文边界开始比较合适12 - 有更多关于系统各它的业务域的知识后,再将它们解耦成微服务。这对那些包含了核心业务域的服务特别重要13。
分布式系统之外的微服务
尽管微服务只是最近才被“发明”出来,你仍可以在工业界发现很多有同样设计理念的实现。这些包括:
跨功能团队
我们知道跨功能团队是最有效的。这种团队让不同专业能力的小组工作在同一个任务上。一个有效的跨功能团队能最大化团队内的交流,最小化团队外的交流。
我们的工业只是最近才发现跨功能团队,但任务组是一直存在的。其底层的原理与基于微服务的系统是一样的:在团队内高聚合,团队间低耦合。团队的“公共接口”通过需要达成任务的技能来进行最小化(如实现的细节)。
微处理
我要通过Vaughn Vernon那经典的相关主题的博文来举这个例子。在他的博客里,Vaughn描绘了一个微服务与微处理器间有趣的相似点。他讲述了处理器与微处理器间的不同:
我发现了一个通过大小规格来帮助确定一个处理器是中央处理器(CPU)还是微处理器:数据总线 21的大小
微处理器的数据总线就是他的公共接口 - 它定义了可以被传给微处理器与其他组件间的数据量。对于公共接口有严格的尺寸规格来定义这个中央处理器(CPU)是不是一个微处理器。
Unix哲学
Unix哲学,或Unix方式,是一种秉承了极简主义的模块化软件开发规范和文化。22
有人可能会反驳我Unix哲学与我的情况不符,你不能用完全独立的组件来组装一个系统。难道unix程序不是完全独立,然后形成一个可工作的系统的吗?事实正相反。Unix方式几乎字面上定义了程序需要暴露的微交互操作。让我们看看Unix哲学与微服务相关的部分:
第一条原理让程序暴露一个与其功能相关的公共接口,而不是与其原始目标不想关的:
让程序只做一件事并做好。要做另一件事, 写个新的而不是在老程序里加新“特性”。
尽管Unix命令被认为是彼此间完全独立的,但并不是。它们之间需要通信,并且第二条原则定义了通信的接口如何设计:
预期所有程序的输出会作为其他程序的输入,尽管可能现在还不知道。 不要让输出有不相关的信息。避免严格的列式或二进制输入格式。不要强制要求交互式命令有输入。不只是通信接口被严格限制(标准输入,标准输出,标准错误),基于这个原则,在命令间的数据传输也被严格限制住了。例如,Unix命令需要暴露微-接口并永远不依赖于其他命令的实现细节。
那么Nano服务呢?
文字nanoservice经常是用来描述一个服务太小了。有人会说上面例子介绍的一个方法的服务就是nano服务。我不同意这个观点。
nano服务用用来在忽略了整体系统时描述单独服务时用的。在上面例子中,一旦我们将系统放入方程中,服务的接口就会增长。实际上,当我们比较一下原来的单服务实现与解耦后的实现,我们可以看到一旦将服务连入系统,系统从8个公开接口增长到38个。而且,每个服务公开方法的平均数量从1涨到4.75.
因此,当我们又花了服务(公共接口),数据nano服务不再成立,因为服务被迫开始增长来支持系统的用例。
这些够了吗?
不。尽管最小化服务的公共接口是一个设计微服务的好原则,它仍然只是一种探索式的方式而不能取代常识。实际上,微接口只是更加基础,且更复杂的耦合与内聚设计原则的抽象。
比如,如果两个服务有微-公开接口,它们让需要在分布式事务中协调,它们仍是互相高耦合的。
针对微-接口在解决不同类型的耦合,比如函数,开发,语义仍然是有启发的。但那就是另一篇博客的主题了。
从理论到实践
不幸的是,我们没有一个客观方式来量化局部与全局复杂度。从另一方面,我们确实有一些设计方式可以优化分布式系统的设计。
这篇文章主要的内容就是想告诉你在评估服务的公共接口是你要不停的问自己:
- 业务的占比是多少 - 给定服务是面向集成的endpoint吗?
- 这是在业务上不想关的endpoint吗?在不引入面向集成的endpoint的前提下你可以将它们分离成2个或更多服务吗?
- 合并两个服务是否能消除当初为了集成原始服务而产生的endpoint?
可以用这些原则来指导你在服务边界和接口的设计。
概要
我想最后用Eliyahu Goldratt的观点来总结下。在他的书里,他经常重复下面这些句子:
"告诉我你如何度量我,我会告诉你我怎样表现" - Eliyahu Goldratt
当设计基于微服务的系统时,很重要的就是度量和优化正确的指标。为微服务代码库设计边界,则微小组的定义会更容易。所以,开发一个系统,我们要学会算账。微服务是用来设计系统的,而不是独立的服务。
回到这片的标题-“在分布式系统中解决,或平衡微服务的复杂度”。解开微服务问题的唯一办法就是平衡每个服务的局部复杂度与整个系统的全局复杂度。
引用索引
- Gergely Orosz’s tweet on Uber
- Monoliths are the future
- Microservices guru warns devs that trendy architecture shouldn’t be the default for every app, but ‘a last resort’
- Monolithic Application(Wikipedia)
- The Majestic Monolith - DHH
- Big Ball of Mud(Wikipedia)
- Definition of a System
- Composite/Structures Design - book by Glenford J. Myers
9.Reference Model for Service Oriented Architecture
10.Managing Data in Microservices - talk by Randy Shoup
11.Tackling Complexity in Microservices
12.Bounded Contexts are NOT Microservices
13.Revisiting the Basics of Domain-Driven Design
14.Implementing Domain-Driven Design - book by Vaughn Vernon
15.Modular Monolith: A Primer - Kamil Grzybek
16.A Design Methodology for Reliable Software Systems - Barbara Liskov
- Designing Autonomous Teams and Services
- Emergent Boundaries - a talk by Mathias Verraes
- Long Sad Story of Microservices - talk by Greg Young
- Principles of Design - Tim Berners-Lee
- Microservices and [Micro]services - Vaughn Vernon
- Unix Philosophy