微内核架构风格(也称为插件化架构)在几十年前被创造出来,直到今天仍然被广泛使用。这种架构风格自然适合基于产品的应用(打包并可作为单个的整体部署进行下载和安装,通常作为第三方产品安装在客户站点上),但也广泛应用于许多非产品定制业务应用。
拓扑结构
微内核架构风格是一种相对简单的单体架构,由两部分架构组件组成:核心系统和插件组件。应用逻辑在独立的插件组件和基本核心系统之间进行划分,提供了可扩展性、适应性以及应用程序特性和自定义处理逻辑的隔离。图12-1说明了微内核架构风格的基本拓扑结构。
核心系统
核心系统正式定义是:运行系统所需的最小功能。Eclipse IDE就是一个很好的例子。Eclipse的核心系统只是一个基本的文本编辑器:打开一个文件,更改一些文本,然后保存文件。直到你添加了一些插件,Eclipse才开始成为一个有用的产品。然而,核心系统的另一个定义是贯穿应用的快乐路径(通用处理流),几乎没有定制处理。去除核心系统的圈复杂度并将其放入单独的插件组件中,可以获得更好的可扩展性和可维护性,以及更高的可测试性。例如,假设一个电子设备回收应用必须为收到的每个电子设备执行特定的自定义评估规则。用于此类处理的Java代码可能如下所示:
public void assessDevice(String deviceID) {
if (deviceID.equals("iPhone6s")) {
assessiPhone6s();
} else if (deviceID.equals("iPad1"))
assessiPad1();
} else if (deviceID.equals("Galaxy5"))
assessGalaxy5();
} else ...
...
}
}
与其将所有特定于某个客户的定制逻辑放在圈复杂度很高的核心系统中,为每个被评估的电子设备创建一个单独的插件组件会好很多。特定的客户插件组件不仅将独立的设备逻辑与其余的处理流程隔离开来,而且顾及到扩展性。添加一个新的要评估的电子设备只需简单添加一个新的插件组件并更新注册表。对于微内核架构风格,评估一个电子设备只需要核心系统定位并调用相应的设备插件,如修订后的源代码所示:
public void assessDevice(String deviceID) {
String plugin = pluginRegistry.get(deviceID);
Class<?> theClass = Class.forName(plugin);
Constructor<?> constructor = theClass.getConstructor();
DevicePlugin devicePlugin =
(DevicePlugin)constructor.newInstance();
DevicePlugin.assess();
}
在这个例子中,评估一个特定电子设备的所有复杂规则和指令都自包含在一个独立的、互不影响的插件组件中,该组件一般可以从核心系统执行。
根据规模和复杂性,核心系统可以实现为分层架构或模块化单体(如图12-2所示)。在某些情况下,核心系统可以拆分为多个单独部署的领域服务,每个领域服务都包含特定于该领域的插件组件。例如,假设支付处理是表示核心系统的领域服务。每种支付方法(信用卡、贝宝、商店信用卡、礼品卡和购货单)都是特定于支付领域的独立插件组件。在上述例子中,通常整个单体应用共享一个数据库。
核心系统的表示层可以嵌入核心系统中,也可以实现为单独的用户界面,核心系统提供后端服务。事实上,一个单独的用户界面也可以使用微内核架构风格来实现。图12-3说明了这些表示层变体与核心系统的关系。
插件组件
插件组件是独立的、互不影响的组件,包含用于增强或扩展核心系统的专门的处理、附加功能和定制化代码。此外,它们还可以用于隔离高度易变性的代码,从而在应用中建立更好的可维护性和可测试性。理想情况下,插件组件应该彼此独立,并且它们之间没有任何依赖关系。
插件组件和核心系统之间的通信通常是点对点的,这意味着将插件连接到核心系统的“管道”通常是对插件组件的入口类的一个方法或函数调用。此外,插件组件可以是基于编译的,也可以是基于运行时的。基于运行时的插件组件可以在运行时添加或删除,而无需重新部署核心系统或其他插件,它们通常通过开放式服务网关倡议(OSGi)(Java)、Penrose(Java)、Jigsaw(Java)或Prism(.NET)等框架进行管理。基于编译的插件组件更易于管理,但需要在修改、添加或删除时重新部署整个单体应用。
点对点插件组件可以实现为共享库(如JAR、DLL或Gem)、Java中的包名或C#中的命名空间。继续以电子设备回收评估应用为例,每个电子设备插件可以编写并实现为一个JAR、DLL或Ruby Gem(或任何其他共享库),设备名称与独立共享库的名称匹配,如图12-4所示。
或者,图12-5所示的一种更简单的方法是在同一个代码库或IDE项目中将每个插件组件实现为一个单独的命名空间或包名。创建命名空间时,建议使用以下格式:app.plug-in.<domain><context>。例如,考虑名称空间app.plugin.assessment.iphone6s。第二个节点(plugin)清楚地表明这个组件是一个插件,因此应该严格遵守关于插件组件的基本规则(即它们是自包含的,并且与其他插件分离)。第三个节点描述领域(在本例中是评估),从而允许插件组件按照一个共同的目的进行组织和分组。第四个节点(iphone6s)描述插件的特定上下文,从而易于定位特定设备插件以进行修改或测试。
插件组件不一定总是与核心系统进行点对点通信。存在其他替代方案,包括使用REST或消息传递作为调用插件功能的手段,每个插件都是一个独立的服务(甚至可能是使用容器实现的微服务)。虽然这听起来是一个提高整体可扩展性的好方法,但请注意,由于单体的核心系统,这种拓扑结构(如图12-6所示)仍然只是一个单一的架构量子。
使用远程访问方式访问作为单个服务实现的插件组件的好处是,它提供了更好的整体组件解耦,允许更好的可扩展性和吞吐量,并且允许在没有任何特殊框架(如OSGi、Jigsaw或Prism)的情况下进行运行时变更。它还允许与插件进行异步通信,在某些场景下可以显著提高总体用户响应能力。以电子设备回收应用为例,核心系统可以发起异步请求,启动对特定设备的评估,而不必等待电子设备评估运行。当评估完成时,插件可以通过另一个异步消息传递通道通知核心系统,转而通知用户评估已完成。
得到这些好处需要作出权衡。远程插件访问将微内核架构转变为分布式架构,而不是单体架构,这使得大多数第三方预置型产品难以实现和部署。此外,它还增加了总体复杂性和成本,并使整个部署拓扑复杂化。如果一个插件没有响应或没有运行,特别是在使用REST时,则请求无法完成。单体部署则不是这种情况。选择是从核心系统点对点还是远程与插件组件进行通信应基于特定的需求,因此需要对这种方法的优缺点进行仔细的权衡分析。
插件组件直接连接到中央共享数据库不是一种常见的做法。相反,核心系统承担这一责任,将所需的数据传递到每个插件中。这种做法的主要原因是为了解耦。对数据库进行更改应该只会影响核心系统,而不会影响插件组件。也就是说,插件可以有自己的独立数据存储,只有该插件才能访问。例如,电子设备回收系统例子中的每个电子设备评估插件都可以有自己的简单数据库或规则引擎,其中包含每个产品的所有特定评估规则。插件组件拥有的数据存储可以是外部的(如图12-7所示),也可以作为插件组件或单体部署的一部分嵌入(如内存数据库或嵌入式数据库的情况)。
注册表
核心系统需要知道哪些插件模块可用,以及如何访问它们。一种常见方法是通过插件注册表来实现。注册表包含有关每个插件模块的信息,包括其名称、数据契约和远程访问协议详细信息(取决于插件与核心系统的连接方式)。例如,标记高风险税务审计项目的税务软件插件可能具有包含服务名称(AuditChecker)、数据契约(输入数据和输出数据)和契约格式(XML)的注册表项。
注册表可以像只包含键值和插件组件引用的核心系统拥有的内部映射结构一样简单,也可以像嵌入核心系统或部署在外部(如Apache Zookeeper或Consul)的注册表和发现工具一样复杂。以电子设备回收系统为例,以下Java代码在核心系统中实现了一个简单的注册表,显示了用于评估iPhone 6S设备的点到点条目、消息条目和RESTful条目示例:
Map<String, String> registry = new HashMap<String, String>();
static {
//point-to-point access example
registry.put("iPhone6s", "Iphone6sPlugin");
//messaging example
registry.put("iPhone6s", "iphone6s.queue");
//restful example
registry.put("iPhone6s", "https://atlas:443/assess/iphone6s");
}
契约
插件组件和核心系统之间的契约通常是跨插件组件领域的标准协议,包括从插件组件返回的行为、输入数据和输出数据。自定义契约通常会在插件组件由第三方开发,你无法控制插件所使用的契约的情况下建立。在这种情况下,通常在插件契约和你的标准契约之间创建一个适配器,这样核心系统就不需要为每个插件编写专门的代码。
插件契约可以用XML、JSON甚至插件和核心系统之间来回传递的对象来实现。在电子设备回收应用中,以下契约(作为名为AssessmentPlugin的标准Java接口实现)定义了总体行为(assessment()、register()和deregister()),以及插件组件(AssessmentOutput)预期的相应输出数据:
public interface AssessmentPlugin {
public AssessmentOutput assess();
public String register();
public String deregister();
}
public class AssessmentOutput {
public String assessmentReport;
public Boolean resell;
public Double value;
public Double resellPrice;
}
在这个契约示例中,设备评估插件将以格式化字符串的形式返回评估报告;转售标志(true或false),表明该设备是否可以在第三方市场转售或被安全处置;最后,如果它可以转售(另一种形式的回收),那么设备的价值是多少,建议转售价格应该是多少。
注意本例中核心系统和插件组件之间的角色和责任模型,特别是assessmentReport字段。核心系统不负责格式化和理解评估报告的细节,只是打印出来或显示给用户。
示例和用例
大多数用于开发和发布软件的工具都是使用微内核架构实现的。一些例子包括eclipseide、PMD、Jira和Jenkins等等。Chrome和Firefox等互联网浏览器是使用微内核架构的另一个常见产品例子:查看器和其他插件添加了在代表核心系统的基本浏览器中没有的附加功能。对于基于产品的软件来说,例子不胜枚举,但是大型商业应用呢?微内核架构也适用于这些情况。为了说明这一点,考虑一个涉及保险索赔处理的保险公司例子。
索赔处理是一个非常复杂的过程。对于保险索赔中允许和不允许的内容,每个司法管辖区都有不同的规章制度。例如,一些司法管辖区(如州)允许在挡风玻璃被岩石损坏时免费更换,而其他州则不允许。这为标准索赔流程创建了几乎无限的条件集合。
大多数保险索赔应用都利用大型复杂的规则引擎来处理这种复杂性。然而,这些规则引擎可能会发展成一个复杂的大泥球,其中更改一个规则会影响其他规则,或者进行一个简单的规则更改需要一大群分析师、开发人员和测试人员来确保一个简单的更改不会破坏任何内容。使用微内核架构模式可以解决许多这些问题。
每个辖区的索赔规则可以包含在独立的插件组件中(通过源代码或插件组件访问的特定规则引擎实例来实现)。通过这种方式,可以为特定辖区添加、删除或更改规则,而不会影响系统的任何其他部分。此外,可以在不影响系统其他部分的情况下添加和删除新的辖区。本例中的核心系统是提交和处理索赔的标准流程,这一过程不会频繁更改。
另一个可以利用微内核架构的大型复杂业务应用的例子是税务准备软件。例如,美国有一个基本的两页的纳税申报表称为1040表格,其中包含计算个人纳税义务所有所需信息的摘要。1040纳税申报表中的每一行都有一个数字,需要许多其他表格和工作表才能得出这个数字(例如总收入)。每一个这些附加表单和工作表中都可以作为一个插件组件来实现,而1040税收汇总表作为核心系统(驱动程序)。这种方式对税法的修改可以隔离于一个独立的插件组件,从而使变更更容易,风险更小。
架构特性评级
特性评级表中的一星级评级(如图12-8所示)意味着特定的架构特性在某种架构中没有得到很好的支持,而五星评级意味着架构特性是某种架构风格中最强大的特性之一。记分卡中确定的每个特性的定义见第4章。
与分层架构风格类似,简单性和总体成本低是微内核架构风格的主要优势,可伸缩性、容错性和可扩展性是其主要弱点。这些弱点是由于微内核架构内在的典型单体部署造成的。而且,与分层架构风格一样,量子数总是单个的(1),因为所有请求都必须经过核心系统才能到达独立的插件组件。这些就是与分层架构的相似之处。
微内核架构风格的独特之处在于它是唯一既可以进行领域划分又可以进行技术划分的架构风格。虽然大多数微内核架构是技术划分的,但是领域划分方面主要是通过一个强大的领域到架构同构来实现的。例如,每个区域或客户需要不同配置的问题与这种架构风格非常匹配。另一个例子是非常强调用户定制和功能扩展性的产品或应用(例如JIRA或像Eclipse这样的IDE)。
可测试性、可部署性和可靠性略高于平均水平(三星级),主要是因为功能可以隔离于独立的插件组件。如果操作正确,这将减少变更的总体测试范围,并降低部署的总体风险,特别是在以运行时方式部署插件组件的情况下。
模块化和可扩展性也略高于平均水平(三星级)。使用微内核架构风格,可以通过独立的、自包含的插件组件添加、删除和更改附加功能,从而相对容易地扩展和增强使用这种架构风格创建的应用,并允许团队更快地响应更改。考虑上一节中的税务准备软件例子。如果美国税法发生变化(它也是一直这么做的),需要一个新的纳税申报表,那么这个新的纳税申报表可以作为插件组件创建并添加到应用中,而不需要做太多的工作。同样地,如果不再需要一个税务表或工作表,则可以从应用中删除该插件。
性能一直是微内核架构风格的一个有趣的特性。我们给它三颗星评级(略高于平均水平),主要是因为微内核应用通常都很小,并且没有像大多数分层架构那样增长得那么庞大。而且,他们也不会受到第10章中讨论的架构污水池反模式的影响。最后,可以通过去掉不需要的功能来简化微内核架构,从而使应用运行更快。一个很好的例子就是Wildfly(JBoss应用服务器的前身)。通过取消诸如集群、缓存和消息传递等不必要的功能,应用服务器的性能比使用这些功能的时候要快得多。