微服务--极简主义服饰中的建筑虚无主义

2020-11-03 02:20:00

最近,我们一直称之为“微服务”的一些倒退再次引发了围绕软件架构模式的辩论。事实证明,对于越来越多的软件人员来说,拥有(有时是几个)数百个服务的后端毕竟不是一个好主意。这场辩论已经进行了一段时间了,已经说了很多,但我还有几件事想说。

“微服务”博士是一个走得太远、应用得太直率的好主意。修复方法不仅仅是降低粒度旋钮,而是要1)关注拆分-联接标准,而不是大小;2)在应用项目模型和部署模型时区分它们。

微服务作为软件体系结构模式最初取得成功的主要原因有三个:1)强制模块化,2)削弱依赖性,3)没有体系结构的借口。按顺序:

在过去的铁板一块中,理论上您可以强制模块边界。比方说,您可以说您的acme-helper或acme-data工具不能依赖acme-domain,您甚至可以使用一些工具来强制执行这一点,但这是很容易出错的。特别是在大公司里,这些巨石跨越的范围超出了一个团队的认知范围,打破这些界限往往是一件简单的事情,当然也很普遍。愤怒的架构师在微服务中看到了让这些成为过去的承诺:现在开发人员被迫只处理API。代码库分道扬镳,并进行调用以沿网络堆栈下行并返回。

因此,人们在构建时不会依赖于其他服务,只有在运行时才会依赖。太棒了。方法调用变成了http调用。“现在我们不需要关心依赖关系了”--实际的人们这样说,好像依赖关系不是基本的,而仅仅是构建设置中的意外产物。每个人都熟悉了他们的HTTP以及不同的服务器和客户端实现,阅读了所有关于REST和SOAP(以及RPC、RMI和CORBA)的内容,并愉快地在模块(现在是服务)之间创建了一个非常松散的间接层。类型化API、精细网络策略和合同测试出现的时间要晚得多。

在API版本控制、交付语义、错误传播、分布式事务管理和服务的所有调用方中的客户端代码蔓延的复杂性开始显现之前,它让人感觉到解放。这是一个巨大的右移,但嘿,构建过程更简单。

也许更隐秘的是,“做微服务”给那些缺乏关于其架构应该是怎样的论文的组织带来了认可。现在,对于大多数建筑难题,有了一个得到认可的答案:另一项微服务。服务目录中的另一个条目,供任何和所有相关方呼叫。这种互动方的生态,每个人都为了共同利益而行动,这表明了一种潜在的默契,即新兴的服务网状结构将近似于领域的潜在自然架构。

不需要画硬的建筑界线的诱惑是如此的柔软和方便,以至于我们在没有的地方变得懒惰起来,在我们已经在的地方接受了我们的懒惰。如果你不认同这个信念,问题就出在你和你对复杂系统的缺乏理解上,你这个客观主义的白痴。

是的,管理巨石确实很痛苦,当然,许多系统太巨石了(即可部署的东西太大),但对新发现的纯洁性的狂热把钟摆摆动得太远了,就像他们总是做的那样。我们不仅不需要运行这么多这么小的服务,而且我们也不会从隔离它们的代码库中获益太多。总结一下:

削弱我们系统不同部分之间的依赖是一种高息的“右移”贷款;以及。

当思考变得困难时,有一个现成的答案是一个令人宽慰的谎言,它只是在移动复杂性。没有什么可以替代努力地将认知力应用到一个问题上。

两件事:关注拆分服务的正确标准,而不是其规模,并更仔细地应用这些标准。

微服务中的微观充其量应该是一个预测,而不是一个目标。我们可能会预测服务将是微不足道的,但它们不一定是微不足道的。沃弗农说的“凝聚力是有原因的”是对的。

不应该有预先规定的服务粒度。服务的大小没有规定。相反,拆分软件系统的各个部分有好的理由,也有不好的理由。

然而,“软件系统”存在于不同的领域:它们既作为我们交互的工件存在,也作为计算机交互的工件存在。代码和二进制。我们以不同的方式组织它们:项目模型(存储库、项目、模块及其依赖项)和部署模型(生产环境是什么样子,以及可部署项在其中是如何运行的)。

