分解微服务,还是平衡分布式系统的复杂性

分解微服务,还是平衡分布式系统的复杂性

著者:Vladik Khononov
 
 
    微服务的蜜月期已经结束了。Uber正在把数千个微服务重构成一个更容易管理的解决方案;Kelsey Hightower又开始预言单体架构就是未来;连Sam Newman(《微服务设计》的作者)都在宣称说微服务不应该是默认首选,而是最后的手段。
    发生什么事情?为什么如此多的项目变得难以维护,尽管微服务承诺简单和灵活?或者说单体架构更加好?
    在这篇文章里,我想解答这些问题。你将会了解到从微服务转变成分布式大泥球所遇到的一些问题,当然你能避免它们。
    首先,我们一起来澄清一下什么是单体。
 
单体
 
    微服务总是被定位为单体代码库的一个解决方案。但是,单体应用一定有问题吗?根据维基百科的定义,一个单体应用是自包含并独立于其他计算机应用。独立于其他应用?这不正是我们在设计微服务时候所追求的吗?David Heinemeier Hansson马上疾呼抹黑单体应用,他警告说分布式系统天生的可靠性和挑战,并使用Basecamp来证明一个服务于数百万客户的大型系统也是可以通过一个单体代码库实现和维护。
    因此,微服务没有解决掉单体。真正的问题是微服务应该解决的真正问题是无法交付业务目标。通常,团队之所以不能实现业务目标,是因为变更成本呈指数级增长,或者更糟糕的是成本不可预测。换句话说,系统无法跟上业务需求的节奏。不受控的变更成本不是单体应用的属性,而是一个大泥球。
 
大泥球是一个杂乱结构的、松散的、带一堆管道或补丁、意大利面式风格的丛林(暗指危险地带)。这类系统展示出不受控的增长和重复性的临时修复等明显迹象。信息在系统中的相互远离的元素间共享,常常达到差不多所有重要信息变成全局性或者重复性的程度。
变更或者演进一个大泥球的复杂性可有由多个因素所引起:协调多个团队的工作、相互冲突的非功能性需求、或者是一个辅助的业务领域。无论如何,我们经常试图通过把这种笨拙的解决方案分解成微服务来解决这种复杂性。
 
微什么?
 
    “微服务”这个名词暗示着一个服务的某部分能够被度量,并且它的价值应该被最小化。但是,微服务究竟意味着什么?让我们看看这个词的集中常见用法。
 
微团队
    第一个是服务团队的规模。这个标准应该用披萨来度量。严肃的说,如果一个服务团队可以用两个披萨来填饱肚子,那么这就是微服务。我发现这真是一种富含启发性的传闻,因为我曾经和一个能被一个披萨喂饱的团队一起建设过一个项目,而且我敢说没人敢把那些泥球称为微服务!
 
微代码库
    另一种广泛使用的方法是根据代码库的大小来设计微服务。有些人将这个概念发挥到了极致,试图将服务大小限制在一定数量的代码行。也就是说,组成一个微服务所需的确切行数还有待去发现。而一旦这个软件架构的圣杯被发现,我们将进入下一个问题——构建微服务的推荐编辑器的宽度是什么?
    更严肃地说,这种方法的一个不那么极端的版本是最流行的。代码库的大小通常被用作判断是否微服务的一个富含启发性的方法。
    在某种程度上,这种方法是讲得通的。因为代码库越小,业务域的范围就越小。因此,它更容易被理解、实现和演进。此外,更小的代码库转变成一个大的泥球的机会更少。如果它确实发生了,那么重构就更简单了。
    不幸的是,上述的简单性只是一种幻觉。当我们基于服务本身评估服务的设计时,我们忽略了系统设计的一个关键部分。我们忘记了系统本身,就是那个服务只是其中一个组件的那个系统。
    
定义服务的边界有许多有用的启发方法。大小是最没用的。——Nick Tune
 
