最近一段时间一直在参与一些SaaS产品的设计,发现SaaS主产品有90%的功能基本都是通用性,其中10%各个租户都会出现定制化,比如一些页面表单字段的差异化、业务规则差异化以及流程差异化等,这些差异化如果软件架构的可扩展性不够,很容易出现后期维护成本非常高。其实技术发展到今天,软件架构设计有一个核心的理念一直没有不变,如何面的业务发展的不确定性快速实现,比如Nosql的出现为了弥补传统关系型数据库数据结构的不确定性,规则引擎的出现为了解决业务规则的不确定性。今天同样面对SaaS产品各个租户的需求不确定性以及差异化,让我很容易到微内核插件架构设计,微内核插件架构设计是一种非常典型的架构设计模式。
微内核架构本质上是为了提高系统的扩展性 。所谓扩展性,是指系统在经历不可避免的变更时所具有的灵活性,以及针对提供这样的灵活性所需要付出的成本间的平衡能力。也就是说,当在往系统中添加新业务时,不需要改变原有的各个组件,只需把新业务封闭在一个新的组件中就能完成整体业务的升级,我们认为这样的系统具有较好的可扩展性。
就架构设计而言,扩展性是软件设计的永恒话题。而要实现系统扩展性,一种思路是提供可插拔式的机制来应对所发生的变化。当系统中现有的某个组件不满足要求时,我们可以实现一个新的组件来替换它,而整个过程对于系统的运行而言应该是无感知的,我们也可以根据需要随时完成这种新旧组件的替换。
在正式介绍微内核插件架构之前,先介绍一下什么是内核以及内核分类。
百度百科是这样介绍内核:
内核,是一个操作系统的核心。是基于硬件的第一层软件扩充,提供操作系统的最基本的功能,是操作系统工作的基础,它负责管理系统的进程、内存、设备驱动程序、文件和网络系统,决定着系统的性能和稳定性。内核的分类可分为单内核和双内核以及微内核。
微内核(Micro kernel)是提供操作系统核心功能的内核的精简版本,它设计成在很小的内存空间内增加移植性,提供模块化设计,以使用户安装不同的接口,如 DOS、Workplace OS、Workplace UNIX 等。IBM、Microsoft、开放软件基金会(OSF)和 UNIX 系统实验室(USL)、鸿蒙 OS 等新操作系统都采用了这一研究成果的优点。
与微内核相对应的一个概念是宏内核,宏内核是包含很多功能的底层程序,干的事情很多,且不可插拔;一点微小的修改都可能会影响到整个内核,典型的”牵一发而动全身“。linux 就是宏内核,也因此被称为 monolithic OS。Linux除了时钟中断、进程创建与销毁、进程调度、进程间通信外,其他的文件系统、内存管理、输入输出、设备驱动管理都需要内核完成,其中的文件系统、内存管理、设备驱动等都被作为系统进程放到了用户态空间,属于可扩展插件部分。
微内核只负责最核心的功能,其他功能都是通过用户态独立进程以插件方式加入进来的,微内核负责进程的管理、调度和进程之间通讯,从而完成整个内核需要的功能。当某个功能出现问题时,由于该功能是以独立进程方式存在的,所以不会对其他进程有什么影响从而导致内核不可用,最多就是内核某一功能现在不可用而已。
微内核架构(Microkernel Architecture),也被称为插件式架构(plug-in architecture),作为一个在几十年前就被创建出来的架构模式,它如今仍然被广泛应用在各个领域中。从组成结构上讲, 微内核架构包含两部分组件:内核系统和插件 。这里的内核系统通常提供系统运行所需的最小功能集,而插件是独立的组件,包含自定义的各种业务代码,用来向内核系统增强或扩展额外的业务能力。
微内核架构由以下两部分组成:核心系统(core system)和插件(plug-in component),将应用系统的业务逻辑拆分成核心系统和插件,能够提供很好的可扩展性和灵活性,极大地方便了后续需求的新增和修改。
核心模块只拥有能使应用运行的最小功能逻辑。许多操作系统使用微内核系统架构,这就是该结构的名字由来。从业务应用的角度,核心系统通常定义了一般商务逻辑,不包含特殊情况、特殊规则、复杂的条件的特定处理逻辑。
插件模块是独立存在的模块,包含特殊的处理逻辑、额外的功能和定制的代码,能拓展核心系统业务功能。通常,不同的插件模块互相之间独立,但是你可以设计成一个插件依赖于另外一个插件的情况。最重要的是,你需要让插件之间的互依赖关系降低到最小,为避免繁杂的依赖问题。
在Web浏览器领域,谷歌的Chrome浏览器之所以被认为功能强大,一个很重要的原因是它有着丰富的插件类型;在开发工具领域,微软的VS Code初始安装后还只是个简单的文本编辑器,但用户可以安装各种插件,从而让它摇身一变成为功能强大的IDE。
Chrome和VS Code以及Eclipse、IDEA都是微内核架构的典型应用例子,它们提供一个具备最基础能力的核心系统,并定义好插件的开发接口。至于需要开发或安装哪种类型的插件,则完全由普通开发者和用户决定,这样的设计让系统具备了极强的可定制化和可扩展能力。
常见的一些开源框架比如Dubbo、ShardingSphere、Skywalking以及Apache ShenYU网关都支持插件化架构,每个开源软件的微内核插件设计核心思想都是一致的,其具体技术实现略有不同。在dubbo中,SPI的使用几乎是疯狂。dubbo针对JAVA SPI的局限性,自己重写了一套加载机制。可以针对不同的需求使用不同的实现类以及提供一些高级特性。
阿里巴巴的星环TMF框架其核心思想也是基于微内核插件架构,TMF2.0框架改造的交易平台支持了淘宝、天猫、聚划、盒马、大润发等一系列集团交易业务,通过业务管理域与运行域分离、业务与业务的隔离架构,大幅度提高了业务在可扩展性、研发效率以及可维护性问题,同时以更好的开放模式,让业务方能自助进行无侵入的需求开发。
微内核插件架构包含两个核心组件:系统核心(Core System)和插件化组件(Plug-in component)。Core System负责管理各种插件,当然Core System也会包含一些重要功能,如插件注册管理、插件生命周期管理、插件之间的通讯、插件动态替换等。整体结构如下:
微内核插件架构设计需要考虑如下四点:
事实上, Java中已经为我们提供了一种微内核架构的实现方式,就是JDK SPI。这种实现方式针对如何设计和实现 SPI 提出了一些开发和配置上的规范,ShardingSphere、Dubbo 使用的就是这种规范,只不过在这基础上进行了增强和优化。另外Java中提供了OSGI,也可以作为插件化架构设计,早期淘宝HSF组件中间版本做过OSGi的尝试,最终还是因为它的使用它的代价大于好处而放弃。
SPI主要用于框架设计。在框架设计中可能会有不同的实现类。如果所有的实现类都放入代码中的话,代码的耦合程度就太高了。SPI就是为了降低代码耦合度的,但是如果不知道SPI机制在看一些源码的时候就感觉云里雾里的。SPI简单来说就是使用配置文件来决定使用哪个实现类。将原来可能要在代码里面直接引用的实现类,写入到配置文件中,然后通过一个加载器去加载。
SPI优势很明显就是简单易用,如果对于只有一个实现类,比如JDBC。每个厂商都去实现但是每个厂商都只有自己一个实现类。针对这种上游接口Java SPI是合理的。
可以看到Java SPI是将文件里面的所有接口都去实现。在系统的实际开发中一个接口的实现类可能很多,在不同的场景下使用的也不一样。Java SPI这个时候就不太适用了。
如果有些实现类在运行时没有使用,并且加载比较繁琐,必然会耗费整个系统的资源。
OSGI的全称是open services gateway initiative,是一个插件化的标准,而不是一个可运行的框架。
OSGI的优势主要如下几点:
1.可复用性强
OSGI框架本身可复用性极强,很容易构建真正面向接口的程序架构,每一个Bundle 都是一个独立可复用的单元。
2.基于OSGI的应用程序可动态更改运行状态和行为。
在OSGI框架中,每一个Bundle实际上都是可热插拔的,因此,对一个特定的Bundle进行修改不会影响到容器中的所有应用,运行的大部分应用还是可以照常工作。当你将修改后的Bundle再部署上去的时候,容器从来没有重新启过。这种可动态更改状态的特性在一些及时性很强的系统中比较重要,尤其是在Java Web项目中,无需重启应用服务器就可以做到应用的更新。
3.职责分离
基于OSGI框架的系统可分可合,其结构的优势性导致具体的Bundle不至于影响到全局,不会因为局部的错误导致全局系统的崩溃。例如Java EE项目中可能会因为某个Bean的定义或注入有问题,而导致整个应用跑不起来,而使用OSGI则不会有这种问题,顶多相关的几个Bundle无法启动。
OSGI同样也有一些缺点:
1.类加载机制
每个Bundle都由单独的类加载器加载,与一些Java EE项目中使用比较多的框架整合比较困难,如Spring MVC、Struts2等,例如笔者尝试在OSGI应用中整合Spring MVC时,通过DispatcherServlet启动的Bean与OSGI Bundle启动的Bean无法相互依赖,需要做特殊处理,后面文章中会有介绍。
2.OSGI框架提供的管理端不够强大
现在的管理端中仅提供了基本的Bundle状态管理、日志查看等功能,像动态修改系统级别的配置(config.ini)、动态修改Bundle的配置(Manifest.mf)、启动级别等功能都尚未提供,而这些在实际的项目或产品中都是非常有必要的。
3.使用成本高
采用OSGI作为规范的模块开发、部署方式自然给现有开发人员提出了新的要求,需要学习新的基于OSGI的开发方式。
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface Extension {
String tenantId() default "0";//根据SaaS租户ID进行隔离
int ordinal() default 0;
Class<? extends ExtensionPoint>[] points() default {};
String[] plugins() default {};
}
扩展点表示一块逻辑在不同的业务有不同的实现,使用扩展点做接口申明。
public interface ExtensionPoint {
}
主程序定义插件类Plugin,用于封装从插件配置文件读取业务扩展实现,具体定义如下:
@Data
public class Plugin {
/**
* 插件名称
*/
private String pluginId;
/**
* 插件版本
*/
private String version;
/**
* 插件路径
*/
private String path;
/**
* 插件类全路径
*/
private String className;
}
创建插件管理类,初始化插件。
/**
* 使用URLClassLoader动态加载jar文件,实例化插件中的对象
*
*/
public abstract class PluginManager {
private Map<String, Class> clazzMap = new HashMap<>();
public PluginManager(List<Plugin> plugins) throws PluginException {
init(plugins);
}
/**
* 插件初始化方法
*/
private void init(List<Plugin> plugins) throws MalformedURLException {
try{
int size = plugins.size();
for(int i = 0; i < size; i++) {
Plugin plugin = plugins.get(i);
String filePath = plugin.getPath();
// URL url = new URL("file:" + filePath);
URL url = new File(plugin.getPath()).toURI().toURL();
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{url});
Class<?> clazz = urlClassLoader.loadClass(plugin.getClassName());
clazzMap.put(plugin.getClassName(), clazz);
}
}catch (Exception e) {
throw new PluginException("plugin " + plugin.getPluginName() + " init error," + e.getMessage());
}
}
/**
* 获得插件
* @param className 插件类全路径
* @return
* @throws PluginException
*/
public ExtensionPoint getInstance(String className) throws PluginException {
// 插件实例化对象,插件都是实现ExtensionPoint接口
Class clazz = clazzMap.get(className);
Object instance = null;
try {
instance = clazz.newInstance();
} catch (Exception e) {
throw new PluginException("plugin " + className + " instantiate error," + e.getMessage());
}
return (ExtensionPoint)instance;
}
PluginState startPlugin(String pluginId);
/**
* 停止所有插件
*/
void stopPlugins();
/**
* 停止插件
*/
PluginState stopPlugin(String pluginId);
/**
* 卸载所有插件
*/
void unloadPlugins();
/**
*卸载插件
*/
boolean unloadPlugin(String pluginId);
}
public class Main {
public static void main(String[] args) {
try {
// 从配置文件加载插件
List<Plugin> pluginList = PluginLoader.load();
// 初始化插件管理类
PluginManager pluginManager = new PluginManager(pluginList);
// 循环调用所有插件
for(Plugin plugin : pluginList) {
ExtensionPoint extensionPoint = pluginManager.getInstance(plugin.getClassName());
System.out.println("开始执行[" + plugin.getName() + "]插件...");
// 调用插件
extensionPoint.xxx();
System.out.println("[" + plugin.getName() + "]插件执行完成");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
以上就是一个非常简版的基于扩展点的微内核插件,其设计思想和多数微内核插件架构相差无几,当然如果要用到生产还需要考虑很多问题,比如热部署问题、类隔离机制以及对于多语言的支持等。
关于Java的一些开源微内核插件架构也有一些开源实现:
SpringPlugin:https://github.com/spring-projects/spring-plugin
p4fj:https://github.com/pf4j/pf4j
jspf:https://code.google.com/archive/p/jspf/
结合我们自己的一些业务场景,参考p4fj设计思想我也开发了一个微内核插件框架,解决了热部署问题、类隔离机制以及对于多语言等问题,主要应用于SaaS一些业务租户定制化需求,后面我也会考虑将其开源出来放到GitHub上,大家敬请关注。
Robert C.Martin曾经说过,软件开发技术发展的历史就是一个如何想方设法方便地增加插件,从而构建一个可扩展、可维护的系统架构的故事。在敏捷开发的潮流之下,需求的变更如同家常便饭,系统不应该因为某一部分发生变更从而导致其他不相关的部分出现问题。将系统设计为微内核架构,就等于构建起了一面变更无法逾越的防火墙,插件发生的变更就不会影响系统的核心业务逻辑。
微内核架构的设计思想,能够极大提升系统的可扩展性和健壮性,在其他的一些软件方法论里,我们也隐约能看到它的影子。比如在领域驱动设计中,领域层就相当于核心系统,它定义了系统的核心业务逻辑;基础设施层则相当于插件,切换不同的基础设施并不会影响系统的业务逻辑,这得益于基础设施层依赖倒置的设计原则。
当然,作为微内核架构也有着一些缺点,它天然具备了单体架构的一些劣势,比如核心系统作为架构的中心节点并不具备Fault tolerance能力。因此,该架构模式往往被广泛应用于一些着重提供很强的用户定制化功能的小型产品,如VS Code等,它们对系统的Elasticity、Fault tolerance和Scalability并没有很高的要求。