模块系统与启动流程

这一篇解决什么
OAP 后端有几十个功能模块(核心分析、存储、集群、告警、查询、各种 receiver 插件……),它们怎么拼成一个可运行的整体?答案是一套自研的"模块系统":ModuleDefine(模块契约)+ ModuleProvider(模块实现)+ 标准 Java SPI 发现 + 拓扑排序启动。这篇拆解这套骨架,然后跟一遍 OAP 的启动 main 流程,最后看 CoreModule 这个"大脑"里装了什么。理解了这一篇,后面所有模块的代码都能对号入座。


为什么自研模块系统

SkyWalking 没用 Spring,而是自己写了一套极简的模块框架。原因是三个工程诉求 Spring 不好满足:

  • 解耦:每个模块独立,对外只暴露 Service 接口,实现可替换。存储可以是 BanyanDB、ES、JDBC,配置一切换就行,调用方代码不动。
  • 可插拔:同一模块可以有多种实现(provider),运行时只装载配置选中的那一个。这让 OAP 能按部署形态裁剪——单机用 H2,集群用 BanyanDB,互不干扰。
  • 有序启动:模块之间有依赖,需要拓扑排序后按顺序启动,还要能检测循环依赖。Spring 的 @DependsOn 能表达依赖但不会在启动期就帮你拓扑排序和报循环错。

整套代码在 oap-server/server-library/library-module/src/main/java/org/apache/skywalking/oap/server/library/module/,只有约 10 个类,非常精简。


核心抽象:四个角色

图1

三个角色一句话

  • Service:空接口(Service.java:26),是所有跨模块能力的根。模块对外承诺"我能提供这些 Service"。
  • ModuleDefine:模块的"契约",声明 name()services()(对外暴露哪些 Service 接口)。抽象类,子类如 CoreModule
  • ModuleProvider:模块的"实现",一个模块可以有多种 provider,但运行时只装一个。生命周期分 prepare() → start() → notifyAfterCompleted() 三段。
  • ModuleManager:全局唯一总管,负责 SPI 加载、拓扑排序、驱动启动,并提供 find(moduleName) 给运行期跨模块查 Service。

ModuleDefine · 契约

oap-server/server-library/library-module/src/main/java/org/apache/skywalking/oap/server/library/module/ModuleDefine.java:31

关键方法:

  • name():46)返回模块名,如 "core""storage"
  • services():53)是抽象方法,子类必须重写,返回这个模块对外暴露的所有 Service 接口的 Class 数组。这是模块的"对外契约清单"。
  • prepare(...):62)是模块的 prepare 阶段入口:遍历所有候选 Provider,选出与配置匹配且 module() 等于当前模块的那个 Provider 装载进来。一个模块只能装一个 Provider,否则抛 DuplicateProviderException:79),然后创建配置 Bean、调用 provider.prepare()

对新手来说要记住:ModuleDefine 是接口/契约,ModuleProvider 是实现。比如 CoreModule 只声明 services(),真正干活的是 CoreModuleProvider


ModuleProvider · 实现

oap-server/server-library/library-module/src/main/java/org/apache/skywalking/oap/server/library/module/ModuleProvider.java:31

生命周期三段是新手最需要理解的核心:

图2

三阶段能干什么、不能干什么

  • prepare():84):只做"不依赖其他模块"的初始化,主要是 registerServiceImplementation(...) 把 Service 实现注册进来。此时不能访问别的模块(find 会抛错)。
  • start():89):此时所有模块都已 prepare 完,可以跨模块互操作,比如 getManager().find(CoreModule.NAME).provider().getService(XxxService.class)
  • notifyAfterCompleted():94):所有模块都 start 完才执行,用于启动定时器、gRPC/HTTP server 等"最后才能做的事"。

声明方法:

  • name():50)Provider 名,如 "default",对应 application.yml 里 selector 选中的名字。
  • module():55)声明本 Provider 属于哪个 ModuleDefine
  • newConfigCreator():61)创建配置 Bean。
  • requiredModules():99)声明依赖哪些模块——拓扑排序就靠它。