我们建设系统
 
    我们建设的是系统,而不是一堆服务。我们利用微服务架构来优化一个系统设计,而不是设计一堆独立的服务。不管其他人怎么说,微服务不能,并且将来也永远既不能被完全解耦,也不能完全独立。你不能使用独立的组件来构建一个系统,因为那违背了“系统”的定义。
 
1. 一组连接在一起的事物或设备
2. 一套用于特定目的的计算机设备和程序
 
    服务之间总是会互相交互而形成一个系统。如果你通过优化它的服务但是却忽视他们之间的交互设计来设计这个系统的话,以下就是你将会见到的一个局面。
    
    

 

        这些“微服务”可能单独很简单,但是系统本身就是一个复杂的地狱!那么我们如何设计微服务来处理服务和系统级别的复杂性呢?这是一个很难回答的问题,但幸运的是这个问题很久以前就有人回答了。

 
关于复杂性的系统性视角
 
    四十年前,没有云计算,没有全球范围的需求,也不需要每11.7秒部署一个系统。但是,工程师们仍然不得不驯服系统的复杂性。尽管当时的工具有所不同,但现在的挑战(更重要的是解决方案)都是相关的,可以应用到基于微服务的系统设计中。
    在Glenford J. Myers的书《复合/结构化设计》中,讨论到如何构造过程代码以减少其复杂性。在该书的第一页写道:
 
There is much more to the subject of complexity than simply attempting to minimize the local complexity of each part of a program. A much more important type of complexity is global complexity: the complexity of the overall structure of a program or system (i.e., the degree of association or interdependence among the major pieces of a program)。
    在我们的上下文中,所谓的局部复杂性就是每个微服务的复杂性,而全局复杂性是整个系统的复杂性。局部复杂性取决于服务如何实现,而全局复杂性则是由服务间的交互和依赖关系所定义的。
    那么,哪种复杂性更重要,局部复杂性还是全局复杂性?让我们看看当只处理其中一种复杂性时会发生什么。
    将全局复杂性最小化是非常容易的,我们所要做的就是消除系统各组成部分之间的相互作用,例如直接在一个单体服务中实现所有的功能。正如我们前面看到的,这种策略在某些情况下可能有效。在另一些地方,它可能会导致可怕的大泥球,即可能是局部复杂性的最高级别。
    另一方面,如果只优化局部复杂性,而忽略了系统的全局复杂性,即更可怕的分布式大泥球,那我们知道将会发生什么。
    
    
 
    因此,如果我们只关注其中一种复杂性,那么选择哪一种已经不重要了。在一个非常复杂的分布式系统中,相反的那个复杂性会陡增(如果只优化一个的话)。因此,我们不能只优化一个,相反地我们必须平衡局部复杂性和全局复杂性。
    有趣的是,《复合/结构化设计》一书描述的平衡复杂性的办法不仅仅跟分布式系统有关,他们还提供如何设计微服务的见解。
 
微服务
    让我们来准确定义我们正在讨论的那些服务和微服务是什么吧。
 
    什么是服务?
    根据OASIS标准,一个服务是一种允许通过规定的接口的方式来访问一个或多个功能的机制。
    
    规定的接口部分是很关键的,因为一个服务接口定义了它向外界提供的功能。根据Randy Shoup的说法,服务的公共接口说白了就是从服务中获取或提供数据的一个简单机制。它可以是类似普通的请求/响应模型的同步模式,也可以是像生产与消耗的事件驱动的异步模式。不管是同步的还是异步的,公共接口只是一个将数据输入或输出的方法。Randy还将服务的公共接口描述为它的前门。
 
    服务是由其公共接口定义的,这个定义足以定义是什么使服务成为微服务。
 
微服务是什么?
 
    如果一个服务是被它的公共接口所定义的话,那微服务就是一个带有微公共接口(微前门)的一个服务。
    这个简单的启发在过程式编程时代就已经被采用了,而且它在分布式系统领域非常重要。你所暴露的公共服务越小,其实现就越简单,其局部复杂性就越低。从全局复杂性的角度来看,越小的公共接口在服务之间产生更少的依赖和连接。
 
    微接口的概念也说明了微服务不暴露其内部数据库的广泛实践。任何微服务都不能直接访问另一个微服务的数据库,而只能通过它的公共接口。这是为什么呢?好吧,数据库将是一个巨大的公共接口!只需考虑一下你在关系数据库上可以执行各种各样的操作。
 
    因此,重申一下,在分布式系统中,我们通过最小化服务的公共接口来平衡本地和全局复杂性,从而使它们成为微服务。
 
