第04课:部署架构松耦合

第04课:部署架构松耦合

概述

当下微服务架构风头正劲,它对系统服务按照业务进行细粒度拆分,每个服务以独立进程的形式启动,以便在架构上提供更好的伸缩性和有效性。

微服务架构使得我们可以轻易对某个服务进行版本升级、扩容,一个服务的崩溃不会影响其他服务正常运行。这些都是微服务架构的优势,但是在获得这些优势的同时,我们需要付出更多的管控成本,因为你部署、维护及监控的应用程序已变为原来的数十倍甚至上百倍。当然,借助现代化的运维工具,这部分工作已不再令人望而却步。

我们不止一次的说,微服务架构的演进更多是由业务进行驱动。若业务过程不能进行合理拆分,那么它的这些优势便不复存在。业务耦合会使它的伸缩性和有效性形同虚设。

抛开微服务架构实施的难度,单从部署架构考虑,微服务可谓将松耦合做到了极致。正所谓:“道可为则,各从其宜”,在具体实践中,受限于客观条件(业务过程还不清晰、团队能力跟不上、运维水平达不到等等),我们很难将系统按照理论模型进行构建。这也使得我们探讨如何进行部署架构松耦合变得有意义。

通过部署架构松耦合的渐进式重构,会在一定程度上提高系统部署的灵活性,使架构在团队可承受的范围内,逐步向理想状态迈进。

几种参考

PHP 内容管理系统

很多人看到这个标题,可能会产生疑惑,但是我一直深信在架构理念上各种系统是可以相互借鉴的,即便是跨语言。

如果你稍微了解当下流行的几款开源 PHP 内容管理系统,如 Joomla,就会发现它们都会提供内置的模块框架。每个模块通过系统可识别的描述文件进行自我描述。系统自动解析描述文件,加载模块,并对外提供服务。

这种模块化框架不仅使系统变得极易扩展(你可以自行开发各种模块以丰富系统功能),也在部署运行期间为系统带来了更好的灵活性。

在系统运行期间,我们可以灵活安装、卸载、升级模块,而不会影响系统中其它模块的运行。

从架构层面进行概括的话,首先系统提供了统一的应用模块运行环境,并定义了模块的发布规范,其次,系统根据定义的规范识别并加载模块,对模块进行生命周期管理。从这个角度讲,它与 Java 应用服务器有很大的相似性,只是它更面向业务功能而已。

Java 应用服务器

我们通常习惯性的认为 Web 应用是一个应用程序,但是从操作系统层面看,它却不是。它只是可以部署到应用服务器中的一种组件而已,它需要在应用服务器的管控之下提供服务。因此,我们可以将应用服务器和部署到它里面的 Web 应用一起视作一个可执行的应用程序。

如此审视应用服务器的架构,我们就会发现它是一个健壮的、高度组件化的、易于扩展、易于维护的应用程序。

我们将部署在应用服务器中的 Web 应用视为其提供业务功能的组件,就会发现这个平台有以下优秀的特性:

  • 平台提供基础的、业务无关的远程访问服务(HTTP/AJP)。
  • 平台自动加载业务组件(Web 应用),并根据其描述文件(web.xml)对远程请求进行分发处理。
  • 业务组件之间是完全松耦合的(类加载器的隔离机制),彼此互不干扰。
  • 平台为业务组件提供生命周期管理。通过 JMX,我们可以添加、升级、卸载组件,以及对组件配置进行修改。
  • 业务组件是可以任意组合的。我们可以将多个 Web 应用部署到一个应用服务器实例下,也可以将其分开部署。

通过这种架构,我们很容易基于平台和业务组件对运行实例进行定制,对可选择的业务组件进行升级、扩容。

Jetty

我们将 Jetty 这款轻量级 Java 应用服务器单独介绍,是因为抛开服务器对 Web 应用的松耦合管理不谈,它自身的模块化架构也是非常有借鉴意义的。

在 Jetty 中,各种基础功能(如 http、session、jmx、gzip、jsp 等等)都是以模块的形式提供的。在我们创建 Jetty 部署目录(jetty-base)时,指定需要包含的模块。那么在这个 Jetty 实例运行时,便只包含这几个模块的功能。这种机制使我们对 Jetty 实例的定制变得非常容易,而这与 Jetty 的高度模块化是分不开的。

Jetty 的模块包含的内容如下图所示:

enter image description here

  • .mod 文件用于声明模块的依赖模块、包含 JAR。
  • .xml 文件用于向 Server 注册本模块的组件。
  • .ini 文件用于设置模块工作需要的系统参数。

当我们使用 Jetty 提供的命令初始化一个 jetty-base 目录时,它会根据 *.mod 文件中的内容,分析模块依赖树,生成模块的 *.ini 配置文件。

当我们基于这个 jetty-base 目录运行 Jetty 实例时,Jetty 会根据 *.ini 文件确定加载的模块,再依据模块对应的 *.xml 文件创建组件并注册到 Server 上。

OSGi

与 PHP 这一类脚本语言开发的框架不同,Java 语言很难实现模块的动态化管理,或者说成本比较大。

这主要因为 Java 语言是基于类加载器机制的。我们要在一个类加载器中实现模块的启动、卸载、升级以及依赖管理是不可能的。首先,一个类加载器不允许存在两个限定名完全一样的类,因此无法实现多版本管理,其次,类加载器中的类不是分模块管理的,当卸载或者升级模块时,你无法实时替换类或者移除类(除非销毁整个类加载器并重新创建)。但是,类加载器之间具有天然的隔离性,应用服务器也采用类加载器机制确保Web应用的隔离。

