37.8 微内核与插件化

一句话

微内核(插件化)把系统切成 内核 + 插件:内核只做 “不会随业务变” 的三件事——插件管理、插件连接、插件通信,所有变化点都封进插件;它换扩展性的方式是 “加一个插件而不改内核”,代价是契约设计、版本兼容、进程内隔离这三笔账要自己付。

先分清三个混淆

混淆 现实
微内核 = 把服务拆小 不是。微内核是同一个应用内的插件架构(IDE、客户端、规则引擎),内核与插件一起打包交付;微服务拆的是部署单元与团队边界,见 30.1 微服务的定义与前提
微内核 = 依赖注入 不完全是。DI 只是 “插件连接” 的一种方式,微内核还必须有注册表(知道有哪些插件)与通信机制(插件之间不直接依赖)
插件化只为 “可插拔 UI” 不止。数据源适配器、支付渠道、保险险种核算、规则引擎、协议解析,都是 “同一套主流程 + 多变体” 的插件场景

三个关键点,缺一个就不算微内核

关键点 要解决的问题 常见实现 缺了会怎样
插件管理 内核怎么知道有哪些插件、何时加载、何时卸载 插件注册表:配置文件 / 代码注解 / 数据库;启动加载 or 按需加载 只能硬编码,等于没插件
插件连接 插件怎么接上内核 OSGi Bundle、依赖注入(Spring)、消息、RPC/HTTP 插件与内核强耦合,改契约就要全量重编
插件通信 插件之间怎么协作 内核提供服务注册表,插件发布/查找服务 插件互相 import,变成 “插件化外表下的单体”

🔑 插件通信是最容易被做错的一条:插件之间不允许直接依赖,必须通过内核中转(像 CPU 与内存通过主板总线通信)。一旦出现 import plugin_b,这套架构就已经退化成 “按目录分的单体”。

契约:微内核真正的设计产物

内核与插件之间只有一份契约(SPI):插件实现什么接口、暴露什么元数据、按什么版本。下面是一个 50 行能跑通的最小内核(Python 3.14 / macOS arm64 实测):

# core.py —— 内核只做:契约校验、加载、生命周期、服务查找、异常兜底
import importlib.util, os

SPI_VERSION = "notify/v1"

class Kernel:
    def __init__(self):
        self.plugins, self.services = {}, {}
    def load(self, path):
        name = os.path.splitext(os.path.basename(path))[0]
        spec = importlib.util.spec_from_file_location(name, path)
        mod = importlib.util.module_from_spec(spec); spec.loader.exec_module(mod)
        if mod.SPI != SPI_VERSION:                     # 契约校验:版本不符直接拒
            print(f"[kernel] reject {name}: SPI {mod.SPI} != {SPI_VERSION}")
            return False
        impl = mod.Plugin(); impl.start(self)
        self.plugins[name] = {"impl": impl}
        print(f"[kernel] load {name} v{mod.VERSION}")
        return True
    def unload(self, name):
        self.plugins.pop(name)["impl"].stop(self); print(f"[kernel] unload {name}")
    def publish(self, k, o): self.services[k] = o
    def lookup(self, k):     return self.services.get(k)
    def dispatch(self, name, **kw):
        try:                                            # 插件崩了不能带崩内核
            return self.plugins[name]["impl"].handle(self, **kw)
        except Exception as e:
            return f"degraded: {name} failed({type(e).__name__}), 走兜底"
# email.py —— 插件:声明契约版本,通过内核拿到别的插件(不 import)
VERSION = "1.2"
SPI = "notify/v1"
class Plugin:
    def start(self, k):
        k.publish("email", self)
        self.sms = k.lookup("sms")        # 插件通信只能走内核
        print(f"[email] started, 通过内核拿到 sms={'yes' if self.sms else 'no'}")
    def stop(self, k): print("[email] stopped")
    def handle(self, k, **kw): return f"email -> {kw['to']}: 欢迎注册"

真实运行输出(含一个契约不兼容的插件、一个会抛异常的插件):

[sms] started, 注册服务 sms
[kernel] load sms v1.0
[email] started, 通过内核拿到 sms=yes
[kernel] load email v1.2
[kernel] reject bad: SPI notify/v2 != notify/v1
[boom] started
[kernel] load boom v0.9
已加载: ['boom', 'email', 'sms']
调度 sms  : sms -> u1001: 验证码 1234
调度 boom : degraded: boom failed(TimeoutError), 走兜底
[kernel] unload boom
卸载 boom 后仍正常: email -> u1001: 欢迎注册

