《从分层架构到微服务架构》是一系列介绍《Fundamentals of Software Architecture》中提到的8种架构模式的文章,这里不会事无巨细地介绍所有的细节,而是会挑选其中关键内容,更多详情请阅读原书。
往期精彩:
前言
微内核架构(Microkernel Architecture),也被称为插件式架构(plug-in architecture),作为一个在几十年前就被创建出来的架构模式,它如今仍然被广泛应用在各个领域中。比如在Web浏览器领域,谷歌的Chrome浏览器之所以被认为功能强大,一个很重要的原因是它有着丰富的插件类型;在开发工具领域,微软的VS Code初始安装后还只是个简单的文本编辑器,但用户可以安装各种插件,从而让它摇身一变成为功能强大的IDE。
Chrome和VS Code都是微内核架构的典型应用例子,它们提供一个具备最基础能力的核心系统,并定义好插件的开发接口。至于需要开发或安装哪种类型的插件,则完全由普通开发者和用户决定,这样的设计让系统具备了极强的可定制化和可扩展能力。
架构视图
微内核架构由以下两部分组成:核心系统(core system)和插件(plug-in component),将应用系统的业务逻辑拆分成核心系统和插件,能够提供很好的可扩展性和灵活性,极大地方便了后续需求的新增和修改。
核心系统
核心系统通常只需提供能够支撑整个系统正常运行的基本功能,比如前文所举的VS Code例子,用户初始安装的是VS Code的核心系统,它只是一个提供了打开文件、编辑文件内容和保存文件等基本功能的文本编辑器,其他的扩展功能(如语法检查)都是通过安装插件集成的。将复杂的业务逻辑从核心系统中剥离出来,并通过插件实现,能够提升系统的可扩展性和可维护性。同时,因为复杂的功能都成了互不干扰的插件,系统的可测性也得到了提高。
考虑现在需要实现一个电子设备回收系统,在回收之前,每种型号的手机设备的回收流程都不一样,那么我们可以这样去实现:
public void assessDevice(String deviceID) {
if (deviceID.equals("iPhone6s")) {
assessiPhone6s();
} else if (deviceID.equals("iPad1"))
assessiPad1();
} else if (deviceID.equals("Galaxy5"))
assessGalaxy5();
} else ...
...
}
}
如果我们把assessDevice
看成是核心系统,那么后面每次新增一个型号的手机,都需要新增一个if
分支,也即对核心系统进行了改动。这样的设计会导致核心系统非常地脆弱,正所谓改的越多,出问题的概率也越大。
比起这种将所有的可定制业务逻辑放在核心系统上的设计,更好的应该是将它们实现为插件的形式,这样不仅每个设备回收逻辑都解耦了,还提供了强大的可扩展性:添加一个新的回收设备类型,只需新增一种插件即可,核心系统无需变动。
public void assessDevice(String deviceID) {
String plugin = pluginRegistry.get(deviceID);
DevicePlugin devicePlugin =
(DevicePlugin)constructor.newInstance();
DevicePlugin.assess();
}
微内核架构在实现时通常都结合了其他架构模式,这主要体现在核心系统的设计上,比如根据具体的业务特点,我们可以将核心系统设计成technically partitioned的分层架构,或者是domain partitioned的模块化架构。
插件
插件就是一些包含了定制化业务逻辑、扩展功能、附加功能的独立组件,用于扩充核心系统的功能。插件之间是独立的,插件与核心系统之间则一般是“点对点”通信:核心系统通过调用插件提供的接口(比如插件类的方法)使用扩展功能。
插件可以划分为编译时插件和运行时插件两种类型,前者每次变更都需要重新构建和部署整个系统,但实现较为简单;后者则可以在系统运行时进行插件的新增和删除操作,相对地,实现也较为复杂。
编译时插件
在编译时插件中,插件通常以package或namespace实现,比如在package中可以以这样的命名规则来区分插件:app.plug-in.<domain>.<context>
。
运行时插件
运行时插件中插件的实现通常是动态库的形式,比如.jar
、.so
、.dll
文件。在上述的设备回收系统的例子中,每种型号的手机设备回收逻辑包含在一个独立的.jar
文件中:
远端插件
当然,插件和核心系统并非只能通过本地接口调用进行通信,还可以采用REST/消息队列/RPC等方式,这种场景下,插件就变成了一个独立部署的服务。远程插件具备运行时插件的特点,而且能够提供更好的scalability:插件和核心系统甚至都不必使用相同的技术栈实现,只需遵守既定的REST接口即可。
为了提升系统处理请求的responsiveness,我们还可以将核心系统调用插件的过程实现为异步通信。以前文的电子设备回收系统为例,在异步通信的架构下,系统通过一个线程触发插件启动对某个设备的回收流程。之后,该线程无需一直等待回收结束,它可以去继续回收别的设备。当设备回收结束后,插件会通过异步队列告知核心系统。这样的异步设计可以减少无谓的等待流程,明显改善系统的responsiveness。
如果涉及到读写数据库,为了能够维持插件的独立性,每个插件最好能够拥有独立的数据库。如果插件间有着无可避免的数据交互,则可以为核心系统配置一个中心数据库,并通过它来进行数据中转。
插件中心
核心系统在加载插件前,必须得知道当前有哪些可用的插件,以及这些插件在哪里可以获取。这要求系统有一个地方去管理插件,这就是插件中心(plug-in registry)的功能。插件中心类似于服务化架构中服务注册中心的作用,它保存了所有插件的基本信息,包括名称、数据契约、通信协议、加载地址等。
我们可以简单地将插件中心实现为一个本地的map
表,其中key可以是插件名称,value为获取插件的地址:
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");
}
为了实现一些较为复杂的功能,如插件上下线通知等,我们还可以借助Apache ZooKeeper、ETCD这类的分布式协同系统实现远程插件中心。
通信契约
通信契约定义了插件与核心系统之间的通信方式、交互行为和数据格式。通信方式可以是本地接口调用、REST、RPC、消息队列等;交互行为则可以理解为插件对核心系统提供的接口,比如本地的函数/方法、REST的URI等;对本地插件而言,数据格式通常是一个类/结构体,对远程插件而言,常用的数据格式有JSON、XML、ProtoBuf等。
考虑电子设备回收系统的例子,系统有着如下定义的通信契约:
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;
}
从该契约定义中可以看出,通信方式为本地接口调用(AssessmentPlugin
接口);它有着3个交互行为,assess()
为回收设备流程、register()
表示将该插件注册到插件中心、deregister
表示去注册;数据格式则是AssessmentOutput
类,它定义了回收流程的结果。
架构评分
和之前介绍的分层架构、管道架构一样,微内核架构同样属于单体架构,因此Simplicity和Overall cost是该架构模式主要优势;而Elasticity、Fault tolerance和Scalability是主要劣势。
另外,微内核架构的Testability、Deployability、Reliability、Modularity之所以能够取得3颗星,得益于不同的功能能够被拆分至独立的插件上,特别地,运行时插件的增删无需重新部署系统。这使得系统能够快速响应需求变更,具备很高的扩展性。比如对于前面的电子设备回收系统,如果需要新增一种新的电子设备回收流程,只需新增一个插件即可;如果某种设备不再需要回收,则去除对应插件即可。
微内核架构比较特别的一点是,它既可以是technically partitioned,也可以是domain partitioned,这取决于核心系统的实现方式,前文也有介绍。
总结
Robert C.Martin曾经说过,软件开发技术发展的历史就是一个如何想方设法方便地增加插件,从而构建一个可扩展、可维护的系统架构的故事。在敏捷开发的潮流之下,需求的变更如同家常便饭,系统不应该因为某一部分发生变更从而导致其他不相关的部分出现问题。将系统设计为微内核架构,就等于构建起了一面变更无法逾越的防火墙,插件发生的变更就不会影响系统的核心业务逻辑。
微内核架构的设计思想,能够极大提升系统的可扩展性和健壮性,在其他的一些软件方法论里,我们也隐约能看到它的影子。比如在领域驱动设计中,领域层就相当于核心系统,它定义了系统的核心业务逻辑;基础设施层则相当于插件,切换不同的基础设施并不会影响系统的业务逻辑,这得益于基础设施层依赖倒置的设计原则。
当然,作为微内核架构也有着一些缺点,它天然具备了单体架构的一些劣势,比如核心系统作为架构的中心节点并不具备Fault tolerance能力。因此,该架构模式往往被广泛应用于一些着重提供很强的用户定制化功能的小型产品,如VS Code等,它们对系统的Elasticity、Fault tolerance和Scalability并没有很高的要求。
每种架构模式都有其合适的应用场景,只有熟悉常用的几种架构模式,才能设计出更好的软件系统。下一篇文章,我们将继续介绍面向服务的架构。