3.4 微服务的构建
3.4.1 微服务构建的原则
在构建微服务之前,首先需要知道构建微服务的原则,其主要描述了如何构建一个微服务及为什么这样做。这些问题可以帮助相关人员在构建系统时做出各种决定,有助于创建出一系列可以很好地进行协同工作的自治的小服务。这些原则包括围绕业务概念建模、接受自动化文化、隐藏内部实现细节、去中心化、可独立部署、隔离失败。微服务的原则如图3-20所示。
图3-20 微服务的原则
1. 围绕业务概念建模
经验表明,围绕业务的限界上下文定义的接口,比围绕技术概念定义的接口更加稳定。针对系统如何工作这个领域进行建模,不仅有助于形成更稳定的接口,也能确保相关人员能够更好地反映业务流程的变化。
2. 接受自动化文化
微服务引入了很多复杂性,其中的关键部分是不得不管理大量的服务。解决这个问题的一个关键方法是,接受自动化文化。前期花费一定的成本构建支持微服务的工具是很有意义的。自动化测试必不可少,因为确保大量的服务能正常工作是一个相当复杂的过程。调用一个统一的命令行,以相同的方式把系统部署到各环境中是一个很有用的实践,这也是采用持续交付(持续交付是一种软件工程手法,让软件产品的产出过程在一个短周期内完成,以保证软件可以稳定、持续地保持随时可以释出的状态)对每次提交后的产品质量进行快速反馈的一个关键部分。需要考虑使用环境定义来帮助明确不同环境间的差异,但同时要保持使用统一的方式进行部署的能力。考虑创建自定义镜像(针对有效运行云服务器的用户,通过已创建的自定义镜像,帮助用户一次性开通多台已拷贝相同操作系统及环境数据等的云服务器,以满足用户弹性扩容的业务需求)来加快部署。同时,创建全自动化的不可变服务器,可以更容易定位系统本身的问题。
3. 隐藏内部实现细节
为了使一个服务独立于其他服务、最大化独自演化的能力,隐藏实现细节至关重要。限界上下文建模在这方面可以提供帮助,因为其有助于关注哪些模型应该共享、哪些应该隐藏。服务还应该隐藏它们的数据库,以避免陷入数据库耦合,这在传统的面向服务的架构中也是最常见的一种耦合类型。使用数据泵(Data Pump)或事件数据泵(Event Data Pump)将跨多个服务的数据整合到一起,以实现报表的功能。在可能的情况下,尽量选择与技术无关的API,这能让开发人员自由地选择使用不同的技术栈。
4. 去中心化
为了最大化微服务带来的自治性,需要持续寻找机会,给拥有服务的团队委派决策和控制权。在这个过程的初期,只要有可能,就尝试使用资源自助服务,允许人们按需部署软件,使开发和测试尽可能简单,并且避免让独立的团队来做这些事。在微服务构建的过程中,确保团队保持对服务的所有权是重要的一步,在理想情况下,甚至可以让团队自己决定什么时候让那些更改上线。使用内部开源模式,确保人员可以更改其他团队拥有的服务,不过需要注意的是,实现这种模式需要很多的工作量。让团队与组织保持一致,从而使康威定律发挥作用,并帮助正在构建面向业务服务的团队,让他们成为其构建的业务领域的专家。一些全局的引导是必要的,尝试使用共同治理模型,使团队的每个成员共同对系统技术愿景的演化负责。像企业服务总线或服务编配系统这样的方案,会导致业务逻辑的中心化,应该避免使用它们。使用协同来代替编排或哑中间件,使用智能端点(Smart Endpoint)确保相关的逻辑和数据,从而在服务限界内保持服务的内聚性。
5. 可独立部署
要确保微服务的独立部署,甚至当需要做不兼容更改时,设计人员也应该同时提供新旧两个版本,允许消费者慢慢迁移到新版本中。这有助于加快新功能的发布。拥有这些微服务的团队,也能够越来越具有自治性,因为他们不需要在部署过程中不断地做编配。当使用基于RPC的集成时,微服务的独立部署避免了使用Java RMI提供的桩代码。RMI目前使用Java远程消息交换协议JRMP进行通信。由于JRMP是专为Java对象制定的,因此,RMI对于用非Java语言开发的应用系统的支持不足,不能与用非Java语言编写的对象进行通信,即只支持客户端和服务器端都是Java程序代码的远程调用。而在微服务中,需要能够避免这种行为的发生、紧密绑定客户端/服务器的技术。通过采用单服务单主机模式,可以减少部署一个服务引发的副作用,如影响另一个完全不相干的服务。考虑使用蓝/绿部署或金丝雀部署技术来区分部署和发布,降低发布出错的风险。使用消费者驱动的契约测试,在破坏性的更改发生前捕获它们。值得注意的是,相关人员可以更改单个服务,并将其部署到生产环境中,无须联动地部署其他任何服务,这是一种常态,而不是例外。消费者应该自己决定何时更新,从而适应他们。
6. 隔离失败
微服务架构能比单体架构更具弹性,前提是设计人员及相关人员了解系统的故障模式,并做出相应的计划。如果设计人员不考虑调用下游可能会失败的事实,系统会遭受灾难性的级联故障,系统也会比以前更加脆弱。当使用网络调用时,不要像使用本地调用那样处理远程调用,因为这样会隐藏不同的故障模式。所以要确保使用的客户端库没有对远程调用进行过度的抽象。如果系统只有一部分行为不正常,要了解其对用户的影响。知道网络分区可能意味着什么,以及在特定情况下牺牲可用性或一致性是否是正确的决定。