然而,在从粗糙到颗粒(即从整体到微服务)的过程中,很少注意到这两个模型之间的差异-以及可能的间接性。锤子不分青红皂白地敲打着两者,让我们因为运行时的考虑而拆分了代码库,由于项目的考虑而拆分了可部署的代码。

就像人类脊柱的一部分僵硬可能会导致另一部分疼痛一样,构建DAG中的僵硬会导致项目和部署模型之间、存储库和服务之间、我们组织代码的方式和服务运行方式之间的过度镜像。这种镜像一方面阻止我们将对模块之间关系的关注转移到左边,而模块之间的关系通常被设置为弱而脆弱的运行时依赖关系,而另一方面鼓励我们拥有比运行时现实所需要的更多的服务。那会带来痛苦。

解决这种僵硬的核心是认识到构建流至少在概念上是DAG导向的非循环图-其中节点是作业和版本化的工件,并且边将作业连接到版本化的工件(“Products”),或者将版本化的工件连接到作业(“Dependency_of”)。根据定义,可部署项是由部署作业使用的版本化构件。

很长一段时间以来,我们忽略了灵活和无摩擦的构建DAG可以在多大程度上帮助我们改进两边的架构。使用中等丰富的构建模式,我们可以在构建时让代码的意图更清晰,可以验证更多的约束,并且仍然可以将其部署为最简单可行的形式,在执行成本更低、速度更快、更安全的地方运行。

我不确定历史上准确的描述是什么,可以解释整个行业构建模式的过度简单性。我确实从经验中了解到,太多的实践仅适用于非常简单和线性的流,即一个存储库独立地构建一个且只有一个服务。不管关于代码复制及其权衡的合理争论如何,似乎都不喜欢构建时的内部依赖关系,即使这些依赖关系会带来明显需要的数据或逻辑(如消息格式定义)。

我怀疑这可能与很少有CI工具本机支持组合有关(即作业的输出能够成为其他人的输入),语义版本控制在实践中有多容易出错,以及自动化确定性版本传播的难度。

我的意思是保持本地副本与CI同步,构建可重复的、由其下游依赖项自动使用的新上游版本。这并不是一件微不足道的事情,需要一些版本控制和构建,据我所知,大多数实践最终要么通过使用最新版本来牺牲可重复性,要么通过需要重复的手工工作来扼杀流程。因此,有一个简单的构建设置的压力。

不过,确切的原因并不重要。重要的是,克服这一点至关重要。

已经提出了许多拆分或加入软件系统的标准,从社会标准(团队、有界环境)到机械标准(cpu或io有界性),它们都有一定的意义。然而,它们中的大多数要么是拆分项目或模块的好理由,要么是拆分可部署的好理由,很少两者兼而有之。牢记这一点将有助于我们更有效地应用它们。

下面是一些可能的标准和关于它们的应用的一些评论。我并不是想要详尽无遗,只是举例说明对我来说有意义的推理。

不同的运行时--如果代码库的一部分编译成不同的运行时,它就变成了不同的可部署组件,我们称之为不同的服务。

弹性剖面-系统的某些部分可能具有较高的负荷剖面。让它们与其他公司分开扩大和扩大规模,可能会带来回报。

负载类型-一般面向延迟的io系统的某些部分可能会偶尔产生CPU负载峰值,这可能会影响响应时间。将它们放在不同的计算基础设施中可能会更好,也许会占用更多CPU资源,并以吞吐量为导向。

安全性-系统的某些部分可能会处理更敏感的数据或一组更有特权的用户,而其他部分甚至可能是公开的。如果需要的部分在不需要的部分上的安全负担是相当大的(这包括未知的风险),那么将它们分开可能是值得的。

进程可丢弃-理想情况下,我们系统的大多数部分可以很好地处理在一个地方终止而在另一个地方旋转的副本。如果某些部分没有这样做,可能是因为它们有保持未复制状态的长时间事务,根据您的可靠性配置文件和进程调度混乱情况,将它们分开可能是值得的。

