Backstage 后端系统构建指南:从最小后端到多后端拆分与启动容错配置
【免费下载链接】backstageBackstage is an open framework for building developer portals项目地址: https://gitcode.com/GitHub_Trending/ba/backstage
本文以 Backstage 官方文档《Building Backends》为核心,系统讲解如何使用新后端系统(New Backend System)创建、装配与定制自己的 Backstage 后端:包括createBackend的轻量初始化、插件/模块/服务的装配方式、通过配置与服务工厂完成定制、将单体后端拆分为多套独立部署,以及用backend.startup配置控制插件启动失败时的容错行为。读完本文,你将掌握一套可复制、可运行的后端搭建与运维方案,并能结合仓库源码理解其底层实现。
整体概览:一个最小后端由什么构成
Backstage 的后端本质上是一个轻量级的 Node.js 包:只需要一个带package.json的目录和一个src/index.ts入口文件,不考虑周围的工具链与文档的话,仅此而已。这个包通常放在 Backstage monorepo 的packages/backend目录下,但位置完全由你决定。任何通过@backstage/create-app创建的项目都会自带这样一个后端包,因此绝大多数情况下你不需要从零手写它。
当你用@backstage/create-app创建新项目时,得到的后端src/index.ts大致长这样:
import { createBackend } from '@backstage/backend-defaults'; // 以下示例中省略此行 const backend = createBackend(); backend.add(import('@backstage/plugin-app-backend')); backend.add(import('@backstage/plugin-catalog-backend')); backend.add(import('@backstage/plugin-scaffolder-backend')); backend.add( import('@backstage/plugin-catalog-backend-module-scaffolder-entity-model'), ); backend.start();初始模板里还会有更多插件和模块,但整体结构是一致的。这段代码做的事情可以拆成三步:
- 调用
createBackend创建后端实例。它负责把喂给后端的各种特性(features)装配在一起,并为插件提供所有核心服务的默认实现。 - 通过
backend.add(...)安装特性。特性分三类:**插件(plugins)**是独立的功能单元;**模块(modules)**用于增强某个已有插件或模块;**服务(services)**则用于更深层的定制,可以覆盖默认行为。需要注意:每个模块只能瞄准一个插件,而且该插件必须存在于同一个后端中。 - 调用
backend.start()启动后端。此时后端才开始真正做初始化工作——创建实例本身不会执行任何实际操作,所有工作都被推迟到start()调用时进行。
关于createBackend的底层实现,可以到 packages/backend-defaults/src/CreateBackend.ts 中查看:它内部调用createSpecializedBackend并传入一长串defaultServiceFactories(auditor、auth、cache、database、discovery、logger、scheduler 等约 27 个服务工厂)。也就是说,createBackend是"开箱即用(batteries included)"的高层入口,而createSpecializedBackend是更底层、不预装任何服务的原始入口。更详细的说明可参考后端实例架构文档。
如果你已经有一个尚未迁移到新后端系统的旧后端,可参考迁移指南,其中讲解了如何把旧式的makeCreateEnv+plugins/*.ts结构逐步收敛为上述极简形态,以及如何用legacyPlugin桥接旧式插件。
仓库实例:一个真实后端的装配清单
理论之外,本仓库自带的示例后端 packages/backend/src/index.ts 就是一份极佳的"装配清单"参考。它在createBackend()之后通过backend.add(...)安装了 Auth、App、Catalog、Events、Kubernetes、Permissions、Proxy、Scaffolder、Search、TechDocs、Signals、Notifications、MCP Actions、User Settings 等大量后端插件及其模块。
值得特别注意的是其中使用的一个进阶特性——特性加载器(Feature Loader):
import { coreServices, createBackendFeatureLoader, } from '@backstage/backend-plugin-api'; const searchLoader = createBackendFeatureLoader({ deps: { config: coreServices.rootConfig, }, *loader({ config }) { yield import('@backstage/plugin-search-backend'); yield import('@backstage/plugin-search-backend-module-catalog'); yield import('@backstage/plugin-search-backend-module-explore'); yield import('@backstage/plugin-search-backend-module-techdocs'); if (config.has('search.elasticsearch')) { yield import('@backstage/plugin-search-backend-module-elasticsearch'); } }, }); backend.add(searchLoader);它用生成器函数按需批量加载一组特性,并且可以在deps中声明对 root 作用域服务的依赖——这里就通过coreServices.rootConfig读取配置,当检测到search.elasticsearch配置时才额外加载 Elasticsearch 搜索模块。这展示了"配置驱动装配"的思路:后端可以依据静态配置动态决定要安装哪些功能。
配套的 packages/backend/package.json 则给出了后端包的典型定义:"role": "backend"、"main": "dist/index.cjs.js"、构建/启动脚本(backstage-cli package build、backstage-cli package start),以及大量@backstage/plugin-*-backend与@backstage/plugin-*-backend-module-*依赖。这说明一个后端包的依赖天然分为"插件本体"与"插件模块"两类,二者都在index.ts中通过backend.add装配。
定制方式一:静态配置(Configuration)
除安装现成插件与模块外,后端定制有几种途径。最易上手的是静态配置,详细写法可参考配置编写文档。后端本身的许多行为、以及大量插件/模块的行为都可以通过配置调整,具体可配置项需要查阅各插件/模块自己的文档;同时建议查阅核心服务文档,其中也覆盖了核心服务的配置方法。典型配置文件如仓库根目录的 app-config.yaml,后端在启动时会读取并下发到各插件。
定制方式二:覆盖核心服务(Services)
服务是另一个重要的定制切入点,它允许对后端做更深、更广的定制。服务与前端系统中的 Utility APIs 类似,都是通过依赖注入把通用能力提供给插件和模块,更深入的解释见服务架构文档。
所有后端都必须安装一组核心服务,负责日志、数据库访问、HTTP 服务等基础能力。好消息是,当你使用@backstage/backend-defaults提供的createBackend时,这组服务已全部默认安装。这些服务全部可以被你自己的实现替换。最省事的替换方式是"沿用现有实现、只加选项"——许多核心服务支持这种用法(并非全部,因为有些服务没有有意义的选项)。
例如,想要定制核心配置服务以启用远程配置加载,可以这样写:
import { rootConfigServiceFactory } from '@backstage/backend-app-api'; const backend = createBackend(); backend.add( rootConfigServiceFactory({ remote: { reloadIntervalSeconds: 60 }, }), );这样配置目标中就可以传入 URL,后端会每 60 秒轮询一次该 URL 获取配置变更。
这里有一个例外:框架内置的PluginMetadataService(插件元数据服务)由框架直接提供,无法被覆盖。它是插件作用域服务,向服务实例提供"当前正在为哪个插件创建实例"的信息,是插件级定制的基石——例如默认日志服务正是通过它给每条日志打上插件 ID 标签。
定制方式三:完全自定义服务工厂(Custom Service Implementations)
覆盖服务时你并不局限于现有实现,完全可以提供自己的服务工厂(service factory),从而用完全自定义的实现全局覆盖某个服务,或在现有实现基础上叠加额外逻辑。
覆盖服务的写法与上文相同(放进services选项),但这次需要用createServiceFactory创建工厂。例如,用自定义实现替换默认的LoggerService:
const backend = createBackend(); backend.add( createServiceFactory({ service: coreServices.logger, deps: { rootLogger: coreServices.rootLogger, plugin: coreServices.pluginMetadata, config: coreServices.rootConfig, }, factory({ rootLogger, plugin, config }) { const labels = readCustomLogLabelsForPlugin(config, plugin); // 自定义逻辑 return rootLogger.child(labels); }, }), );这个例子背后涉及两个容易混淆的服务:
LoggerService(coreServices.logger):插件作用域服务,负责为每个插件创建带插件专属上下文的日志实例;默认实现会给日志附加一个包含插件 ID 的plugin标签。RootLoggerService(coreServices.rootLogger):root 作用域服务,是真正的日志实现本体。上面的自定义实现就是从配置中读出额外的标签并叠加到rootLogger的子 logger 上。
这恰好引出**服务作用域(service scope)**的概念:服务只有两种作用域——'plugin'(默认)和'root'。插件作用域服务会为每个依赖它的插件各创建一个实例,实现插件间的一定隔离;root 作用域服务则在所有插件与服务间共享单个实例,且无论是否有插件依赖它都会被初始化,适合承载与具体插件无关的后端级关注点。root 作用域服务只能依赖其他 root 作用域服务,而插件作用域服务可以依赖两者。从源码结构看,这类"成对"服务(rootLogger/logger、rootConfig/config等)的划分正是为了既让 root 服务也能访问日志/配置,又鼓励插件优先使用插件作用域版本。完整的服务接口、引用与工厂规范可阅读服务架构文档。
拆分为多个后端(Split Into Multiple Backends)
更高阶的部署形态是把后端插件拆分到多个后端部署中。拆分的好处(独立扩缩容、性能与安全隔离等)在部署扩展文档与威胁模型中有详细说明,这里聚焦"怎么拆"。
创建独立后端需要额外新建一个后端包,它会与现有后端分别构建、分别部署。目前yarn new还没有提供后端模板,最快的做法是复制现有后端包再修改。命名取决于你的拆分方式,这里以简单后缀为例,目录结构可能变成:
packages/ backend-a/ src/ index.ts package.json <- "name": "backend-a" backend-b/ src/ index.ts package.json <- "name": "backend-b"然后裁剪各自的src/index.ts,只保留想归属该后端的插件与模块。例如要把 Scaffolder 插件拆出去,backend-a可能是:
const backend = createBackend(); backend.add(import('@backstage/plugin-app-backend')); backend.add(import('@backstage/plugin-catalog-backend')); backend.add( import('@backstage/plugin-catalog-backend-module-scaffolder-entity-model'), ); backend.start();而backend-b则是:
const backend = createBackend(); backend.add(import('@backstage/plugin-scaffolder-backend')); backend.start();注意backend-b的package.json也要同步清理依赖,移除不再需要的插件包。
把后端拆成两套独立部署后,剩下的关键问题是让它们能互相通信——这也是最繁琐的部分,因为 Backstage 目前没有现成的开箱即用方案。你需要:
- 为两个后端手动配置自定义的
DiscoveryService实现,让它们能返回彼此正确的 URL; - 在前端提供自定义的
DiscoveryApi实现,除非你通过一个负责路由的反向代理把两个后端统一暴露出来。
多后端部署架构示例
下面是一个更复杂的示例:三套后端部署,每套承载各自的插件与模块;前端与各后端实例之间有一个反向代理负责把流量路由到正确的实例。作为加固选项,该代理也可以配置为带认证的反向代理,拒绝未认证用户访问后端实例。
在这个示例中,Catalog 与 Search 插件被拆到一套后端部署,代理把/api/catalog/和/api/search/的流量全部路由到该实例。通过这种分离,这两个插件可以独立扩缩容与部署,并且在性能与安全上相互隔离;同理,TechDocs 与 Scaffolder 也被单独拆出;其余流量则路由到承载 App、Auth、Proxy 插件的实例。图中还可以看到:每个插件拥有自己逻辑上的数据库,但通常共享同一个数据库管理系统(DBMS)实例——这当然不是硬性要求,你可以按需进一步拆分或合并数据库。
启动配置:控制插件启动失败时的行为
backend.startup配置块用于控制后端在插件或插件模块启动失败时的行为。默认情况下,任何插件或模块的启动失败都是致命错误,会导致后端中止启动。该配置允许你把特定插件/模块设为可选,或翻转全局默认值,让所有插件/模块默认可选、只有显式要求的才必须成功。
插件启动失败处理
默认情况下,插件启动失败会让后端中止。你可以按插件用onPluginBootFailure: continue改变这一行为:
backend: startup: plugins: catalog: onPluginBootFailure: continue配置后,如果catalog插件在启动时崩溃,后端会记录错误并继续启动其余插件。这在排查与数据相关的问题时非常有用——可以让一个会崩溃的插件保持安装状态,同时让后端其余部分继续对外服务。
插件模块启动失败处理
对单个插件模块可以用onPluginModuleBootFailure做同样的控制:
backend: startup: plugins: catalog: modules: github: # moduleId,即 createBackendModule({ moduleId: '...' }) 中声明的 ID onPluginModuleBootFailure: continue这允许github目录模块失败而不拖垮catalog插件或后端其余部分。注意modules下的键是createBackendModule中声明的moduleId,而不是插件名或实体提供者(entity provider)名。
设置全局默认值
与其逐个把插件设为continue,不如翻转全局默认值,让所有插件失败时都继续,只要求特定插件必须成功:
backend: startup: default: onPluginBootFailure: continue plugins: auth: onPluginBootFailure: abort这个示例中,除auth被显式设为abort(必须成功启动)外,其余插件全部可选。default机制对模块同样生效(通过onPluginModuleBootFailure):
backend: startup: default: onPluginModuleBootFailure: continue plugins: catalog: modules: github: onPluginModuleBootFailure: abort完整配置参考
backend: startup: # 未按插件/模块单独指定时应用的全局默认值 default: # 默认值为 'abort'。设为 'continue' 可使所有插件默认可选。 onPluginBootFailure: abort # 或 continue # 默认值为 'abort'。设为 'continue' 可使所有插件模块默认可选。 onPluginModuleBootFailure: abort # 或 continue # 按插件、按模块的覆盖配置 plugins: <pluginId>: # 覆盖该插件默认的启动失败行为。 onPluginBootFailure: abort # 或 continue modules: <moduleId>: # 覆盖该插件模块默认的启动失败行为。 onPluginModuleBootFailure: abort # 或 continue这一配置的底层解析逻辑可以在 packages/backend-app-api/src/wiring/createAllowBootFailurePredicate.ts 中看到:它启动时一次性读取backend.startup.default.onPluginBootFailure、backend.startup.default.onPluginModuleBootFailure的默认值,再读取backend.startup.plugins下按插件、按模块的覆盖项,最后返回一个(pluginId, moduleId?) => boolean的谓词函数——返回true表示允许该插件/模块启动失败(即配置为continue),否则按abort处理。这也印证了配置键的取值仅支持'abort'与'continue'两种,且模块级配置一定嵌套在对应插件之下。
值得一提的是,Backend.start()返回一个BackendStartupResult,包含所有插件与模块详细的成功/失败状态和耗时信息;启动失败时会抛出BackendStartupError,其中携带完整的启动结果,便于诊断究竟是哪个插件或模块失败。这对构建额外的监控或调试工具很有价值,详见后端实例架构文档。
小结与延伸阅读
搭建一个 Backstage 后端可以概括为三步:createBackend()创建实例 →backend.add(...)装配插件/模块/服务工厂 →backend.start()启动。在此基础上,你可以通过静态配置、给现有服务工厂传选项、或编写完全自定义的服务工厂三种粒度进行定制;当单体后端不再满足部署需求时,可以按插件拆分出多套独立后端并用自定义DiscoveryService/反向代理打通互访;最后用backend.startup配置为启动阶段引入容错能力。
继续深入可以参考仓库内以下资料:
- 后端实例(Backend)架构文档:
createBackend/createSpecializedBackend的关系、BackendStartupResult与BackendStartupError - 服务(Services)架构文档:服务引用、服务工厂、作用域、Multiton、默认工厂等完整机制
- 核心服务索引:Auth、Cache、Database、Discovery、Logger、Scheduler 等全部核心服务的文档入口
- 后端迁移指南:旧后端到新后端系统的完整迁移步骤
- 示例后端装配清单 与 示例后端包定义:真实项目的组装与依赖结构
- createBackend 实现:
defaultServiceFactories完整列表 - 启动失败谓词实现:
backend.startup配置的解析与判定逻辑
【免费下载链接】backstageBackstage is an open framework for building developer portals项目地址: https://gitcode.com/GitHub_Trending/ba/backstage
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考