registerServiceImplementation(...):105)是 final 方法,把 Service 实现塞进内部 services Map。requiredCheck(...):120)在 start 阶段校验:注册的 Service 数量必须和 ModuleDefine.services() 声明的完全一致,多了少了都报错。


模块之间怎么注入依赖

不是 Spring 那种字段注入,而是服务定位器(Service Locator)模式

图3

每个 Provider 持有 getManager()ModuleProvider.java:43),需要别的模块时直接:

getManager().find(CoreModule.NAME).provider().getService(ConfigService.class)

查找链是:ModuleManager.find("core")ModuleDefine(同时也是 ModuleProviderHolder)→ .provider()ModuleProvider(同时也是 ModuleServiceHolder)→ .getService(XxxService.class)

这套 ModuleDefineHolder 接口被传给几乎所有 Worker,让 Worker 也能跨模块找 Service。所以"依赖注入"在 SkyWalking 里实质是"持有 ModuleManager 引用,按名 + 接口查找"。

为什么用服务定位器而不是 Spring 的字段注入?因为后者要在构造期就拿到依赖,而 OAP 的模块是按拓扑序逐个启动的,先启动的模块此刻后面的还没就绪。服务定位器把"找依赖"推迟到运行时调用那一刻,自然避开了这个时序问题。代价是调用方要主动 find,没有 Spring @Autowired 那种声明式简洁——但对一个要按 selector 动态裁剪 provider 的系统,这个代价是值得的。


SPI 机制:标准 Java SPI

模块系统用的是标准 JDK java.util.ServiceLoader,不是自定义 SPI。

证据在 oap-server/server-library/library-module/src/main/java/org/apache/skywalking/oap/server/library/module/ModuleManager.java:50-51

ServiceLoader.load(ModuleDefine.class);
ServiceLoader.load(ModuleProvider.class);

注册文件在各 jar 的 META-INF/services/ 下,文件名即接口全限定名。

server-core 的 SPI 注册
oap-server/server-core/src/main/resources/META-INF/services/org.apache.skywalking.oap.server.library.module.ModuleDefine 内容列出了 StorageModuleClusterModuleCoreModuleQueryModuleAlarmModuleExporterModule 六个模块定义类。

同目录的 ...ModuleProvider 文件只有一行:org.apache.skywalking.oap.server.core.CoreModuleProvider

这意味着 server-core 这个 jar 声明了自己实现了哪些模块(6 个 Define),但只自带了 1 个 Provider 实现(CoreModuleProvider)。其他模块(Storage/Cluster/Query/Alarm/Exporter)的 Provider 由各自的插件 jar 提供,靠 selector 决定装哪个。

模块发现的两种顺序不要混

  • 发现顺序:由 ServiceLoader 遍历 META-INF/services 决定,这是无序的
  • 启动顺序:由 BootstrapFlow.makeSequence 基于 requiredModules() 做拓扑排序决定,与发现顺序无关

模块之间正确的先后关系靠依赖图保证,不靠配置文件里写的顺序。


启动流程:从 main 到模块全就绪

入口极简。oap-server/server-starter/src/main/java/org/apache/skywalking/oap/server/starter/OAPServerStartUp.java:22main 只调 OAPServerBootstrap.start()

真正的活在 oap-server/server-starter/src/main/java/org/apache/skywalking/oap/server/starter/OAPServerBootstrap.java:38start()

完整启动时序

图4