但实现复杂并不代表不可能实现。OSGi 便是利用类加载器的隔离性,实现的一套动态模块化框架(更准确的说它只是一套规范,具体实现有 Equinox 和 Felix)。本文不打算详细展开介绍 OSGi 规范,感兴趣的可以参考这里

简而言之,OSGi 框架是充分模块化的,体现在几点:

  • 每个模块一个类加载器。
  • 每个模块可以选择依赖的模块及版本,也可以选择自己对外公开的包路径(不公开的包路径外部无法访问)。
  • 每个模块可以独立安装、升级、卸载,不影响程序运行。
  • 支持多版本并存,透明实现升/降级。

当然,OSGi 还有很多优秀的特性,不再一一赘述。

你可以发现,OSGi 是一个最完善的模块化方案,但是不得不说,在大多数情况下它显得过重。换句话说就是,你没有享受到它的好处,却承受了它的复杂度。

我们该怎么办

需要考虑的几个方面

通过分析上面的几个参考,我们大概总结一下模块化需要考虑的几个方面:

  • 自我描述文件。模块需要一个描述文件以告知运行环境(容器)它提供了哪些功能(包)、版本、依赖哪些模块以及相关的参数配置。运行环境(容器)在启动时,可以根据自我描述文件完成模块的加载。简而言之,需要至少做两件事,告知容器如何加载“我”,告知容器“我”能做什么(需要容器提供相应的机制,便于模块进行服务/功能注册)。
  • 支持模块对容器生命周期事件的监听及处理。模块可以根据这些生命周期事件完成一些初始化、注册以及销毁的处理。
  • 在系统运行状态下,实现对模块的安装、升级、卸载。这是最灵活也是要求最高的一个特征,当前只有 OSGi 和应用服务器(Web 应用管理)可以做得到。
  • 模块之间要有适当的隔离性,避免产生耦合。这个不难理解,大多数情况下我们在开发架构层面已经做了充分的工作。
  • 容器负责对模块生命周期进行管理及监控。通过容器监控,我们可以实时获取模块的运行状态,可以在运行状态下修改模块配置。

方案选择

从前面的讲解,我们可以知道,基于 Java 的模块化方案,动态的安装、升级、卸载复杂度是最高的,而这些都需要底层的基础容器完成。因此,我们需要仔细考虑引入这种复杂度是否划算,其带来的收益是否值得。

对于一些需要持续运行的非集群环境(如车载系统)就必须要考虑动态模块管理的问题,因为你不可能将系统停止进行升级。而对于可集群部署的环境(如常见的 Web 应用),那么我们完全可以将应用停止来进行升级,因此对于动态管理的需求并不突出。而引入诸如 OSGi 这种动态模块化框架所带来的成本则非常大,包括开发、测试、管理及运维监控。它对整体架构的影响甚至比你直接拆分为微服务都要大。

笔者几年前曾经尝试一种更粗粒度的基于 OSGi 的模块化方案,在这个方案中,数个 OSGi 组件合并为一个模块,系统以模块为单位确定依赖关系,进行生命周期管理。通过监控平台,支持对模块进行动态维护。后来发现在 OSGi 环境下开发系统所带来的成本远远超过了它带来的收益。

更多的情况下,我们并不是希望动态管理模块,而是只需要做到灵活部署就行了。因此,像 Jetty 这种,通过简单的命令便可以确定当前实例包含的模块,是一种更好的选择。至于模块之间的隔离性,多数情况下并不需要做到类加载器级别。

对于一款 Web 应用,模块化架构如图所示:

enter image description here

每个模块提供一个描述文件,告知容器它依赖的模块、包含的 JAR 包、支持的属性参数等。

应用容器在初始化时,根据“配置文件”确定要加载的模块及其依赖模块。如果你的开发架构松耦合做的足够好,已经将 web.xml 去掉,那么可以在应用中添加一个自定义文件,否则,你可以将配置放到 web.xml 中。容器分析依赖路径,得到当前实例需要加载的模块。

对于模块的加载,则有两种方式:

  • 得到加载模块的 JAR 包合集并进行加载。你会发现这种情况下,类加载器是自己维护的,作为 Web 应用类加载器的子加载器(如果你的应用服务器为嵌入式,如 Spring Boot 应用,那么你完全可以避免这一点)。这相当于是你自己实现了一套类加载机制,因此我们并不推荐这种方案。
  • 如果你仍希望使用 Web 应用类加载器,那么可以只模块化加载配置文件,对于 JAR 包则全集加载。这种情况,不需要改变 Web 应用的类加载器,但是仍需要平台提供模块解析及加载的功能,会对开发架构的松耦合带来影响。

除此之外,还有一种方式,就是将模块配置分析的过程放置到部署脚本中,部署脚本根据分析结果创建定制的部署包进行部署。这种方式不需要对应用程序的类加载机制进行改造,实现起来相对比较简单,对开发架构的影响也最小(还有就是它的产出物没有任何多余的内容,无论是配置文件还是 JAR 包)。

小结

本文探讨的部署架构的松耦合是在将应用拆分成独立的进程之前的一种中间状态,它比管理多个进程要容易一些,适合对应用按模块灵活组合并以独立实例的方式运行。

它需要结合前期的开发架构松耦合同步进行,尽可能不改造原有 Web 应用的构架(否则会带来不可预知的管理成本),在符合 Servlet 规范的前提下实现。

经过这种改造,在没有实现微服务架构的情况下,也可以对系统进行按模块运行以及扩容。

上一篇
下一篇
目录