前言
最近一段時間一直在參與一些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也會包含一些重要功能,如插件注冊管理、插件生命周期管理、插件之間的通訊、插件動態替換等。整體結構如下:

微內核插件架構設計需要考慮如下四點:
插件管理
- 核心系統需要知道當前有哪些插件可用,如何加載這些插件,什么時候加載插件。常見的實現方法是插件注冊表機制。
- 核心系統提供插件注冊表(可以是配置文件,也可以是代碼,還可以是數據庫),插件注冊表含有每個插件模塊的信息,包括它的名字、位置、加載時機(啟動就加載,還是按需加載)等。
插件連接
- 插件連接指插件如何連接到核心系統。通常來說,核心系統必須制定插件和核心系統的連接規范,然后插件按照規范實現,核心系統按照規范加載即可。
- 常見的連接機制有 OSGi(Eclipse 使用)、消息模式、依賴注入(Spring 使用),甚至使用分布式的協議都是可以的,比如 RPC 或者 HTTP Web 的方式。
插件通信
- 插件通信指插件間的通信。雖然設計的時候插件間是完全解耦的,但實際業務運行過程中,必然會出現某個業務流程需要多個插件協作,這就要求兩個插件間進行通信。
- 微內核的核心系統也必須提供類似的通信機制,各個插件之間才能進行正常的通信。
事實上, Java中已經為我們提供了一種微內核架構的實現方式,就是JDK SPI。這種實現方式針對如何設計和實現 SPI 提出了一些開發和配置上的規范,ShardingSphere、Dubbo 使用的就是這種規范,只不過在這基礎上進行了增強和優化。另外Java中提供了OSGI,也可以作為插件化架構設計,早期淘寶HSF組件中間版本做過OSGi的嘗試,最終還是因為它的使用它的代價大于好處而放棄。
SPI
SPI主要用于框架設計。在框架設計中可能會有不同的實現類。如果所有的實現類都放入代碼中的話,代碼的耦合程度就太高了。SPI就是為了降低代碼耦合度的,但是如果不知道SPI機制在看一些源碼的時候就感覺云里霧里的。SPI簡單來說就是使用配置文件來決定使用哪個實現類。將原來可能要在代碼里面直接引用的實現類,寫入到配置文件中,然后通過一個加載器去加載。
SPI優勢很明顯就是簡單易用,如果對于只有一個實現類,比如JDBC。每個廠商都去實現但是每個廠商都只有自己一個實現類。針對這種上游接口Java SPI是合理的。
可以看到Java SPI是將文件里面的所有接口都去實現。在系統的實際開發中一個接口的實現類可能很多,在不同的場景下使用的也不一樣。Java SPI這個時候就不太適用了。
如果有些實現類在運行時沒有使用,并且加載比較繁瑣,必然會耗費整個系統的資源。
OSGI
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的開發方式。
微內核插件架構設計
1.定義擴展點注解
@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 {};
}
2.定義業務擴展點
擴展點表示一塊邏輯在不同的業務有不同的實現,使用擴展點做接口申明。
public interface ExtensionPoint {
}
3.插件實體定義
主程序定義插件類Plugin,用于封裝從插件配置文件讀取業務擴展實現,具體定義如下:
@Data
public class Plugin {
/**
* 插件名稱
*/
private String pluginId;
/**
* 插件版本
*/
private String version;
/**
* 插件路徑
*/
private String path;
/**
* 插件類全路徑
*/
private String className;
}
4.插件管理
創建插件管理類,初始化插件。
/**
* 使用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);
}
5.測試
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并沒有很高的要求。