OAPServerBootstrap.start()OAPServerBootstrap.java:38-69)的步骤:

  1. new ModuleManager("Apache SkyWalking OAP"):39)创建内核。
  2. 读系统属性 modeRunningMode.setMode(mode):42-43)。
  3. new ApplicationConfigLoader(bootingParameters):45),写入 Running Mode、Version(:47-48)。
  4. configLoader.load() 得到 ApplicationConfiguration:51)。
  5. manager.init(applicationConfiguration):52)——这是整个模块启动流程。
  6. 启动成功后 ServerStatusService.bootedNow(...):54-57)标记启动完成时间,这对 MetricsPersistentWorker 的缓存策略很关键。
  7. RunningMode.isInitMode():59)则只做初始化(建表)然后 System.exit(0):61)。
  8. 任何异常 log.errorSystem.exit(1):63-65)。
  9. finally 块打印 bootingParameters 表格(:67)。

RunningMode · 运行模式

oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/RunningMode.java:26

通过 -Dmode=init / -Dmode=no-init 控制:

  • isInitMode():44):init 模式只做存储初始化(建表/建 measure),建完即退出,用于首次部署或 schema 升级。
  • isNoInitMode():53):no-init 模式启动但不做存储初始化。

CoreModuleProvider.notifyAfterCompleted()CoreModuleProvider.java:489)处生效:if (!RunningMode.isInitMode()) 才真正 grpcServer.start() 等网络服务。


application.yml 怎么解析

配置加载在 oap-server/server-starter/src/main/java/org/apache/skywalking/oap/server/starter/config/

ApplicationConfigLoader.load()ApplicationConfigLoader.java:64)两步:

  1. loadConfig:72):用 SnakeYAML 把 classpath 下的 application.yml 读成 Map<String, Map<String, Object>>,结构是 模块名 → (provider 名 → 配置)
  2. overrideConfigBySystemEnv:122):遍历 System.getProperties(),按 moduleName.providerName.settingKey 格式覆盖 YAML 配置(overrideModuleSettings :161),支持 int/String/long/boolean 类型转换。

selector 机制
selectConfigApplicationConfigLoader.java:128-159)是关键:每个模块下有 selector 字段,值为某个 provider 名(或 - 表示禁用)。

  • 解析 selector(支持 ${...} 占位符替换系统属性,:138)。
  • removeIf 把不等于 selector 值的所有 provider 配置删掉(:141)。
  • 删完为空且 selector 不是 -,抛 ProviderNotFoundException:148)。
  • selector 是 -,直接把这个模块从配置里移除(:156)——这就是"禁用某模块"的官方写法。

BootstrapFlow · 拓扑排序

BootstrapFlow 是 package-private 类(BootstrapFlow.java:28),只在 ModuleManager.init 内部用。

makeSequenceBootstrapFlow.java:56-120)的核心:

图5

反复把"所有依赖都已在序列中"的 Provider 加入 startupSequence,直到全部排完。若一轮下来没有任何 Provider 能排进去(说明互相依赖),抛 CycleDependencyException:114)。还会校验依赖的模块是否真的加载了(:62-68,缺失抛 ModuleNotFoundException)。

排好序后,start:40-48)按拓扑序对每个 Provider 先 requiredCheck(校验 Service 注册齐全),再 provider.start()notifyAfterCompleted:50-54)按同一序调每个 Provider 的收尾方法。


CoreModule · 大脑里装了什么

oap-server/server-core/src/main/java/org/apache/skywalking/oap/server/core/CoreModule.java:80 是整个 OAP 的"大脑",services():88)列出约 60 个 Service 接口。

图6