警告
 
    这种启示听起来似乎很简单。如果一个微服务只是一个具有微公共接口的服务,那么我们可以将公共接口限制为只有一个方法。既然前门不能再小了,那就应该是完美的微服务,对吧?其实不然。为了说明为什么,我将使用我的另一篇关于这个主题的文章中的例子:
 
    这里我们假设有以下的backlog management服务:
 
    一旦我们把它解耦成8个服务,每个有一个单一公共方法,那我们就得到那些具有完美局部复杂性的服务。
 
    
    但是我们可以将它们拼接在到实际管理backlog的系统中吗?其实不然。为了形成系统,服务间必须彼此交互并共享对每个服务状态的更改。但它们不能,因为它们的公共接口不支持这一点。
    因此,我们必须使用支持服务之间集成的公共方法来扩展这个前门。
 
    
 
    Boom。如果我们只是单独地优化每个服务的复杂性(即局部复杂性),那么简单的分解就可以工作得很好。然而,当我们试图将服务连接到系统中时,全局复杂性就会引入。结果是系统不仅变成一个纠缠不清的烂摊子,而且为了集成我们还必须扩展公共接口,而这是超出(或者说违背)我们最初的意图。改述一下Randy Shoup的话,除了小小的前门,我们创造了一个巨大的“员工专用”通道,而这会引出了一个重要的问题:
    对分布式大泥球来说,一个服务里面它具有为集成而引入的方法比业务方法更多的话,这本身就是极具启发性的。
    因此,使服务的公共接口最小化的阈值不仅取决于服务本身,而且主要取决于服务所属的系统。对微服务的适当分解应该综合平衡系统的全局复杂性和其服务的局部复杂性。
 
设计服务边界
    
    Udi Dahan曾经说过:“发现服务边界是真的很难,这里没有流程图。”。
    
    上述说法对于基于微服务架构的系统尤其正确。设计微服务的边界是困难的,而且可能无法在第一次就做好。这使得设计一个相当复杂的基于微服务的系统成为一个迭代过程。
 
    因此,更安全的做法是从更宽的边界入手(可能是适当的有界上下文的边界),然后随着对系统及其业务领域获得更多知识再将它们逐步分解为微服务,这与包含核心业务域的服务尤其相关。
 
分布式系统以外的微服务
 
    即使微服务是最近才“发明”出来的,你也可以在其他行业找到大量相同设计原则的实践。这些简单的理念包括:
    
跨职能团队
    我们都知道跨职能团队是最有效的团队,这样的团队是由为了完成同一个任务而由不同专业人员组成的。这样一个高效的跨职能团队能够最大化团队内部的沟通和最小化团队外部的沟通。
 
    我们的行业最近才发现了跨职能团队,但是任务组已经存在很久了。这些的基本原则与基于微服务的系统的基本原则相同:团队内部的高内聚性和团队之间的低耦合。团队的“公共接口”通过将完成任务所需的技能纳入团队(即实现细节)。
 
微处理器
    我在Vaughn Vernon关于同一主题的精彩博客中偶然发现了这个例子。在他的文章中,Vaughn将微服务和微处理器进行了有趣的对比。他特别讨论了处理器和微处理器之间的区别:
我发现一个有趣的现象,这里有一个给定的大小分类来帮助确定一个中央处理器(CPU)是否被认为是一个微处理器,这就是它的数据总线的大小。
 
    微处理器的数据总线是它的公共接口——它定义了微处理器和其他组件之间可以传递的数据量。公共接口有一个严格的大小分类,它定义了一个中央处理单元(即CPU)是否被认为是微处理器。
 
