ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Backstage Backend System 架构指南:Backend、Plugins、Modules 与核心服务全景

Backstage Backend System 架构指南:Backend、Plugins、Modules 与核心服务全景 Backstage Backend System 架构指南Backend、Plugins、Modules 与核心服务全景【免费下载链接】backstageBackstage is an open framework for building developer portals项目地址: https://gitcode.com/GitHub_Trending/ba/backstageBackstage 的后端系统Backend System是一个模块化架构用于构建、扩展和拆分 Backstage 的后端能力。阅读本文你将掌握后端的四大核心构建块Backend、Plugin、Service、Extension Point/Module的职责边界学会用createBackend组装可部署的后端实例并通过createBackendPlugin、createBackendModule、createServiceFactory等 API 编写、测试与定制自己的后端功能。概览Backstage 的后端系统提供了一个灵活的基础用于构建和扩展 Backstage 后端。它采用模块化架构让你可以创建和定制 plugins、modules 以及 service 实现。系统既面向自研功能开发也面向安装生态中第三方 plugin 与 module 的场景并且在设计上追求可扩展性与可维护性适用于各种规模的团队。这套文档体系分为四个关键领域分别对应后端的架构原理、后端搭建、功能开发、以及框架提供的核心服务Architecture解释后端系统的核心构建块与概念——backend 实例、plugins、modules、services、feature loaders以及贯穿全系统的命名规范。Building Backends讲解如何搭建和定制自己的 Backstage 后端包括后端包结构、安装 plugin/module、通过配置与服务实现进行定制以及将后端拆分为多个部署。Building Plugins and Modules展示如何创建、测试后端 plugin 与 module包括yarn new创建新 plugin、用服务与扩展点实现功能以及使用backstage/backend-test-utils进行测试。Core Services记录所有后端 plugin 可用的核心服务每个服务都附有接口、实现细节与使用示例。核心构建块无论你在部署自己的 Backstage 实例、开发插件、还是用新特性扩展现有插件理解以下构建块都很重要。下图给出了各构建块及其相互关系的总览这些概念在旧版后端系统中以某种形式存在过但在新系统中都成为了“一等公民”first-class concerns。Backend部署单元Backend 实例本身就是一个部署单元unit of deployment。它自身不承担任何业务功能只负责把各个 feature 装配wiring在一起。由你决定部署多少个 backend可以把所有 feature 放进单个后端也可以拆分成多个更小的部署具体取决于你对扩展性与功能隔离的需求。Plugins相互隔离的微服务Plugin 提供实际的基础功能。各个 plugin 完全独立运行如果要通信只能通过网络调用over the wire代码层面不存在任何直接通信路径。因此每个 plugin 都可以被视为一个独立的微服务这也是其“可水平扩展”和“隔离”两大规则见下文 Rules of Plugins的由来。Services依赖注入的实现载体Service 为 plugin 提供共享功能避免每个 plugin 都从零实现日志、数据库访问、配置读取等通用能力。框架内置了大量核心服务你也可以导入第三方服务或创建自己的服务。Service 同时是单个后端安装的重要定制点你可以用自定义实现覆盖override某个服务也可以在现有服务之上做小范围调整。Extension Points编码化的扩展模式许多 plugin 天然具有扩展面比如 Catalog 的 entity provider、Scaffolder 的自定义 action。在新系统中这类扩展模式被显式编码为 Extension Point。Extension Point 看起来有点像 service——你以依赖的方式使用它但关键区别在于扩展点由 plugin 或 module 本身注册并提供基于各 plugin 想暴露的定制能力而定。Extension Point 独立于 plugin/module 实例本身导出且可以同时暴露多个不同的扩展点。这使单个扩展点能够随时间独立演进和废弃而不必面对一整块庞大的 API 表面。Modules通过扩展点注入功能Module 使用 Extension Point 为其他 plugin 或 module 添加新功能例如为 Catalog 增加一个 Entity Provider或为 Scaffolder 增加一个或多个 action。约束规则每个 module 只能使用属于同一个 plugin的扩展点module 必须与目标 plugin 部署在同一个 backend 实例中module 只能通过已注册的扩展点与其 plugin 或其他 module 通信。与 plugin 一样module 也能访问服务、依赖自己的服务实现但会与所扩展的 plugin 共享服务——不存在 module 专属的服务实现scope 使用目标 plugin 的pluginId。包结构约定从源码结构看这套系统落实到包命名约定上详见 架构总览 的包架构部分包名模式内容plugin-pluginId-backend后端 plugin 本身的实现plugin-pluginId-node该 plugin 的扩展点以及 module/其他 plugin 需要的工具plugin-pluginId-backend-module-moduleId通过扩展点扩展该 plugin 的 modulebackend把所有东西装配成可部署单元的后端本体创建并启动 Backend 实例Backend 实例是创建后端的主入口点。调用createBackend创建实例用.add(...)安装 feature最后调用.start()启动。一个只安装 catalog 与 scaffolder 插件的最小示例import { createBackend } from backstage/backend-defaults; import scaffolderPlugin from backstage/plugin-scaffolder-backend; // Create your backend instance const backend createBackend(); // Install desired features backend.add(import(backstage/plugin-catalog-backend)); // Features can also be installed using an explicit reference backend.add(scaffolderPlugin); // Start up the backend backend.start();几个关键机制createBackend负责装配所有传入的 feature并为所有 plugin 提供全部 核心服务的默认实现。创建后端时不做实际工作一切延迟到backend.start()调用时执行.add(...)接受三类 featureplugin、module、service factory.start()会先校验所有 feature 是否存在冲突例如检查循环依赖再依次初始化各 featurecreateBackend底层调用的是 backend-app-api 中的createSpecializedBackend后者负责在没有任何服务与 feature 的情况下创建裸的后端实例——可以把createBackend理解为“batteries included”方案createSpecializedBackend则是更底层的版本。仓库中的真实后端入口本仓库自带的示例后端 packages/backend/src/index.ts 展示了标准项目如何装配 featureconst backend createBackend(); backend.add(import(backstage/plugin-auth-backend)); backend.add(import(./authModuleGithubProvider)); 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), ); // ... 其余 plugin 与 module backend.add(searchLoader); // feature loader见下文 backend.start();值得注意的是标准项目并不是逐个裸写backend.add而是用createBackendFeatureLoader按配置条件加载一组 feature例如按search.elasticsearch配置项决定是否安装 Elasticsearch 模块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); } }, });Feature loader 是编程式选择与安装 feature 的机制支持按静态配置启用/禁用功能、运行时动态加载等场景。loader 函数可以是同步、异步或 generator其约束是只能依赖 root 作用域的服务如 root config、root logger详见 Feature Loaders。启动结果与错误诊断Backend.start()返回一个BackendStartupResult包含所有 plugin 和 module 的详细成功/失败状态与耗时信息。当启动失败时会抛出BackendStartupError其中携带完整的启动结果便于定位具体是哪个 plugin 或 module 失败backend.start( ({ result }) { console.log(Backend startup result: ${JSON.stringify(result, null, 2)}); }, error { if (error instanceof BackendStartupError) { console.error( Backend startup failed: ${JSON.stringify(error.result, null, 2)}, ); } else { console.error( Unexpected error during backend startup: ${error.message}, ); } }, );这些信息本质上是启动日志的结构化表示主要价值在于为后端增加额外的监控或调试工具。配合backend.startup配置块你可以控制 plugin/module 启动失败时的行为——默认任何 boot 失败都是致命的可以按 plugin 或 module 粒度改为continue或翻转全局默认值。模块层级的 key 是createBackendModule({ moduleId: ... })中声明的moduleId而不是插件名或 entity provider 名。完整参考backend: startup: # Global defaults applied when not specified per-plugin or per-module default: onPluginBootFailure: abort # or continue onPluginModuleBootFailure: abort # or continue plugins: pluginId: onPluginBootFailure: abort # or continue modules: moduleId: onPluginModuleBootFailure: abort # or continue例如让catalog插件崩溃时其余后端继续启动用于排查依赖数据的问题backend: startup: plugins: catalog: onPluginBootFailure: continue编写后端 PluginPlugin 用createBackendPlugin创建通常从 plugin 包导出。每个 plugin 必须有 ID 和一个register方法且 ID 必须与包名去掉-backend后缀后的 plugin ID 一致。一个带 HTTP 路由的最小 plugin 如下// src/plugin.ts import { createBackendPlugin, coreServices, } from backstage/backend-plugin-api; import { createExampleRouter } from ./router; export const examplePlugin createBackendPlugin({ pluginId: example, register(env) { env.registerInit({ deps: { // Declare dependencies to services that you want to consume logger: coreServices.logger, httpRouter: coreServices.httpRouter, }, async init({ logger, httpRouter }) { const example createExampleRouter(logger); logger.info(Hello from example plugin); httpRouter.use(example); }, }); }, }); // src/index.ts export { examplePlugin as default } from ./plugin;env对象的各方法声明了 plugin 的外部表面env.registerInit注册后端启动时运行的初始化函数deps声明服务依赖init回调接收解析后的依赖实例。依赖 plugin 作用域的服务时你得到的是本 plugin 专属的实例——例如 logger 会给日志打上 plugin ID 标签HTTP router 会把 API 路由前缀为本 plugin IDenv.registerExtensionPoint注册扩展点实现详见下节按约定plugin 包应把 plugin 实例作为 default export 导出这样backend.add(import(backstage-plugin-example-backend))即可直接按包安装。从源码看createBackendPlugin 会做两件防御性检查校验pluginId是否匹配命名模式仅字母、数字和连字符且以字母开头不合法则报错并强制register中必须恰好调用一次registerInit未调用则抛出registerInit was not called by register in pluginId且registerExtensionPoint/registerConnection必须在registerInit之前调用。Plugin 的设计规则以下规则适用于生产环境下的 Backstage plugin 生态所有backstage命名空间下的 plugin 都应遵守可水平扩展Scalableplugin 不得在内存中保存状态或确保状态可跨实例复制要么无状态要么把状态存到数据库等外部服务相互隔离Isolatedplugin 之间绝不能通过代码直接通信只能走网络需要暴露外部接口的 plugin 建议通过-node库包导出 API client 服务。开发/测试环境可以例外以简化开发流程。编写后端 Module创建新 plugin 或 module 的推荐入口是yarn new选择backend-plugin会在plugins/pluginId-backend创建包选择backend-module会在plugins/pluginId-backend-module-moduleId创建包。Module 用于扩展 plugin或其他 module补充新特性或改变既有行为。它必须与目标 plugin 安装在同一 backend 实例中且一次只能扩展一个 plugin。一个给 Catalog 注册自定义 processor 的模块示例// src/module.ts import { createBackendModule } from backstage/backend-plugin-api; import { catalogProcessingExtensionPoint } from backstage/plugin-catalog-node; import { MyCustomProcessor } from ./MyCustomProcessor; export const catalogModuleExampleCustomProcessor createBackendModule({ pluginId: catalog, moduleId: example-custom-processor, register(env) { env.registerInit({ deps: { catalog: catalogProcessingExtensionPoint, logger: coreServices.logger, }, async init({ catalog }) { catalog.addProcessor(new MyCustomProcessor(logger)); }, }); }, }); // src/index.ts export { catalogModuleExampleCustomProcessor as default } from ./module;要点扩展点放在deps中与 service 一样声明——初始化时可以同时依赖多个扩展点与多个服务module 依赖的是目标 plugin 的-node库包如backstage/plugin-catalog-node而不是直接依赖 plugin 包本身以避免 plugin 包被重复安装库包重复安装则总是被支持的每个 module 包只应包含一个 module但该 module 可以扩展多个扩展点注入的plugin作用域服务与目标 plugin 收到的是完全相同的实例scope 使用目标的pluginId。Module 的 HTTP 路由与数据库命名Module 能访问目标 plugin 的同一套服务因此也可以注册自己的 HTTP handler。为避免与 plugin 或其他 module 冲突推荐注册在/modules/module-id路径下例如标准部署中完整路径为backendUrl/api/catalog/modules/example-custom-processor/v1/validators。同理运行迁移、访问数据库的 module 与目标 plugin 共享同一个逻辑数据库表名必须避免冲突。推荐模式是package name__table name。如果使用默认的 Knex 迁移设施内部记账表也要加前缀await knex.migrate.latest({ directory: migrationsDir, tableName: backstage_backend_tasks__knex_migrations, });暴露扩展点让 Module 扩展你的 Plugin当希望 module 能动态定制你的 plugin例如像 catalog 那样注入 entity provider时使用扩展点机制。一个典型的“注册型”扩展点实现import { createExtensionPoint } from backstage/backend-plugin-api; // This is the extension point interface, which is how modules interact with your plugin. export interface ExamplesExtensionPoint { addExample(example: Example): void; } // This is the extension point reference that encapsulates the above interface. export const examplesExtensionPoint createExtensionPointExamplesExtensionPoint({ id: example.examples, }); export const examplePlugin createBackendPlugin({ pluginId: example, register(env) { // We can share data between the extension point implementation and our init method. const examples new ArrayExample(); // This registers the implementation of the extension point, which is internal to your plugin. env.registerExtensionPoint(examplesExtensionPoint, { addExample(example) { examples.push(example); }, }); env.registerInit({ deps: { logger: coreServices.logger }, async init({ logger }) { // We can access examples directly logger.info(The following examples have been registered: ${examples}); }, }); }, });另一种常见的配置方式是基于静态配置依赖coreServices.rootConfig读取app-config尤其适合需要按环境差异化的定制env.registerInit({ deps: { config: coreServices.rootConfig }, async init({ config }) { const value config.getOptionalString(example.value); }, });添加自定义配置项前建议先阅读 定义插件配置 schema 的文档。服务系统Service、Factory 与作用域后端服务向所有 plugin 与 module 提供共享功能通过内嵌接口类型的 service reference 暴露机制与前端系统中的 Utility APIs 类似。整体是一套依赖注入DI实现每个 backend 实例就是一个 DI 容器每个服务的实现由 service factory 提供。Service Reference定义接口后用createServiceRef创建引用import { createServiceRef } from backstage/backend-plugin-api; export interface FooService { foo(options: FooOptions): PromiseFooResult; } export const fooServiceRef createServiceRefFooService({ id: example.foo, // owner 为 example plugin });ID 必须全局唯一一般遵循pluginId.serviceName格式。注意前端叫 API、后端叫 Service 是有意为之两者相似但不能互换使用。接口设计建议保持简单精瘦少而强的方法、倾向“options 对象入参 result 对象返回”不确定是否异步时一律用 async。Service Factory用createServiceFactory定义工厂声明它服务哪个 ref、依赖哪些服务、以及如何创建实例export const fooServiceFactory createServiceFactory({ service: fooServiceRef, deps: { bar: barServiceRef }, factory({ bar }) { return new DefaultFooService(bar); }, });无依赖时deps传空对象{}factory可以是 async服务工厂之间不允许循环依赖运行时校验检测到冲突后端拒绝启动依赖了未被任何工厂提供的服务同样导致启动失败核心服务的引用统一通过backstage/backend-plugin-api导出的coreServices对象访问例如日志服务是coreServices.logger服务可以定义defaultFactory传入createServiceRef当后端没有显式安装工厂时自动生效——建议对所有供其他 plugin/module 使用的服务都定义默认工厂。注意默认工厂回调里要用参数service而不是直接引用 ref否则会造成循环引用若你的服务在运行时出现重复安装依赖版本区间不完全对齐时可能发生会出问题就不要定义默认工厂改为要求使用者显式安装。作用域plugin 与 root默认情况下服务是plugin 作用域的每个依赖它的 plugin 都会拿到一个独立实例。这既允许为各 plugin 定制实现也保证了 plugin 间的隔离。作用域只有两种取值plugin每个 plugin 一个实例可按需懒初始化可依赖 root 和 plugin 作用域的服务root全后端共享单实例且总是会被初始化即使没有 plugin 依赖它只能依赖其他 root 作用域的服务。一些服务成对出现如rootLoggerroot 作用域承载日志主实现与loggerplugin 作用域在其上叠加 plugin 标签export const loggerServiceFactory createServiceFactory({ service: coreServices.logger, deps: { rootLogger: coreServices.rootLogger, pluginMetadata: coreServices.pluginMetadata, }, factory({ rootLogger, pluginMetadata }) { return rootLogger.child({ plugin: pluginMetadata.getId() }); }, });plugin 作用域的服务都能访问一个不可覆盖的Plugin Metadata ServicecoreServices.pluginMetadata它是所有 plugin 级定制的基础。另外工厂可以通过createRootContext定义一个跨所有 plugin 实例共享的 root context例如共享数据库连接池但不应在生产环境用它跨 plugin 共享状态——那会违反 plugin 隔离规则。还有一种multiton: true选项使一个 ref 指向实例数组用于“追加 handler 而非覆盖”的场景。核心服务一览默认后端开箱即用地提供了一组核心服务全部通过coreServices命名空间访问import { coreServices } from backstage/backend-plugin-api;服务文档说明Auth Serviceauthtoken 认证与凭据管理Cache Servicecache键值缓存Database Servicedatabase基于 knex 的数据库访问与管理Discovery Servicediscoveryplugin 间通信的服务发现Http Auth Servicehttp-authHTTP 请求认证Http Router Servicehttp-routerplugin 的 HTTP 路由注册Lifecycle Servicelifecycle启动/关闭生命周期钩子Logger Serviceloggerplugin 级日志Metrics Servicemetricsplugin 级指标alphaPermissions Servicepermissions用户动作的授权集成Plugin Metadata Serviceplugin-metadata当前 plugin 的元数据Root Config Serviceroot-config静态配置读取Root Health Serviceroot-health后端健康检查端点Root Http Router Serviceroot-http-routerroot 服务的 HTTP 路由注册Root Lifecycle Serviceroot-lifecycle后端级生命周期钩子Root Logger Serviceroot-loggerroot 级日志Scheduler Servicescheduler分布式后台任务调度Tracing Servicetracingplugin 级 trace spanalphaUrl Reader Serviceurl-reader读取外部系统内容User Info Serviceuser-info获取已认证用户信息其中 Identity Service 与 Token Manager Service 已废弃均应改用 Auth Service。定制你的后端安装除了安装现成的 plugin 与 module还有几种重要的定制方式。静态配置最易上手的定制途径。后端本身、以及众多 plugin/module 的行为都可以配置具体可配置项需查各 plugin/module 的文档核心服务的配置也在 Core Services 中说明。覆盖服务实现核心服务日志、数据库、HTTP 等使用createBackend时都已默认安装且几乎都可以替换为自己的实现。最简单的方式是沿用现有工厂但附加选项例如让 root config 支持远程配置轮询import { rootConfigServiceFactory } from backstage/backend-app-api; const backend createBackend(); backend.add( rootConfigServiceFactory({ remote: { reloadIntervalSeconds: 60 }, }), );这样就能在配置目标中传 URL且每 60 秒轮询一次变更。唯一的例外是框架提供的PluginMetadataService它不可覆盖。更深入的定制是直接写自定义工厂例如替换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); // custom logic return rootLogger.child(labels); }, }), );拆分为多个后端部署更高级的部署方式是把后端 plugin 拆进多个独立的 backend 部署。部署扩展文档与威胁模型解释了这样做的收益这里聚焦“怎么做”。由于目前没有yarn new模板直接创建新后端最快的方式是复制现有后端包并修改例如拆出packages/backend-a与packages/backend-b各自src/index.ts只保留该实例要安装的 plugin 与 module。拆出后仍需让两个后端能互相通信——目前 Backstage 没有开箱即用的方案需要手动为两端配置自定义DiscoveryService前端则可能需要同样的DiscoveryApi实现或者用一个处理路由的反向代理统一暴露。上图示例有三套后端部署前端应用与后端实例之间有一个反向代理把/api/catalog/、/api/search/路由到 CatalogSearch 实例TechDocs 与 Scaffolder 单独拆出其余流量App、Auth、Proxy路由到第三个实例。代理还可以兼作认证型反向代理拒绝未认证用户访问后端。各 plugin 拥有自己的逻辑数据库但通常共享同一个 DBMS 实例——这并非硬性要求可以按需求进一步拆分或合并。命名规范后端系统有一套命名约定帮助保持跨包导出的一致性详见 Naming Patterns对象导出命名ID 格式示例PlugincamelIdPluginkebab-case字母开头catalogPlugincatalogModulepluginIdModuleModuleIdkebab-casecatalogModuleGithubEntityProvidergithub-entity-providerService小驼峰 Service/Ref/Factory后缀pluginId.serviceNameloggerServiceRefcoreServices.logger除 plugin/module ID 使用 kebab-case 外其余命名统一为 camel case。小结Backstage 后端系统用四个构建块组织了整个后端的能力边界Backend是可自由拆分的部署与装配单元Plugin是彼此网络隔离的功能微服务Service是通过依赖注入按 plugin/root 作用域分发的共享设施也是最重要的定制点Extension Point 与 Module则把插件间的扩展模式显式化使第三方能力可以安全注入而不破坏隔离性。结合backend.startup启动策略、feature loader 的条件装配与多后端拆分部署这套体系足以支撑从单节点试用到水平拆分的大型部署形态。若要动手实践建议路径是先阅读 Architecture 建立概念再照着 Building Backends 与 Building Plugins and Modules 搭建和扩展最后按 Core Services 逐项熟悉可用服务。【免费下载链接】backstageBackstage is an open framework for building developer portals项目地址: https://gitcode.com/GitHub_Trending/ba/backstage创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表