按职责分组(CoreModule.java 行号):

  • 配置类ConfigService:90)、DownSamplingConfigService:92)、NamingControl:93,命名长度控制)、EndpointNameGroupService:118)。
  • 流处理骨架IWorkerInstanceGetter/IWorkerInstanceSetter:99-100,Worker 注册与查找,用于集群 RPC 路由)、MeterSystem:102,MAL 入口)。
  • 服务端接口GRPCHandlerRegister/HTTPHandlerRegisteraddServerInterface :173,注册 gRPC/HTTP handler)。
  • 接收入口SourceReceiveraddReceiverInterface :191,所有 source 数据的统一入口)。
  • 内部服务ModelRegistryIModelManagerModelManipulator(存储模型注册表)、RemoteClientManagerRemoteSenderService(集群消息分发)。
  • 缓存NetworkAddressAliasCacheProfileTaskCache/AsyncProfilerTaskCache/PprofTaskCache:136-148)。
  • 查询服务(面向 UI):TopologyQueryServiceMetricsQueryServiceTraceQueryServiceLogQueryServiceMetadataQueryServiceAggregationQueryServiceAlarmQueryServiceTopNRecordsQueryServiceBrowserLogQueryServiceEventQueryServiceTagAutoCompleteQueryServiceRecordQueryServiceHierarchyQueryService 等。
  • OAL runtimeOALEngineLoaderService:151)。
  • 其他SpanListenerManager:97,trace span 监听器)、CommandService:116)、HierarchyService:117)。

这些 Service 的默认实现全部在 CoreModuleProvider.prepare()CoreModuleProvider.java:185)里通过 registerServiceImplementation 注册。


CoreModuleProvider 的三阶段实样

CoreModuleProvider 为真实样本,看三阶段各做什么:

prepare / start / notifyAfterCompleted 的真实分工
prepare()CoreModuleProvider.java:185):注册全部 Service 实现、建 GRPCServer/HTTPServer不启动、建 RemoteClientManager、扫描 Scope 注解、设置 StreamProcessor 参数——全是"自我准备"。

start()CoreModuleProvider.java:417):把 RemoteServiceHandler 加进 gRPC、调 receiver.scan() 扫描 Dispatcher、启动 ClusterCoordinator、注册动态配置 watcher——开始跨模块协作。

notifyAfterCompleted()CoreModuleProvider.java:481):grpcServer.start()httpServer.start()remoteClientManager.start()PersistenceTimer.INSTANCE.start(...)DataTTLKeeperTimerCacheUpdateTimer——真正对外服务。

这个三段式分工是 SkyWalking 所有 Provider 的通用模式:prepare 备料、start 接线、notifyAfterCompleted 通电。


异常体系

启动期的校验失败都是可读异常,方便定位:

异常 触发场景
ProviderNotFoundException selector 指定的 provider 不存在
DuplicateProviderException 一个模块装了多个 Provider
ServiceNotProvidedException Provider 注册的 Service 数与声明不符
ModuleNotFoundException requiredModules 声明的依赖没加载
CycleDependencyException 模块间循环依赖

关键文件速查

子系统 关键类 路径
模块系统 ModuleDefine oap-server/server-library/library-module/.../module/ModuleDefine.java
ModuleProvider 同目录 ModuleProvider.java
ModuleManager 同目录 ModuleManager.java
BootstrapFlow 同目录 BootstrapFlow.java
Service/ModuleConfig 同目录 Service.java / ModuleConfig.java
启动入口 OAPServerStartUp oap-server/server-starter/.../starter/OAPServerStartUp.java
OAPServerBootstrap 同目录 OAPServerBootstrap.java
ApplicationConfigLoader oap-server/server-starter/.../starter/config/ApplicationConfigLoader.java
RunningMode oap-server/server-core/.../core/RunningMode.java
server-core CoreModule oap-server/server-core/.../core/CoreModule.java
CoreModuleProvider 同目录 CoreModuleProvider.java
SPI 注册文件 oap-server/server-core/src/main/resources/META-INF/services/org.apache.skywalking.oap.server.library.module.{ModuleDefine,ModuleProvider}

接下来

骨架搞清楚了,下一步看数据怎么进来:02-数据采集层-Receiver。

贯穿全篇的心智模型
ModuleDefine 想成"插座规格"(声明能插什么),ModuleProvider 想成"插头"(具体实现),ModuleManager 想成"配电箱"(管哪些插座通电、按什么顺序通电)。Service 就是插座上流出来的电。这个比喻贯穿后面所有模块——你看到的每一个插件(storage-banyandb、cluster-zookeeper、receiver-trace)都是一个插头,插进对应的插座。