这 11 行输出把微内核的四件事全演示了:契约校验拒绝不兼容插件、服务查找、插件异常被内核兜住、热卸载不影响其他插件。

OSGi:微内核最完整的工业实现

OSGi 是 1999 年 3 月由 Sun、IBM、爱立信等成立的联盟制定的规范(全称 Open Services Gateway initiative),最初面向嵌入式设备,因为 Eclipse 3.0 采用它做插件体系而流行。它的三层结构与微内核三要素一一对应:

OSGi 层 对应微内核要素 做什么
Module 层 插件管理 每个插件是一个 Bundle(JAR + MANIFEST.MF),声明名字、版本、导入/导出的包
Lifecycle 层 插件连接 定义 Bundle 的安装/启动/停止/更新/卸载,插件实现 BundleActivator.start/stop
Service 层 插件通信 插件把服务注册到服务注册表,其他插件按接口名查找,不直接引用实现

MANIFEST.MF 就是那份契约的物理形态:

Bundle-ManifestVersion: 2
Bundle-Name: UserRegister
Bundle-SymbolicName: com.test.userregister
Bundle-Version: 1.0
Bundle-Activator: com.test.UserRegisterActivator
Import-Package: org.log4j;version="2.0"
Export-Package: com.test.userregister;version="1.0"

注意 Import-Package: org.log4j;version="2.0"——依赖带版本区间,这是 OSGi 能在运行时判定 “这个插件能不能装” 的根本。常见实现:Eclipse 用 Equinox,Apache Felix,Spring 早期有 Spring DM。

另一个典型实例是规则引擎:执行引擎是内核,业务规则是插件,改规则不改引擎——和保险险种核算(不同险种封装成插件)是同一套结构。

什么时候用,什么时候别用

场景 适合微内核? 理由
同一产品有多个变体(IDE、客户端、行业软件的不同客户版本) ✅ 内核稳定、变体多,插件是唯一不炸主干的扩展方式
规则频繁变化(风控规则、计价规则、保险核算) ✅ 规则是数据/插件,引擎不动
同一主流程接多家外部系统(支付渠道、数据源、协议适配) ✅ 每家一个适配器插件,加渠道不改主干
团队边界清晰、要独立发布与独立伸缩 ❌ 用微服务/独立服务,插件做不到独立部署与故障隔离
只是想把代码分模块 ❌ 用包/模块划分即可,引入插件框架是纯成本
插件需要独立扩缩容、独立技术栈、独立数据库 ❌ 这是服务边界,不是插件边界

🔑 判据只有一句:变化的是 “同一进程内的行为变体”,用微内核;变化的是 “部署单元与团队”,用服务化。

生产落地:指名 → 可抄 → 可验 → 可退

指名:Java 系用 OSGi(Equinox / Felix),轻量场景用 Spring 的 DI + 条件装配;Go 用接口 + 注册表(plugin 包在跨平台上有局限,生产多用编译期注册);Python 用 entry_points(pyproject.toml 里 [project.entry-points],pip 生态的标准做法);前端用 webpack/vite 插件;Nginx 用动态模块。

可抄(Python 生态最标准的插件注册,包安装即生效):

# pyproject.toml
[project.entry-points."myapp.notify"]
sms = "myapp_sms:SmsPlugin"
email = "myapp_email:EmailPlugin"
# 内核侧:按 entry point 组名发现插件,不写死任何插件名
from importlib.metadata import entry_points

def load_plugins():
    plugins = {}
    for ep in entry_points(group="myapp.notify"):
        plugins[ep.name] = ep.load()()      # 实例化
    return plugins

⚠️ 以上两段未在本机装包验证(本机没有对应包);逻辑与 importlib.metadata 的标准用法一致。上面那份可运行的 Python 内核演示用的是 “按路径加载”,生产里应优先用 entry_points 这种声明式发现。

可验:三个必须存在的检查。

检查 合格线
契约校验 版本不兼容的插件被拒绝加载并留下日志(不是加载后崩)
故障隔离 单插件抛异常/超时,内核返回降级结果,其他插件不受影响
热卸载 卸载一个插件后,内核和其他插件继续工作

