微服务最常被看作是一种架构方法,一种单体架构的替代方案。为了更清楚地区分微服务架构,并帮助你更好地理解微服务是否值得考虑,我们需要讨论一下 单体 (monolith)到底是指什么。
本书中提到的单体,主要是指部署单元。当系统中的所有功能必须一起部署时,我们可以视它为一个单体。符合这个定义的架构有很多种,但是本书仅讨论常见单体,比如单进程单体、模块化单体和分布式单体。
论及单体,最常见的一个例子是,一个系统中的所有代码都部署在 单一进程 中,如图1-6所示。出于健壮性或可扩展性的考虑,该进程可能有多个实例存在,但从根本上来说,一个进程包含了整个应用程序。实际上,这类单进程系统本身可以是简单的分布式系统,因为通常它们要么在数据库中存取数据,要么向网页端或移动端提供信息。
图1-6:在单进程单体内,所有代码被打包到一个进程中
虽然这符合大多数人对典型单体的理解,但我遇到的大多数系统都比这种单体复杂。比如两个或多个彼此紧密的耦合单体,其中还可能包含供应商提供的软件。
对于许多组织来说,典型的单进程单体部署是明智的选择。Ruby on Rails 的发明者 David Heinemeier Hansson 证明了这种架构对于小型组织的意义。 然而,随着组织的不断发展,单体也在随之发展,从而逐步演变为模块化单体。
作为单进程单体的一类, 模块化单体 (modular monolith)是一种变体,其中单个进程由一个或多个单独模块组成。每个模块都可以独立工作,但仍然需要将所有模块组合起来一起部署,如图1-7所示。将软件分解成模块的方法并不是一个新鲜事物。模块化软件是结构化程序设计的制品,起源于20世纪70年代,甚至更早。尽管如此,在我看来这种方法还没有被广泛和正确地运用。
图1-7:在模块化单体内,进程内的代码被分割成多个模块
对于许多组织来说,模块化单体架构是一个很好的选择。明确模块边界可以让工作高度并行,同时通过简单的部署拓扑结构可以避免分布式微服务架构所带来的挑战。Shopify 便是一个非常成功的案例,它运用这样的方式来替代微服务的解耦。
采用模块化单体架构的挑战之一是,数据库往往缺乏在代码层面的解耦;如果你想将来拆分单体架构,则会面临更大的挑战。我曾看到一些团队在进一步推动模块化单体架构时,尝试按拆分模块的方式拆分数据库,如图1-8所示。
图1-8:拆分了数据库的模块化单体
在分布式系统中,你甚至不知道哪一台计算机的故障会导致计算机失效。
——Leslie Lamport
分布式单体 (distributed monolith)是一个由多个服务组成的系统,无论出于何种原因,整个系统都必须部署在一起。分布式单体架构很可能非常符合 SOA 的定义,但它往往无法兑现 SOA 的承诺。根据我的经验,分布式单体兼具分布式系统和单进程单体的短处,且没有足够的优势。我在工作中遇到了许多分布式单体,这在很大程度上影响了我对微服务架构的兴趣。
分布式单体所处的环境往往缺乏对信息隐藏和业务功能内聚性等概念的关注。相反,高度耦合的架构会导致更改需跨越服务边界,而那些在服务内部看似无害的更改会破坏系统的其他部分。
当越来越多的人在同一个地方工作,他们会越来越互相妨碍。比如,不同的开发人员想要更改同一段代码,不同的团队想要在不同的时间推出功能(或推迟功能的部署),或者团队成员对谁来负责某项工作或由谁做出决定而感到困惑。大量研究表明所有权混淆会带来挑战。 我将此类问题称为 交付争用 (delivery contention)。
拥有单体并不意味着一定会面临交付争用的挑战,就像采用微服务架构并不意味着永远不会遇到问题一样。但是,微服务架构确实可以提供更契合实际的边界,你可以沿着这些边界在系统中划定所有权边界,从而减少交付争用问题,获得更大的灵活性。
有一些单体,例如单进程单体或模块化单体,也有很多优点。它们采用更简单的部署拓扑,以此避免与分布式系统相关的许多麻烦。这可以使开发人员的工作流程更加简单,系统监控、故障排除和端到端测试等工作也可以大大简化。
单体还可以简化内部的代码复用。如果我们想在分布式系统中复用代码,我们需要决定是复制代码、拆分成库还是将共享功能移到某个服务中,而有了单体,我们的选择要简单得多。很多人喜欢这种简单性——所有的代码都在那里,直接使用即可!
遗憾的是,人们开始觉得需要避免采用单体架构——认为它本质上是有问题的。我遇到过很多人,他们认为单体就是“过时”的代名词,而这才是有问题的。单体架构仍然是一种选择,而且是一种有效的选择。摊开说,在我看来,如果你正在选择架构风格,首选单体架构合情合理。同理,具体到微服务,我们要找的也是使用的理由,而不是不用的借口。
如果我们认为系统性地减少单体的使用应该作为交付软件的可行措施,那么我们可能会陷入陷阱,让自己或用户处于无法正确工作的风险中。