Unix哲学
    Unix哲学,或者说Unix方式,是一组文化规范和哲学方法,用于抽象化、模块化的软件开发。
 
    有人可能会说,Unix的哲学与我的观点相矛盾,即不能用完全独立的组件来构建一个系统。难道Unix程序不是完全独立的吗?事实正好相反,Unix方式几乎总是定义程序必须暴露微交互。让我们看看Unix哲学的原则如何与微服务的概念相呼应。
 
    第一个原则要求程序的公共接口必须暴露一个高内聚的功能,而不是用与原始目标毫不相干的功能来把程序弄得一团糟。
目标:让每个程序都做好一件事。要完成一项新的工作,应该重新构建而不是通过添加新的“特性”使旧的程序变得复杂。
    
    即使Unix命令之间被认为完全互相独立,实际上它们不是。它们仍然必须与其他通讯。还有,第二个原则定义了通讯接口应该被如何设计:
期望每个程序的输出成为另一个未知程序的输入。不要用无关的信息干扰输出,避免严格的列或二进制输入的格式,不要坚持交互式输入。
    不仅通信接口受到严格的限制(标准输入、标准输出和标准错误),而且根据这个原则,在命令之间传递的数据也应该受到严格的限制。例如,Unix命令必须公开微接口,并且永远不能依赖于彼此的实现细节。
 
Nano-Service(纳米服务)又怎样?
 
    术语“nanoservice”通常用来描述规模非常小的服务。有人会这样说,前面示例中那些简单的单方法服务就是纳米服务。然而,我并不一定同意这种分类。
 
    纳米服务被用来描述单个服务而忽略了系统的整体性。在上面的例子中,一旦我们将系统放入等式中,服务的接口就必须增长。事实上,如果我们将原始的单服务实践与简单的分解进行比较,我们可以看到,一旦我们将服务连接到一个系统中,整个系统的公共方法将会从8个增加到38个。此外,每个服务的公共方法的平均数量会从期望的1个变成4.75个。
 
    因此,如果我们优化服务的公共接口而不是优化代码库,术语纳米服务将不再适用,因为服务将被迫重新变大以支持其系统的用例(暗指支持系统的整体功能)。
 
就这些吗?
 
    不。虽然最小化服务的公共接口对于微服务设计来说是一个很好的指导原则,但它仍然只是一种启示但并不能取代常识。实际上微接口只是一种抽象,它涵盖了更基本、但更复杂的耦合和内聚设计原则。
 
    例如,如果两个服务具有微公共接口,但是它们必须在分布式事务中进行协调,它们之间仍然是高度耦合的。
 
    也就是说,以微接口为目标仍然是解决不同类型耦合(如功能耦合、开发耦合和语义耦合)的一种强大的启示,但这就是另一个话题。
 
从理论到实践
 
    不幸的是,我们还没有一个客观的方法来量化局部和全局复杂性。另一方面,我们确实有一些设计启发可以用来改进分布式系统的设计。
 
    这篇文章的主要想表达的是你应该通过提出以下问题来不断地评估你的服务的公共接口:
 
    1、在给定的服务中,面向业务和面向集成的端点的比例是多少?
    2、服务中是否存在业务上不相关的端点?您能否在不引入面向集成的端点的情况下将它们拆分为两个或多个服务?
    3、合并两个服务是否会消除由于集成原始服务所引入的端点吗?
 
    请使用这些启示来指导服务边界和接口的设计。
 
总结
 
    我想通过 Eliyahu Goldratt的一个观察来总结这一切。在他的书中,他反复提到:告诉我你如何度量我,那我会告诉你我如何表现。
 
    在设计基于微服务的系统时,测量和优化正确的度量是至关重要的。为微代码库和微团队设计边界肯定更容易。然而,要建立一个系统,我们必须考虑它。微服务是关于设计系统的,而不是设计单独服务的。
 
    这又把我们带回到文中的标题:“分解微服务,还是平衡分布式系统的复杂性”。解决微服务的唯一方法是平衡每个服务的局部复杂性和整个系统的全局复杂性。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章