可退:插件出问题的退路是 “关掉它”,所以内核必须支持按插件开关(配置中心控制)+ 降级路径(没有插件时的默认行为)。反过来,如果关掉某个插件系统就不可用,说明它其实不是插件,是内核的一部分。

反模式

反模式 后果
内核里写 if type == "sms" 这类业务分支 加一个变体就改内核,插件化名存实亡
插件之间直接 import 退化成按目录分的单体,卸载一个就牵连一串
契约没有版本 老插件加载到新内核,运行到某个分支才炸
插件无超时、无异常兜底 一个插件卡住,整个进程(内核 + 所有插件)一起挂
用微内核做跨团队拆分 拿不到独立部署、独立扩容,反而多了一层框架
插件里藏状态 热卸载/热更新后状态丢失,行为不可预期
插件注册表写死在代码里 等于编译期硬编码,失去 “按需加载” 的意义
没有 “默认实现” 插件缺失时系统直接不可用,退路为零

三条铁律

🔑 内核只放 “不会随业务变” 的东西:加载、契约、通信、路由;任何一个业务分支写进内核,都是在给未来挖坑。

🔑 插件之间只能通过内核通信:出现插件间直接依赖,就说明边界划错了。

🔑 契约必须带版本,且内核要能拒:能拒绝不兼容插件,比 “加载成功但运行错乱” 便宜一个数量级。

自检

Q1 微内核与微服务最本质的区别是什么?

▸ 答案:边界性质不同。微内核切的是 “同一进程/同一产品内的变化点”,插件与内核一起打包、共享进程与内存;微服务切的是 “部署单元与团队边界”,独立进程、独立发布、独立数据库。所以微内核拿不到独立扩缩容与故障隔离,微服务拿不到 “一个进程内零网络开销的扩展”。

Q2 插件管理、插件连接、插件通信分别解决什么问题?缺一个会怎样?

▸ 答案:管理 = 内核怎么知道有哪些插件(注册表);连接 = 插件怎么接上内核(OSGi Bundle / DI / RPC);通信 = 插件之间怎么协作(服务注册表)。缺管理只能硬编码;缺连接则契约变更要全量重编;缺通信则插件互相 import,退化成单体。

Q3 为什么插件之间不允许直接依赖?

▸ 答案:直接依赖会把 “插件” 变成 “模块”:卸载 A 会牵连 B,A 的版本升级会迫使 B 重编,故障也会跨插件传播。通过内核中转(发布/查找服务)才能保持 “任意插件可插拔” 的性质——这也是 OSGi Service 层存在的理由。

Q4 OSGi 的三层分别对应微内核的哪个要素?

▸ 答案:Module 层(Bundle + MANIFEST.MF)对应插件管理;Lifecycle 层(BundleActivator.start/stop、安装/更新/卸载)对应插件连接;Service 层(服务注册表、按接口查找)对应插件通信。

Q5 Import-Package: org.log4j;version="2.0" 这行的作用是什么?

▸ 答案:声明带版本的依赖契约。OSGi 在安装/解析阶段就能判断 “当前环境是否满足这个版本区间”,不满足直接拒绝安装,而不是等到运行时抛 NoClassDefFoundError。这是 OSGi 相比 “手动 classpath” 的核心价值之一。

Q6 一家公司要给不同客户交付不同功能组合的同一套系统,该用微内核还是微服务?

▸ 答案:用微内核。这是典型的 “同一产品多变体” 场景:内核稳定、差异封装成插件,按客户授权加载不同插件;微服务解决的是 “独立部署与团队边界”,在这里只会把交付复杂度抬高(每个客户一套服务编排)。反例:如果不同客户需要独立的数据隔离与独立扩容,那就该走多租户 + 服务化。

Q7 微内核最大的运行风险是什么?怎么兜?

▸ 答案:没有进程隔离——插件与内核共享同一个进程,一个插件的死循环、内存泄漏、崩溃会带走整个应用。兜底三件套:① 内核调用插件必须带超时 + 异常捕获(本机演示里 dispatch 的那个 try);② 重活/不可信插件外置成独立服务(退化成本文 “什么时候别用” 的场景);③ 插件开关 + 默认实现,随时能关掉。

关联