
最佳选择其实是应用模块化系统设计原则,同时避免微服务的操作复杂性。
关于从巨型服务打碎成微服务的说法已经很多。但是这种方法真的是组织的最佳选择吗?的确,维护混乱的整体应用程序有很多缺点。但有一个令人信服的替代方案,常常被忽视:模块化应用程序开发。在本文中,我们将探讨这一选择所带来的内容,并展示它与构建微服务的关系。
模块化微服务
“通过微服务,我们最终可以让团队独立工作”,或者“我们的整体太复杂,使我们慢下来。”这些表达式只是导致开发团队走上微服务路径的许多原因中的一小部分。另一个是对可伸缩性和弹性的需求。开发者共同渴望的是系统设计和开发的模块化方法。软件开发中的模块化可以归结为三个指导原则:
强封装:隐藏组件内部的实现细节,导致不同部件之间的低耦合。团队可以在系统的去耦部分隔离工作。
定义良好的接口:你不能隐藏所有的东西(或者你的系统不会做任何有意义的事情),所以必须在组件之间定义明确和稳定的API。组件可以由符合接口规范的任何实现来替换。
显式依赖:具有模块化系统意味着不同的组件必须协同工作。你最好用一种好的方式来表达和验证他们之间的关系。
这些原则中的许多可以通过微服务实现。微服务可以以任何方式实现,只要它为其他服务公开一个定义良好的接口(通常是REST API)。它的实现细节是服务内部的,并且可以在没有系统范围影响或协调的情况下改变。在开发时,微服务之间的依赖关系通常不十分明确,导致运行时可能出现服务编排失败。我们可以说,最后一个模块化原则可以在大多数微服务架构中使用。
因此,微服务实现了重要的模块化原则,从而带来切实的好处:
团队可以独立工作和规模化。
微服务是小型和集中的,减少复杂性。
服务可以在不影响整体的情况下进行内部更改或替换。
有什么不对的地方吗?一路上,你已经从一个单一的(虽然稍微肥胖)应用到一个分布式的微服务系统。这带来了大量的操作复杂性。突然,你需要不断地部署许多不同的(可能是容器化的)服务。出现新的关注点:服务发现、分布式日志记录、跟踪等。你现在更倾向于分布式计算,接口和配置管理的版本化成为主要关注点,等等等等。
事实证明,微观服务之间的连接有着复杂的逻辑,因为所有的单个微服务的组合业务逻辑都存在。为了到达这里,你不能只顾着砍掉你的巨大系统。而留在整体代码库中的“意大利面条代码”是有问题的,将网络边界置于两者之间,将这些纠缠问题升级为彻头彻尾的痛苦。
模块化替代方案
这是否意味着我们要么沦落到混乱的庞然大物,要么淹没在微服务疯狂的复杂性中?模块化也可以通过其他手段来实现。重要的是我们可以在开发过程中有效地绘制和执行边界。但我们也可以通过建立一个结构良好的整体来实现这一目标。当然,这意味着接受从编程语言和开发工具中获得的任何帮助来执行模块化的原则。
例如,在Java中,有几个可以帮助构建应用程序的模块系统。OSGi是最著名的一个,但是随着Java 9的发布,一个本地模块系统被添加到Java平台本身。模块现在是语言和平台的一部分,是一流的结构。Java模块可以表示依赖于其他模块,并公开导出接口,同时强封装实现类。甚至Java平台本身(一个巨大的代码库)也被使用了新的Java模块系统模块化。
其他语言也提供类似的机制。例如,JavaScript得到了ES2015的模块系统。在此之前,Node.js已经为JavaScript后端提供了一个非标准的模块系统。然而,作为一种动态语言,JavaScript对执行模块间的接口(类型)和封装有较弱的支持。您可以考虑使用JavaScript之上的Type Script来恢复这个优势。微软的.NET Framework确实具有像Java那样的强类型化,但是它在强大的封装性和组件之间的显式依赖性方面没有直接等同于Java即将到来的模块系统。尽管如此,通过使用在.NET中标准化的控制模式和创建逻辑相关的程序集,可以实现良好的模块化架构。甚至C++正在考虑在将来的修订中添加模块系统。许多语言正在获得模块化的增值,这本身就是一个惊人的发展。
当你有意识地使用你的开发平台的模块化特性时,你就可以获得与早期的微服务相同的模块化好处。基本上,模块系统越好,开发过程中的帮助越多。不同的团队可以在不同的部分工作,其中只有定义良好的接口是团队之间的接触点。然而,在部署时,模块在一个部署单元中聚集在一起。通过这种方式,您可以防止与移动服务开发和管理相关的复杂性和成本。确实,这意味着不能在不同的技术堆栈上构建每个模块。但是你的组织真的准备好了吗?
模块设计
创建好的模块需要与创建良好的微服务一样的设计严谨性。模块应该对域的单个有界上下文建模(部分)。选择微服务边界是一个架构重大的决策,当出错时会带来昂贵的后果。模块化应用程序中的模块边界更容易更改。跨模块的重构通常由类型系统和编译器支持。重新绘制微服务边界涉及大量的个人间通信,以便不让问题在运行时爆发。
在很多方面,静态类型语言中的模块为定义良好的接口提供了更好的构造。通过另一个模块公开的类型化接口调用方法比更改另一个微服务上的REST服务更方便。REST + JSON是普遍存在的,但在没有(编译器检查)模式的情况下,它不是良好互操作性的标志。网络传输方面,序列化和反序列化需要耗费资源,更不用说图片的处理了。更重要的是,许多模块系统允许在其他模块上表达依赖关系。当这些依赖关系被违反时,模块系统将会禁止它。微服务之间的依赖性只在运行时实现,导致系统难以调试。
模块也是代码所有权的自然单位。团队可以负责系统中的一个或多个模块。与其他团队共享的唯一东西是他们的模块的公共API。在运行时,与微服务相比,模块之间的隔离度更小。毕竟,一切都在同一过程中运行。
一个巨型服务中的模块没有理由不能像一个好的微服务一样控制它的数据。在模块化应用程序*共中**享,然后通过模块间定义良好的接口或消息交互,而不是通过共享数据存储,并且在同一进程中执行。不应低估最终一致性的问题。通过模块,最终的一致性可以是一种深思熟虑的战略选择。或者,您可以“逻辑地”将数据存储在同一数据存储区中,并暂时使用跨域事务。对于微服务,你没有选择:最终的一致性是给定的,你需要适应。
什么时候您的系统适合使用微服务?
那么,你应该什么时候转向微服务呢?到目前为止,我们主要集中于通过模块化来解决复杂性。因此,微服务和模块化应用都可以。但除了目前所提出的挑战之外,还有一些不同的挑战。
当你的组织处于谷歌或Netflix的规模时,拥抱微服务是完全有意义的。你有能力建立自己的平台和工具包,工程师的数量太大导致不能使用任何整体方法。但是大多数组织都没有这样的规模经营。即使你认为你的组织总有一天会变成十亿美元的独角兽,用模块化的开发不会有太大的伤害。
另一个很好的原因是,不同的服务本质上更适合于不同的技术栈。然后,你必须有足够的规模来吸引这些不同的栈上的人才,并保持这些平台的运行。
微服务还能够独立部署系统的不同部分,这在大多数模块化平台中更难(甚至不可能)。隔离部署增加了系统的弹性和容错性。此外,每个微服务的缩放特性可能是不同的。可以部署不同的微服务来匹配硬件。模块化的整块也可以水平缩放,但是您可以将所有模块扩展到一起。虽然在实践中,你可以通过这种方法得到很好的结果,但这并不总是最好的。
结论
一如既往,最好的选择是找到一个平衡。这两种方法都有优点,哪种方法最好取决于环境、组织和应用本身。为什么不从模块化应用开始呢?你可以选择以后迁移到微服务。然后,不用去敲碎你的巨型服务,因为你已经有了合理的模块边界。它甚至不是唯一的选择:您也可以使用模块来在内部构造微服务。问题是,为什么微服务必须是“微”?
即使你离开了一个单一的模块化应用程序,你的服务也不必是微小的,而是可以维护的。同样,在服务中应用模块化的原则,使他们能够超越通常被认为是微观服务的复杂性。模块和微服务可以并存。实际成本节约可以通过减少体系结构中的服务数量来实现。模块可以帮助构造和缩放服务,就像它们可以帮助构建单个单片应用程序一样。
如果你在追求模块化的好处,那就确保你不要欺骗你自己。探索您最喜爱的技术栈的模块化特征或框架。你会在实施模块化设计中尝到甜头,而不是仅仅依靠约定来避免意大利面条代码。然后,慎重选择是否要接受微服务的复杂性。有时候你不得不这样做,但通常情况下,你可以找到更好的前进方向。