历史总会重演。一切刚过去的,又会被重新提起。开源项目Codename One的联合创始人Shai,曾是Sun Microsystems开源LWUIT项目的共同作者,参与了无数开源项目。作为最早一批Java开发者,最近感慨道:单体,又回来了!
Shai说道:我已经在这个圈子里很久时间了,看到了一次次被抛弃、被重新发现的想法,超越“时髦词汇”,并凯旋而归。
他进一步举例,“近年来,SQL也挣扎过后,死而复生。我们再次热爱关系数据库。我认为单体架构将再次迎来奇幻之旅。微服务和无服务器是云供应商推动的趋势,目的当然是在向我们兜售更多的云计算资源。然而对于大多数用例来说,微服务在财务上意义不大。是的,供应商当然也可以降低成本。但当他们扩大规模时,他们会以股息来覆盖掉成本。单是可观测性成本的增加,就让‘大型云’供应商的腰包鼓起来了!”
作为从业近30年的资深技术大神,为何做此感叹?本文通过一场“利用模块降低架构成本”的探讨,帮助大家梳理现在的架构设计难题,希望对诸君有所启发。
1、问题背景
我最近领导了一个会议小组,讨论了微服务与单体服务的主题。组内认为,单块的规模不如微服务。这对于亚马逊、eBay等所取代的那些庞然大物来说可能是正确的。这些确实是巨大的代码库,其中的每一次修改都是痛苦的,而且它们的扩展都是具有挑战性的。但这不是一个公平的比较。较新的方法通常优于旧的方法。但如果我们用更新的工具构建一个整体,我们会得到更好的可扩展性吗?它的局限性是什么?现代的单体(也称巨石)到底该是什么样子?
2、单体回归范例:Modulith
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团队。更糟糕的是,我们没有给他们正确的工具来理解这种复杂性,所以他们不得不从外部管理。
这就是为什么我们行业的基础设施成本在上升,而传统行业的基础设施价格却在下降。当DevOps团队遇到问题时,他们会投入资源。这显然不是正确的做法。
3、其他模块
我们可以使用标准Java平台模块(Jigsaw)来构建Spring Boot应用程序。这样做的好处是可以分解应用程序和标准Java语法,但有时可能会很尴尬。当使用外部库或将一些工作拆分为通用工具时,可能会更有效。
另一个选项是Maven中的模块系统。这个系统允许我们将构建分解为多个单独的项目。这是一个非常方便的过程,可以让我们省去大量项目的麻烦。每个项目都是独立的,易于使用。它可以使用自己的构建过程。然后,当我们构建主项目时,这些全部都变成了一个单体。在某种程度上,这才是我们真正想要的。
4、单体架构:扩展,有解吗
可以使用大多数微服务扩展工具来扩展我们的单体们。许多与扩展和集群相关的开发都是在单体架构的情况下进行的。这是一个更简单的过程,因为只有一个移动部分:应用程序。我们复制其他实例并观察它们。没有哪项服务是失败的。我们有细粒度的性能工具,所有的功能都可以作为一个统一的版本。
我认为扩展单体为微服务比直接构建微服务更简单——
- 我们可以使用分析工具,并获得瓶颈的合理近似值。
- 我们的团队可以轻松地(并且经济实惠地)设置运行测试的登台环境。
- 我们拥有整个系统及其依赖关系的单一视图。
- 我们可以单独测试单个模块并验证性能假设。
跟踪和可观测性工具非常棒。但它们也会影响生产,有时还会产生噪音。当我们试图解决伸缩瓶颈或性能问题时,这些工具可能会让设计者踩一些坑。
我们可以将Kubernetes与monolits一起使用,就像将其与微服务一起使用一样有效。镜像尺寸会更大(如果我们使用GraalVM这样的工具,则可能不会太大)。有了这一点,我们可以跨区域复制monolith ,并提供与微服务相同的故障转移行为。相当多的开发人员将monolics部署到Lambdas。笔者不太喜欢这种方法,因为非常昂贵。
5、单体的瓶颈问题:有解
但仍有一点是巨大的障碍:数据库。由于微服务固有地具有多个独立的数据库,因此它们实现了巨大的规模。单体架构通常与单个数据存储一起工作。这通常是应用程序的真正瓶颈。有多种方法可以扩展现代数据库。集群和分布式缓存是强大的工具,可以让我们达到在微服务架构中很难达到的性能水平。
在一个单体结构中,也并不需要单个数据库。例如:在使用Redis进行缓存时,选择使用SQL数据库也是很常见的事情。但我们也可以为时间序列或空间数据使用单独的数据库。我们也可以使用单独的数据库来提高性能,尽管根据笔者经验,这种情况从未发生过。将数据保存在同一数据库中的好处是巨大的。
6、回归单体的好处
事实上,这样做有一个惊人的好处,我们可以在不依赖“最终一致性”的情况下完成交易。当我们尝试调试和复制分布式系统时,可能会遇到一个很难在本地复制的过渡状态,甚至很难通过查看可观测性数据来完全理解。
原始性能消除了大量网络开销。通过适当调整的二级缓存,我们可以进一步删除80-90%的读IO。在微服务中,要实现这一点要困难得多,而且可能不会删除网络调用的开销。
正如我之前提到的,应用程序的复杂性在微服务架构中不会消失。我们只是把它搬到了另一个地方。所以从这个层面讲,微服务并不算真正的进步,因为在此过程中平白添加了许多移动部件,增加了整体复杂性。因此,回归更智能、更简单的统一架构更有意义。
7、再看微服务的卖点
编程语言的选择是微服务亲和力的首要指标之一。微服务的兴起与Python和JavaScript的兴起相关。这两种语言非常适合小型应用程序,对于较大型的应用就不太适用了。
Kubernetes使得扩展此类部署相对容易,因此为已经增长的趋势增添了动力。微服务也有一些相对快速的升降能力。这可以以更细粒度的方式控制成本。在这方面,微服务被出售给组织,作为降低成本的一种方式。
这并非完全没有优点。如果以前的服务器部署需要强大(昂贵)的服务器,那么这一论点可能有一定道理。这可能适用于极端使用的情况,比如:突然面临非常高的负载,但随后没有堵塞。在这些情况下,可以从托管的Kubernetes提供商动态(廉价)获取资源。
微服务的主要卖点之一是组织调度方面。这使得各个敏捷团队能够在不完全了解“大局”的情况下解决小问题。问题也在于此,这就会创造一种“单干”文化,让每个团队都“自己做自己的事情”。在缩减规模的过程中,尤其是在代码“腐烂”的情况下,问题更甚。系统可能仍能工作数年,但实际上无法维护。
8、互联网建立在单体之上
为什么要离开呢?
笔者组内中的一个共识是,我们应该始终从单体开始。它更容易构建,如果我们选择使用微服务,我们可以稍后将单体分解。
提及具体某个软件相关的复杂性,我们讨论单个模块而不是单个应用程序要更有意义些。二者在资源使用和财务浪费上的差异是巨大的。在这个追求“降本”的时代,为什么人们还要不知变通地默认构建微服务,而不是动态的模块化单体架构呢?
我们可以从这两大“架构阵营”学到很多东西。诚然,微服务为亚马逊创造了奇迹。但公平地说,他们的云成本已包含在这个奇迹之中。所以,一位的搞微服务教条肯定是有问题的。
另一方面,互联网是建立在单体之上的。它们中的大多数都不是模块化的。两者都有普遍适用的技术。因此,笔者看来,正确的选择是构建一个模块化的单体结构,先搭建好合适的身份验证基础设施,如果我们想在未来转向微服务,我们可以利用这些基础设施来进行解构拆分。
9、后记
在设计应用时,我们目前更多是面临“二选一”的架构选择:单体和微服务。它们二者通常被视为相反的方法。
在小型系统演进过程中,有这样一个不争的事实:单体应用程序往往会随着时间的推移而在架构上降级,即使在其生命周期开始阶段就定义其为架构。随着时间的推移,各种架构的禁止事项会不知不觉地进入项目,久而久之,系统变得更难改变,进化性受到影响。
另一方面,微服务提供了更强的分离手段,但同时也带来了许多复杂性,因为即使对于小型应用程序,团队也必须应对分布式系统的挑战。
单体回归,也是具体的有条件的回归。我们看到,趋势的改变,代表着某段时期具体任务或者目标正在变化。出于目标的变化,我们对于微服务和单体架构的二选一的选择问题,也不能再教条式的看待。
事物往往都在螺旋式的演进,对于架构而言,亦如是。