可用性要求-系统的某些部分可能可以被终止,要么是因为它们对时间不敏感,可以恢复,要么是因为它们不是那么重要。让它们运行在更便宜、不太可靠的基础设施(如Spot-Instance或本地服务器)上可能是有回报的。

流程复制-如果系统的某些部分不能以分布式方式正常运行,迫使您进行单副本主动-备用部署(充其量也就是),那么您最好将位于单点故障上的逻辑量降至最低。

可靠性-如果一个重要服务的可靠性受到一个行为不端的模块的影响,无论出于什么原因都不能轻易改进,那么将它们分开可能是更可取的做法。

爆炸半径-类似地,服务的表面积可能太大,以至于无论它多么可靠,对可用性的影响都太大了。

依赖地狱-一些运行时有一个扁平的地址空间,其中所有模块都平等链接,而不考虑它们的依赖路径(JVM,我们现在看的是JVM)。当同一模块被引入两次或更多时,它必须是唯一的,并且适用于其所有依赖项,因此需要选择单个版本。虽然与最新版本保持同步,库的次要版本之间的二进制兼容性,以及将模块的不同版本命名为空间的工具确实减轻了重复解决这些版本约束的负担,但有时它可能足够大,足以保证将可部署项拆分并将依赖图一分为二。

正如你所看到的,这些都是关于我们的生产过程在实际运行时遇到的现实,以及由此产生的紧张局势。牢记构建DAG可以提供给我们的项目模型和部署模型之间的间接性,我们看到,在大多数情况下(如果不是所有情况下),几乎没有理由拆分代码库。

因此,部署端拆分的整体设置会产生几个服务,为它们的执行提供了近似最佳的安排,但不一定会产生几个独立的代码库。

变化率-波动性越大应取决于波动性最小的。这就是众所周知的稳定依赖原则。“在稳定的方向上依赖”有助于遏制变化的系统固有的波动性。

不只做一件事--经常被提起的格言“做好一件事和一件事”是规模不变的,因此近乎没有意义。这个东西可以是大的,也可以是小的,但仍然是一个。服务可以进行“计费”或“保存序列化发票”:两者是一回事。此外,做一件以上的事情从根本上没有什么错。

服务的调用图是可分区的(多个事物的可行定义?)。可能是在代码端模块化的一个很好的理由,但仅仅是拆分可部署的正当原因可能存在的迹象。

不同的限定上下文-系统的某些部分可能解决不同类型的问题,因此需要单独的词汇表。在这些不同部分接触的地方,将会将数据从一个域对象转换到另一个域对象。这并不意味着翻译的两端必须是不同的服务。对于图书馆,我们一直都是这样做的。

沃恩·弗农(Vaughn Vernon)建议从有限的上下文开始,更多的是作为一个启发式的,而不是一个明确的规则,但我仍然反对两点:我们不应该仅仅因为感觉正确就为服务设定一个特定的大致规模,也不应该在我们可以有硬标准的时候依赖启发式和“代理理由”。

构建时间-当太多内容是同一构建的一部分时,重新构建相同的模块会带来不便,因为其他模块中的任何更改都会成为一种痛苦。这是拆分构建的一个很好的理由,即使一些模块与其他模块分开构建,并使它们之间的依赖关系是特定于版本的(外部),而不是直接的(内部构建)。这不是改变事情运行方式的好理由。但是,请参见从属关系地狱。

发布大小和适宜性-当集成来自不同项目和模块的更改延迟发布时,您的CD设置就会出现问题(测试覆盖率、依赖关系方向、语义版本控制、…。?),这不是一个人的体系结构的问题。在解决根本问题的同时,可能值得拆分一到两个服务来减轻痛苦。但是,就像伤口引流有时在医学上是必要的一样,尽管皮肤并不缺少开口,但我们要意识到,出于这个原因拆分可部署设备仍然只是一种策略。

不同的团队-团队结构和软件架构之间的关系非常密切。康威定律是真实的,相反的康威策略有效,但它们将人与人之间的沟通模式与“系统”之间的边界联系在一起,“系统”被认为是“人们工作”或“担心”不一定是“被部署的东西”的东西。

