历史总会重演。一切刚过去的,又会被重新提起。开源项目Codename One的联合创始人Shai,曾是Sun Microsystems开源LWUIT项目的共同作者,参与了无数开源项目。作为最早一批Java开发者,最近感慨道:单体,又回来了!
Shai说道:我已经在这个圈子里很久时间了,看到了一次次被抛弃、被重新发现的想法,超越“时髦词汇”,并凯旋而归。
他进一步举例,“近年来,SQL也挣扎过后,死而复生。我们再次热爱关系数据库。我认为单体架构将再次迎来奇幻之旅。微服务和无服务器是云供应商推动的趋势,目的当然是在向我们兜售更多的云计算资源。然而对于大多数用例来说,微服务在财务上意义不大。是的,供应商当然也可以降低成本。但当他们扩大规模时,他们会以股息来覆盖掉成本。单是可观测性成本的增加,就让‘大型云’供应商的腰包鼓起来了!”
我最近领导了一个会议小组,讨论了微服务与单体服务的主题。组内认为,单块的规模不如微服务。这对于亚马逊、eBay等所取代的那些庞然大物来说可能是正确的。这些确实是巨大的代码库,其中的每一次修改都是痛苦的,而且它们的扩展都是具有挑战性的。但这不是一个公平的比较。较新的方法通常优于旧的方法。但如果我们用更新的工具构建一个整体,我们会得到更好的可扩展性吗?它的局限性是什么?现代的单体(也称巨石)到底该是什么样子?
Spring Modulith是一个模块化的单体结构,可以让我们使用动态隔离件构建单体结构。通过这种方法,我们可以分离测试、开发、文档和依赖项。这有助于微服务开发的独立方面,而所涉及的开销很少。它消除了远程调用和功能复制(存储、身份验证等)的开销。
Spring Modulith不是基于Java平台模块化(Jigsaw)。他们在测试期间和运行时强制分离,这是一个常规的Spring Boot项目。它有一些额外的运行时功能,可以实现模块化的可观测性,但它主要是“最佳实践”的执行者。这种分离的价值超出了我们通常使用微服务的价值,但也有一些权衡。
举个例子,传统的Spring monolith将采用分层架构,其包如下:
com.debugagent.myapp
com.debugagent.myapp.services
com.debugagent.myapp.db
com.debugagent.myapp.rest
这很有价值,因为它可以帮助我们避免层之间的依赖关系;例如,DB层不应依赖于服务层。我们可以使用这样的模块,并有效地将依赖关系图推向一个方向:向下。但随着我们的成长,这没有多大意义。每一层都将充满业务逻辑类和数据库复杂性。
有了Modulith,我们的架构看起来更像这样:
com.debugagent.myapp.customers
com.debugagent.myapp.customers.services
com.debugagent.myapp.customers.db
com.debugagent.myapp.customers.rest
com.debugagent.myapp.invoicing
com.debugagent.myapp.invoicing.services
com.debugagent.myapp.invoicing.db
com.debugagent.myapp.invoicing.rest
com.debugagent.myapp.hr
com.debugagent.myapp.hr.services
com.debugagent.myapp.hr.db
com.debugagent.myapp.hr.rest
这看起来非常接近一个合适的微服务架构。我们根据业务逻辑分离了所有部分。在这里,可以更好地控制交叉依赖关系,团队可以专注于自己的孤立区域,而不必互相踩脚。这是微服务的价值之一,且没有开销。
我们可以使用注释进一步深入地和声明性地实现分离。我们可以定义哪个模块使用哪个并强制单向依赖关系,因此人力资源模块将与发票无关。客户模块也不会。我们可以在客户和发票之间建立单向关系,并使用事件进行反馈。Modulith中的事件是简单、快速和事务性的。它们消除了模块之间的依赖关系,无需麻烦。这可以用微服务实现,但很难实现。比如,开票需要向不同的模块公开接口。如何防止客户使用该界面?
有了模块,我们就可以做到。对用户可以更改代码并提供访问权限,但这需要经过代码审查,这会带来自己的问题。请注意,对于模块,我们仍然可以依赖常见的微服务,如功能标志、消息传递系统等。您可以在文档和Nicolas Fränkel的博客中阅读有关Spring Modulith的更多信息。
模块系统中的每个依赖项都被映射并记录在代码中。Spring实现包括使用方便的最新图表自动记录所有内容的能力。你可能会认为,依赖性是Terraform的原因。对于这样的“高级”设计来说,这是正确的地方吗?
对于Modulith部署,像Terraform这样的基础设施即代码(IaC)解决方案仍然存在,但它们会简单得多。问题是责任的划分。正如下图所展示,微服务并没有消除整体结构的复杂性。我们只把“难啃的骨头”踢给了DevOps团队。更糟糕的是,我们没有给他们正确的工具来理解这种复杂性,所以他们不得不从外部管理。
我们可以使用标准Java平台模块(Jigsaw)来构建Spring Boot应用程序。这样做的好处是可以分解应用程序和标准Java语法,但有时可能会很尴尬。当使用外部库或将一些工作拆分为通用工具时,可能会更有效。
另一个选项是Maven中的模块系统。这个系统允许我们将构建分解为多个单独的项目。这是一个非常方便的过程,可以让我们省去大量项目的麻烦。每个项目都是独立的,易于使用。它可以使用自己的构建过程。然后,当我们构建主项目时,这些全部都变成了一个单体。在某种程度上,这才是我们真正想要的。
可以使用大多数微服务扩展工具来扩展我们的单体们。许多与扩展和集群相关的开发都是在单体架构的情况下进行的。这是一个更简单的过程,因为只有一个移动部分:应用程序。我们复制其他实例并观察它们。没有哪项服务是失败的。我们有细粒度的性能工具,所有的功能都可以作为一个统一的版本。
我认为扩展单体为微服务比直接构建微服务更简单——
我们可以使用分析工具,并获得瓶颈的合理近似值。
我们的团队可以轻松地(并且经济实惠地)设置运行测试的登台环境。
我们拥有整个系统及其依赖关系的单一视图。
我们可以单独测试单个模块并验证性能假设。
跟踪和可观测性工具非常棒。但它们也会影响生产,有时还会产生噪音。当我们试图解决伸缩瓶颈或性能问题时,这些工具可能会让设计者踩一些坑。
我们可以将Kubernetes与monolits一起使用,就像将其与微服务一起使用一样有效。镜像尺寸会更大(如果我们使用GraalVM这样的工具,则可能不会太大)。有了这一点,我们可以跨区域复制monolith ,并提供与微服务相同的故障转移行为。相当多的开发人员将monolics部署到Lambdas。笔者不太喜欢这种方法,因为非常昂贵。
但仍有一点是巨大的障碍:数据库。由于微服务固有地具有多个独立的数据库,因此它们实现了巨大的规模。单体架构通常与单个数据存储一起工作。这通常是应用程序的真正瓶颈。有多种方法可以扩展现代数据库。集群和分布式缓存是强大的工具,可以让我们达到在微服务架构中很难达到的性能水平。
在一个单体结构中,也并不需要单个数据库。例如:在使用Redis进行缓存时,选择使用SQL数据库也是很常见的事情。但我们也可以为时间序列或空间数据使用单独的数据库。我们也可以使用单独的数据库来提高性能,尽管根据笔者经验,这种情况从未发生过。将数据保存在同一数据库中的好处是巨大的。
在设计应用时,我们目前更多是面临“二选一”的架构选择:单体和微服务。它们二者通常被视为相反的方法。
在小型系统演进过程中,有这样一个不争的事实:单体应用程序往往会随着时间的推移而在架构上降级,即使在其生命周期开始阶段就定义其为架构。随着时间的推移,各种架构的禁止事项会不知不觉地进入项目,久而久之,系统变得更难改变,进化性受到影响。
另一方面,微服务提供了更强的分离手段,但同时也带来了许多复杂性,因为即使对于小型应用程序,团队也必须应对分布式系统的挑战。
单体回归,也是具体的有条件的回归。我们看到,趋势的改变,代表着某段时期具体任务或者目标正在变化。出于目标的变化,我们对于微服务和单体架构的二选一的选择问题,也不能再教条式的看待。
事物往往都在螺旋式的演进,对于架构而言,亦如是。