网上已经有非常多从单体服务转移到微服务讨论,但大家有没有反思过,这种做法真的是最佳选择吗? 虽然维护一个凌乱的单体应用有很多缺点,但有一个令人信服的替代方案经常被忽视:在服务内部实现良好的模块化。 下面我们来探讨这种替代方案,并展示它与微服务的关系。
为了模块化的微服务
“通过微服务,我们终于可以让团队独立工作”,或者“我们的单体服务太复杂,这会降低我们的开发速度”。这些说法是让开发团队走向微服务之路的众多原因之一。服务化另外的原因还包括可扩展性和弹性部署。工程师大多追求系统设计和开发的模块化方法,软件开发中的模块化可以归结为三个指导原则:
强封装:隐藏组件内部的实现细节,让不同部件之间的耦合度较低。团队可以在系统的解耦的部分单独工作。
明确定义的接口:我们无法隐藏所有内容(否则系统将无法做任何有意义的事情),因此组件之间必须有定义良好且稳定的 API。组件可以被符合接口规范的任何实现所替代.
显式依赖:具有模块化系统意味着不同的组件必须一起工作,需要有一个表达他们的关系的好方法。
这些原则大部分情况是可以通过微服务来实现,一个服务只要有公开明确定义的接口(通常为 REST API),就可以以任何方式实现微服务。实施细节对服务来说是内部问题,可以在没有系统范围的影响或协调的情况下进行更改。微服务器之间的依赖关系在开发时通常不是很明确,从而导致可能的运行时服务编排失败。
因此,微服务实现了重要的模块化原则,带来实质效益:
尽管如此,从一个单一的(虽然轻微肥胖的)应用程序进入到一个分布式的微服务系统,这带来了巨大管理复杂性。突然间,您需要持续部署许多不同的(可能是容器化的)服务。新的关注点开始出现:服务发现、分布式日志记录、跟踪等。你现在更容易出分布式服务各种错误。接口和配置管理的版本控制也成为新的需要关注的地方,问题的列表还会继续增长。(高可用架构小编注:以学习“新技术”为己任的工程师未必会这么想)
微服务之间连接的复杂性在于连接了所有微服务的业务逻辑,你不能简单粗暴分开你的单体式服务来实现微服务。虽然"意面代码"的单体服务是有问题的,但是引入网络边界放在新的服务之间,会使这些令人纠缠的问题升级到彻头彻尾的痛苦。
模块化的替代方案
这是否意味着我们要么必须维护混乱的单体服务,要么就需要淹没在微观服务的复杂性之间?其实,模块化也可以通过其他方式实现,我们只要在开发过程中有效地制定和实现边界,也可以通过创建一个结构良好的单体应用来实现这一点。而且如果只考虑模块化,我们可以从已有的编程语言和开发工具比如 IDE 中获得很多开发的便利,以实现模块化的原则。
在 Java 中,有几个可以帮助应用模块化的系统。 OSGi 是最知名的,随着 Java 9 的发布,本地模块系统被添加到 Java 平台本身。模块现在是 Java 语言及平台的一部分,作为一级结构,Java 模块可以表达对其他模块的依赖关系,并在强制封装实现类的同时公开导出接口。甚至 Java 平台本身(一个巨大的代码库)已经使用新的 Java 模块系统进行了模块化。在即将出版的 Java 9 Modularity 一书中,您可以了解使用 Java 9 来实现模块化开发的更多信息。
其他语言提供类似的机制。例如,JavaScript 获得了 ES2015 的模块系统。在此之前,Node.js 已经提供了一个用于 JavaScript 后端的非标准模块系统。然而,作为一种动态语言,JavaScript 对于实现模块之间的接口(类型)和封装有较弱的支持。您可以考虑在 JavaScript之上使用 TypeScript 来再次获得这一优势。
Microsoft 的 .Net 框架确实具有像 Java 这样的强类型,但是它没有类似 Java 即将到来的模块系统,也没有程序集之间的显式依赖。尽管如此,通过使用 .Net Core 中标准化的 Inversion-of-Control 模式和通过创建逻辑相关的程序集,可以实现良好的模块化架构。即使 C++ 也正在考虑在将来的版本中添加模块系统。
综上所述,许多语言正对模块化表示重视,这本身就是一个惊人的发展。
当你使用开发平台的模块化功能时,就可以实现微服务相同的模块化优势。基本上,模块系统越好,在开发过程中获得的帮助越多。不同的团队可以在不同的部分工作,只要明确定义团队之间交互的接口。
在部署时,所有模块组合在一个单独的单元中部署,这样可以防止微服务开发和管理带来的巨大复杂性和成本。当然,这也意味着您不能在不同的技术栈上构建每个模块。但是贵公司真的打算好使用异构技术栈来搭建同一个系统吗?
设计模块的哲学
设计模块需要与设计微服务一样保证设计的严谨性。模块应该对域的单个有边界的上下文建模。选择微服务边界是一个具有重要架构意义的决策,选择错误时会产生昂贵的后果。模块的边界更易于更改,它的重构通常由类型系统和编译器支持。重新划分微服务需要涉及到大量的开发人员之间的通信,以便不会摧毁线上系统。老实说,你需要经历多少次尝试才能得到微服务合适的界限?
在许多方面,静态类型语言中,模块为明确定义的接口提供了更好的结构支持。通过另一个模块公开的类型化接口调用方法比在另一个微服务器上调用 REST 接口更加鲁棒。REST + JSON 是无处不在的,但是在没有(编译器检查)模式的情况下,它不是良好类型互操作性的标志。补充一点,通过网络进行(反)序列化仍然不是没有成本的。此外,许多模块系统允许您表达对其他模块的依赖。当试图违反这些依赖关系时,模块系统将不允许如此。微服务器之间的依赖关系只在运行时实现,导致难以调试系统。
模块也是代码所有权的自然单元。团队可以负责系统中的一个或多个模块。与其他团队分享的唯一的东西就是模块的公共 API。在运行时,相比微服务来讲,模块之间的隔离较少。毕竟,一切仍然运行在相同的进程中。
没有任何理由,在单体应用中,模块不能像微服务器一样拥有它的数据。模块化应用程序之间通过定义良好的接口或模块之间的消息来共享数据,而不是通过共享数据存储,与微服务的巨大差异在于一切都在同一个进程中。最终的一致性问题不应低估,使用模块,最终的一致性可以是一个策略的选择。或者,您只需“逻辑”地将数据分开存储在同一数据存储区中,并且仍然使用跨域事务。对于微服务,则没有选择:最终的一致性是必须的,您需要适应这点。
微服务什么时候适合您的组织?
那么什么时候应该转向微服务器?上面描述的都是如何通过模块化来解决复杂性。对于这一点,微服务和模块化应用都可以做到。但到各有不同的挑战。
当您的组织处于 Google 或 Netflix 规模时,拥抱微服务是完全有道理的。您有能力建立自己的平台和工具包,而且工程师的数量也不允许任何单体服务的可能。但是大多数组织都没有在这个规模下运作。即使你认为你的组织有一天将成为一个十亿美元的独角兽,开始使用模块化也不会有太多的危害。
微服务另一个好的理由是,不同的服务可以使用不同的技术栈。但是前提是你必须拥有足够规模来吸引开发人员跨越这些不同的栈,并保持这些平台的运行。
微服务还可以独立部署系统的不同部分,这在大多数模块化系统中很难(甚至是不可能)。隔离部署增加了系统的弹性和容错能力。此外,对于每个微服务器,伸缩能力可以不同。可以部署不同的微服务来匹配硬件。模块化整体式服务也可以水平缩放,但您需要将所有模块放在一起。尽管在实践中,你可以用这种方法得到很多好处,但是这可能并不是最好的方式。
结论
跟所有架构思路相似,最好的选择是找到一个折衷的位置。两种方法都有适合的地方,需要根据环境,组织和应用程序本身来做选择。
在大部分情况何不考虑从模块化单体应用开始?您随时可以选择转移至微服务。如果您已经有明确的模块界限,您也不必手术肢解您的单体应用,它甚至不是一个排他选择:您也可以在微服务内部使用模块。因此也许问题就变成了,为什么微服务必须“微”?
即使从模块化应用程序中脱离出来,服务也不必变“微”才可维护。在服务中应用模块化的原则使他们能够超越通常微服务那种复杂性。在这里有模块化和微服务共存的地方。通过减少您的架构中的服务数量,可以实现实际的成本节约。模块可以帮助构建和扩展服务,就像它们可以帮助构建单个整体式应用程序一样。
如果您希望获得模块化好处,请确保不要欺骗自己进入仅限微服务的心态。探索您最喜爱的技术堆栈中模块化功能或框架,就可以获得模块化设计的支持,而不必仅仅依靠约定来避免“意面一样的代码”。仔细考虑是否需要引入微服务带来的复杂性,有时你需要(但往往可以)找到更好的方法。
Sander Mak 是一名荷兰 Luminis 的研究员。 在 Luminis,他开发了系列模块化和可扩展的软件,大部分是基于 JVM 的。 他是即将出版的 O'Reilly Java 9 Modularity 的作者,他喜欢通过博客
推荐阅读
本文为高可用架构翻译,转载请注明出处,技术原创及架构实践文章,欢迎通过公众号菜单「联系我们」进行投稿。
高可用架构
改变互联网的构建方式
长按二维码 关注「高可用架构」公众号