要使这一点成为有效的部署端理由(即,“不同的团队”意味着“不同的服务”),协调工作和由于共同关注而将随着拆分而消失的额外认知负荷必须足够高,以弥补拆分它们的成本。如果在测试、部署、可观察性和支持的后期阶段存在分歧,通常会发生这种情况。这些问题中的大多数都被提供自助式操作的横向平台团队吸收,或者落入支持轮值中,后者通常是已经在处理共享操作问题的人员的子集。就像所有与人类打交道的东西一样,加盐吧。

不同的语言--只要不同的语言能够令人满意地编译到相同的运行时(比如Java和Scala),用它们编写的不同模块就可以成为同一服务的一部分。同样,对于图书馆,我们一直都在这样做。

与上一节相反,上面的标准更符合我们人类在使用我们的系统时所遇到的现实,以及由此产生的紧张局势。再次牢记项目部署的间接性,我们认为没有什么理由拆分可部署的项目。相反,这些是模块、项目、它们的依赖项以及它们组合成可部署的方式的不同排列的标准。

受到项目端拆分的单一设置将导致存储库、模块及其依赖项被分解成一种安排,这种安排在由人操作时会产生最小的紧张,而不会在部署端造成不必要的复杂性。

对于本文来说,将项目模型作为一个整体来考虑就足够了。我希望将存储库、项目、模块和依赖项的理想安排的细节留给另一个帖子。

状态-如果您没有状态,只有纯逻辑,那么您可能有一个库,而不是服务。如果您有状态,但它不会随着用户请求而更改,则您拥有的是配置,无论是全局的还是特定于环境的。如果对配置的更改不是时间敏感的,则它可以是部署的参数,当更改时会触发该参数。如果它们是时间敏感型的,您可能需要持续为它们提供服务的配置服务,很少是定制的配置服务。如果您确实有可以随请求和跨请求更改的全局状态,则您可能拥有一个服务。

Cookie切割器-拥有一个项目模板来帮助创建新的微服务确实很有帮助。也许太有帮助了。

API树-查看调用图,整齐的树中,顶级服务被许多人调用,而最底层的服务只由它们的父级调用,特别是如果它们被某些业务概念分割的话,这是一种气味,表明可部署文件是根据其角色的浪漫化视图划分的,而不是与其计算现实相关的硬性标准。

这些是最容易连接起来的。通常,他们的数据已经与稳定的标识符相关,因此数据迁移是最小的、容易的或没有必要的。

作为一个发人深省的例子,这里有一些模式说明了在构建DAG中具有一定的灵活性,我们可以如何以适合人类和机器的方式来组织我们的系统。我省略了在生产过程中发生的测试和升级,也许是在另一个岗位上。

策略插件模式-构建支持策略模式的流程,其中必须在运行时选择算法实例。

假设我们有产品a、b、c和帐单。在运行时,帐单从其他每一个获取事件。它有自己的计费周期和批处理风格的、以吞吐量为导向的工作负载。其他的更多是事务性的、始终在线的和面向延迟的。帐单一般知道如何开具发票,但不知道每种产品的所有细节。其他的每个人都只知道他们的产品的全部情况。

基于编译时分别来自其中每个项目的逻辑(即,对于定义开票逻辑的工件,帐单在编译时依赖于其他项目),它可以通过在运行时调用正确的逻辑来履行其职责。

我不想在这里对任何人指手画脚,但是我可以看到这个场景正在面向微服务的实践中实现,其中任何一个都有:

在特定于产品的发票逻辑点进行更多服务调用,可能会在批处理作业的执行过程中破坏其他服务并创建部署耦合,或者由于循环而强制进行另一次拆分;或者。

服务器提供的客户端库-不完全是关于拆分-联接标准,但很好地演示了非线性构建流。与其让服务的每个调用者实现其接口(子集),使其保持最新,并在测试中保留其行为,不如考虑将每个服务作为库提供:接口、客户端实现和测试存根。

契约图面变为编译时检查,连接协议由被调用服务的团队控制,客户端实现不存在随时间漂移或累积的重复,我们得到的存根实际上模拟了服务的行为。兰德·戴维斯(Rand Davis)一直在鼓吹(文章,演示文稿)。

我们。

.