微内核架构
微内核架构概述
什么是微内核架构?如果换一个名字,或许大家就很熟悉了,那就是插件系统。
我们实际工作生活中接触到的大型软件,大部分都拥有插件系统。
比如开发工具 vscode,拥有一个强大的插件系统,可以为 vscode 添加新的语法支持,新的主题,甚至添加 vscode 原本不支持的能力,通过社区贡献的2万多个插件,vscode 的能力变得所向披靡。
相较之下,没有插件系统的notepad之类的软件,功能就很单一,也没有任何扩展的可能性。
拥有强大的插件系统还有浏览器 chrome,前端的构建工具 webpack, rollup 等。几乎所有大型的软件,都拥有一个插件系统。
微内核架构中,软件的核心部分通常被称为微内核,或者宿主程序。微内核提供了一些标准接口和扩展点,允许插件以某种方式与其进行交互。插件则是独立的模块,可以独立开发并在宿主应用程序中加载和执行。
实际开发过程中,是不是一定要引入微内核架构呢,答案是否定的,具体需要结合软件系统诉求,看是否有对自身进行定制或者扩展的需求,是否能解决当前软件系统面临的问题。
为什么需要微内核架构
那微内核架构到底解决了什么软件问题?
主要体现在两个方面:
- 对软件本身现有的能力进行定制化
- 为软件提供全新的能力
这样的好处就在于,它提供了一套开放的接口,可以方便第三方来参与软件的定制和扩展,让大型软件的能力得以灵活的扩展。
其实微内核架构的实现并没有统一的标准,它的架构示意如下图所示:
微内核架构的核心代码保持逻辑单一,只负责程序的启动销毁,功能模块的加载,执行,卸载。软件的功能叠加由不同的插件来实现,并挂载到核心上实现功能的扩展。
这样允许软件的功能可以被动态地扩展和定制,在增强现有软件的功能或添加新功能的同时,无需修改核心程序代码。
可以看一下跟微内核完全相反的另一种架构设计:
把一个软件和它的各种功能都做在一起,内核功能与各个功能模块耦合在一起,如下图:
这种场景下,当我们需要定制某个功能时,我们需要直接修改软件的内核逻辑,显然不符合软件设计的开闭原则,不仅增加了软件开发的维护难度,同时也大大提升软件扩展的难度,从而使软件本身不具备有良好的扩展性。
如果将宏内核的架构改为微内核架构:
每个功能都成为插件,独立维护开发,不与内核耦合。每个插件需要定制,可以独立修改、发布,不影响其他插件及内核,同时也可以添加新的插件。相比宏内核,软件的维护难度大大降低,同时只要遵循插件的接口定义,就可以为软件开发新的功能,降低了软件扩展的难度。使得软件获得了很好的灵活性和扩展性。
总结下来,微内核架构有如下的优势:
- 灵活性和可扩展性: 插件系统允许软件在运行时加载和卸载插件,从而实现灵活的功能扩展和定制化。通过插件,可以根据用户需求添加、移除或替换特定功能,而不需要修改核心代码,使得软件更易于扩展,易于适应变化的需求。
- 代码重用和模块化: 插件可以看作是独立的模块,它们可以在不同的应用中重复使用。这种模块化的设计使得代码更加可维护,减少了代码冗余,提高了代码重用率。
- 社区参与和共享: 插件系统鼓励社区的参与和贡献,第三方开发者可以开发自己的插件并与软件进行集成。这样,软件的功能得到了大大丰富,社区成员可以共享自己的扩展,促进了软件生态系统的发展。
- 解耦合和维护性: 插件系统帮助将软件的功能划分为独立的部分,降低了模块之间的耦合度。这使得软件更易于维护,当需要修改或升级某个功能时,只需关注相应的插件而不会影响整个系统。
- 性能和资源优化: 插件的动态加载和卸载使得软件可以根据需要来选择加载特定的功能,从而节约了内存和计算资源,提高了软件的性能。
- 定制化和个性化: 插件系统允许用户根据自己的需求来定制软件的功能和外观。用户可以选择安装和启用特定的插件,以满足个人喜好和工作流程。
总体来说,微内核架构为软件提供了灵活性、可扩展性和定制化的能力,使得软件更加强大和适应性更强。它是构建功能丰富、易于维护和具有强大生态系统软件的关键要素之一。
插件的实现方式
微内核架构尽管实现的方式不尽相同,但总的来说都包含下面几个步骤:
- 定义插件接口: 首先,需要定义插件与主程序之间的接口,包括插件的初始化方法、执行方法、事件监听等。这样可以确保插件与主程序之间的交互是规范的。
- 插件的加载方式: 确定插件的加载形式,比如通过npm包,通过文件,通过 git 仓库等等,好的插件的组织形式使整个系统足够灵活。设计好插件的加载时机,比如惰性加载,按依赖加载等,好的加载时机把控,可以让大型系统的性能得到提升。
- 插件注册和管理: 主程序需要提供插件注册和管理的功能,用于管理已加载的插件列表。当插件加载完成后,将其注册到主程序中,这样主程序就可以调用插件的能力。
- 事件通信机制: 主程序和插件之间需要建立事件通信机制,以便在需要的时候进行交互。可以使用自定义事件、发布订阅模式或观察者模式等方式来实现事件的监听和触发。
- 插件配置: 可以为插件提供一些配置选项,使得插件的行为可以根据用户需求进行定制化。
- 安全性考虑: 插件系统涉及动态加载代码,因此安全性是一个重要考虑因素。确保只加载受信任的插件,并对插件的代码进行安全性检查,以防止潜在的恶意代码注入。
业界关于插件设计模式有很多种,但是经过归纳总结,我们认为最常用的主要是以下三种插件模式:管道式、洋葱式和事件式,其中应用最为广泛的是事件式插件,以下也将分别从“特点”、“应用”两个方面介绍下这三种插件模式。
管道式插件
管道式插件(Pipeline Plugin)是常用的插件设计模式之一。它的主要目标是将处理流程分解为一系列独立的步骤,并允许开发者通过插件来扩展或修改这些步骤,从而实现更灵活和可维护的代码。
如上图所示,在管道式插件中,处理流程被表示为一条管道,数据从管道的一端输入,经过一系列步骤进行处理,最终在管道的另一端输出。每个处理步骤都由一个插件来实现,该插件负责执行特定的任务,并将处理后的数据传递给下一个插件。
优点
- 解耦性强,管道的每个环节之间相互独立,只处理特定的问题,可单独开发、测试和维护。
- 在输入输出标准化的情况,可以灵活组合插件,根据需求动态改变管道结构,实现数据处理流程的定制化和扩展性。
- 通过管道架构,可以方便进行数据缓存、异步处理和并发等优化,提高处理效率和系统性能。
缺点
- 管道的设计需要考虑插件之间的数据密切性和执行顺序,可能会增加开发难度和设计复杂度
- 如果不合理的设计管道流程,可能会导致数据的不完整性和不准确性,对系统造成影响
应用
管道式插件在许多领域都有应用,例如:
- 数据处理管道:在数据处理中,可以使用管道式插件来处理数据的转换、过滤、验证等任务,确保数据在不同步骤中按照预期进行处理。
- 自动化任务执行:自动化构建、自动化部署等任务的执行,比如CI/CD流水线,再比如云服务部署
- 前端构建工具:在前端构建工具中,如Gulp,管道式插件被广泛用于处理和转换源代码,例如编译、压缩、合并文件等。
以前端工具Gulp举例,以下是gulp的架构图:
综上,管道式插件是一种强大的设计模式,可以使代码更加灵活、可维护和可扩展,同时提供了一种模块化的方式来组织和处理复杂的任务。
洋葱式插件
洋葱式插件(Onion Architecture Plugin)也是常用的一类插件设计模式,它是从洋葱架构(Onion Architecture)演化而来的。
洋葱架构是一种用于构建可维护、灵活且可测试的应用程序的软件架构模式。在洋葱架构中,应用程序的核心逻辑位于内部,而外部依赖(如数据库、UI等)则位于外部。洋葱架构通过层层包裹的方式来表示不同的关注点,类似于洋葱的结构,因此得名。
洋葱式插件将洋葱架构与插件系统相结合,以实现可插拔的、可扩展的应用程序。在这种模式下,插件可以被动态地加载和卸载,而不会影响应用程序的核心逻辑,从而使得应用程序更具灵活性和可维护性。
优点
- 洋葱架构的层次分明, 洋葱式插件保留了洋葱架构的内部核心和外部依赖的层次结构。插件通常被视为外部依赖,而宿主应用程序的核心逻辑位于内部。
- 具备良好的重用性,洋葱架构中的各个层次和组件都可以独立地被重复利用,可以在不同的项目和场景中进行复用,提高了代码的可重用性。
举例:比如KOA中很多中间件具备良好的复用性(如koa-session),多个项目均可以引入使用
- 洋葱式插件允许插件在请求处理过程中先后执行,可以按需添加或删除插件,并且每个插件可以根据需要决定是否继续执行或终止执行,这使得洋葱式插件非常适合承当服务拦截器的角色
- 与管道式插件相比,洋葱式插件对数据干涉的时机更加完备,不仅仅可以对自身的数据输入环节进行干涉和处理,在数据输出环节还能对其他插件的输出进行干涉和处理
缺点
- 相比管道式插件复杂性更高,洋葱式插件模式需要插件之间的协作和数据传递,即处理输入流和处理输出流,在处理复杂逻辑时可能导致代码变得复杂难以理解。
- 洋葱架构中的层次嵌套可能会增加函数调用的次数和层次,进而导致一定的性能损耗。
应用
洋葱式插件模式在服务中间件中广泛应用:
洋葱式插件对数据流具备灵活和高权限的处理能力(能在输入输出两个环节来决定是否中断还是继续执行),非常符合服务中间件的使用场景
在前端领域,除了Koa、Express使用了洋葱式插件模式外,一些知名Nodejs框架也使用了洋葱式插件模式,比如Midway、Uni-request
以Koa为例,洋葱式插件运行阶段会经过3个环节:
- 任务注册:Koa通过use方法进行任务注册
- 任务编排:任务编排分为前置处理、核心逻辑、后置处理器
- 任务调度:Koa中的任务调度由Koa-compose来统一负责
以上是执行第一个中间件,触发dispatch(0),第一个中间件执行next()后,就会触发dispatch(1),进入第二个中间件,以此类推
事件式插件
事件式插件(Event-based Plugin)是插件设计模式中最灵活的一种,它基于事件驱动编程。在事件式插件中,主程序(或宿主应用程序)通过触发事件来通知插件执行相应的操作。插件系统允许插件注册特定事件的监听器,并在相应事件被触发时执行相应的功能。
优点
- 灵活度高,应用场景广
运行方式多样,事件类型多,十分灵活,能适应于各种场景。
如webpack当中,其通过 Tapable 实现了一种发布订阅者模式的插件机制,提供同步/异步钩子,串行/并行钩子,按照执行类型分为瀑布/保险/循环钩子,并且可以进行灵活组合来满足webpack编译打包的所有功能扩展需求
- 执行时机异步化,提升整体性能
因为事件式插件是基于发布订阅实现的,执行的时机异步化,非阻塞式地执行代码,有利于提升整体的性能
vscode在插件系统中,应对几十个插件的应用,也不会有太大的性能问题,不仅仅是因为事件触发之后才会初始化插件,也是得益于事件式插件带有的益处。
- 可插拔式的设计
事件式插件还有一个重要的特点,可插拔式的设计,使插件在添加或删除的时候,都不会影响主流程的执行
如Chrome 浏览器支持使用事件式插件的方式来扩展其功能,但是不会影响原有的浏览器功能的执行。
缺点
事件式插件虽然在插件注册和执行上具备非常大的灵活性,但是相应架构设计上会比管道式和洋葱式更为复杂,从而更容易引入未知问题。
事件式插件系统完全可以覆盖管道式插件系统的职能(使用串行的事件模式达到管道的效果),但是如果明确一个管道式的需求,则更建议使用管道式插件系统,因为管道式插件系统更为简单。
应用
事件式插件在前端领域有着广泛的应用,比如构建工具webpack,以及知名代码编辑器vscode,这里以vscode为例来讲述一下事件式插件的运行原理
这里主要研究客户端的插件系统运行流程,web端类似
整体的运行流程如下:
- 初始化插件系统
/**
vscode/src/vs/workbench/services/extensions/electron-sandbox/electronExtensionService.ts
*/
export abstract class ElectronExtensionService extends AbstractExtensionService implements IExtensionService {
(
@ILifecycleService lifecycleService: ILifecycleService,
) {
// 初始化插件系统服务
lifecycleService.when(LifecyclePhase.Ready).then(() => {
// reschedule to ensure this runs after restoring viewlets, panels, and editors
runWhenIdle(() => {
this._initialize();
}, 50 /*max delay*/);
});
}
}
/**
vscode/src/vs/workbench/services/extensions/electron-sandbox/electronExtensionService.ts
*/
export abstract class ElectronExtensionService extends AbstractExtensionService implements IExtensionService {
(
@ILifecycleService lifecycleService: ILifecycleService,
) {
// 初始化插件系统服务
lifecycleService.when(LifecyclePhase.Ready).then(() => {
// reschedule to ensure this runs after restoring viewlets, panels, and editors
runWhenIdle(() => {
this._initialize();
}, 50 /*max delay*/);
});
}
}
在客户端插件服务初始化时,所有的service被设置之后,会将生命周期转为Ready阶段,然后进行服务的初始化
- 扫描插件
在插件系统初始化的时候,通过CachedExtensionScanner模块扫描已经安装的插件,主要是解析出以下信息:
- 插件的名称
- 插件的版本
- 入口文件
- 与插件主流程相关的配置
{
"name": "ts2plantuml",
"version": "1.0.4",
"description": "",
"main": "./out/extension.js",
"activationEvents": [
"onCommand:ts2plantuml.explorer.preview"
],
"contributes": {
"commands": [
{
"command": "ts2plantuml.explorer.preview",
"title": "Preview Class Diagram",
"category": "TS2PLANTUML"
}
],
"menus": {
"explorer/context": [
{
"command": "ts2plantuml.explorer.preview",
"when": "resourceLangId == typescript"
}
],
"commandPalette": [
{
"command": "ts2plantuml.explorer.preview",
"when": "resourceLangId == typescript"
}
]
}
}
}
{
"name": "ts2plantuml",
"version": "1.0.4",
"description": "",
"main": "./out/extension.js",
"activationEvents": [
"onCommand:ts2plantuml.explorer.preview"
],
"contributes": {
"commands": [
{
"command": "ts2plantuml.explorer.preview",
"title": "Preview Class Diagram",
"category": "TS2PLANTUML"
}
],
"menus": {
"explorer/context": [
{
"command": "ts2plantuml.explorer.preview",
"when": "resourceLangId == typescript"
}
],
"commandPalette": [
{
"command": "ts2plantuml.explorer.preview",
"when": "resourceLangId == typescript"
}
]
}
}
}
- 注册插件
根据上诉扫描出来的配置,通过ExtensionDescriptionRegistry模块,对插件进行注册,首先通过commands字段对指令进行注册,同时声明激活插件的事件,以及各操作路径可以触发的指令
- .监听激活事件
通过监听激活事件,来激活插件,如上诉的配置中,当ts2plantuml.explorer.preview指令触发时,激活对应的插件
// vscode/src/vs/workbench/api/common/extHostExtensionService.ts
export abstract class AbstractExtHostExtensionService extends Disposable implements ExtHostExtensionServiceShape {
private _startExtensionHost(): Promise<void> {
if (this._started) {
throw new Error(`Extension host is already started!`);
}
this._started = true;
return this._readyToStartExtensionHost.wait()
.then(() => this._readyToRunExtensions.open())
// 监听激活事件
.then(() => this._handleEagerExtensions())
.then(() => {
// 激活插件
this._eagerExtensionsActivated.open();
this._logService.info(`Eager extensions activated`);
});
}
}
// vscode/src/vs/workbench/api/common/extHostExtensionService.ts
export abstract class AbstractExtHostExtensionService extends Disposable implements ExtHostExtensionServiceShape {
private _startExtensionHost(): Promise<void> {
if (this._started) {
throw new Error(`Extension host is already started!`);
}
this._started = true;
return this._readyToStartExtensionHost.wait()
.then(() => this._readyToRunExtensions.open())
// 监听激活事件
.then(() => this._handleEagerExtensions())
.then(() => {
// 激活插件
this._eagerExtensionsActivated.open();
this._logService.info(`Eager extensions activated`);
});
}
}
通过触发的激活事件,激活插件,同时将激活的事件做一个缓存,防止重复执行,这里即是监听激活事件里面的逻辑
// vscode/src/vs/workbench/api/common/extHostExtensionActivator.ts
export class ExtensionsActivator implements IDisposable {
public async activateByEvent(activationEvent: string, startup: boolean): Promise<void> {
if (this._alreadyActivatedEvents[activationEvent]) {
return;
}
const activateExtensions = this._registry.getExtensionDescriptionsForActivationEvent(activationEvent);
await this._activateExtensions(activateExtensions.map(e => ({
id: e.identifier,
reason: { startup, extensionId: e.identifier, activationEvent }
})));
this._alreadyActivatedEvents[activationEvent] = true;
}
}
// vscode/src/vs/workbench/api/common/extHostExtensionActivator.ts
export class ExtensionsActivator implements IDisposable {
public async activateByEvent(activationEvent: string, startup: boolean): Promise<void> {
if (this._alreadyActivatedEvents[activationEvent]) {
return;
}
const activateExtensions = this._registry.getExtensionDescriptionsForActivationEvent(activationEvent);
await this._activateExtensions(activateExtensions.map(e => ({
id: e.identifier,
reason: { startup, extensionId: e.identifier, activationEvent }
})));
this._alreadyActivatedEvents[activationEvent] = true;
}
}
- 加载插件,并执行
最后通过AbstractExtHostExtensionService模块加载插件,这里加载插件时,会对require进行拦截,对vscode进行代理,从而保证安全的执行环境,最后执行插件入口暴露出的activate函数进行激活插件