登记到目录里的服务悄悄消失了
目标
在 VM 中启动一个真实的目录后端,亲手制造实体无法进入目录的四种原因,并找出痕迹留在哪里。通过真实的 API 响应,确认静态 location 的自动采集、通过 API 注册的 location、孤儿实体的清理,以及 location 的删除。
为什么重要
“我提交了 catalog-info.yaml,但门户里看不到”是 Backstage 运维中最常见的咨询。只用验证库检查一个 YAML 文件是不够的。在运行中的目录里,失败有时既不体现在 API 响应中,也不体现在默认日志中,一个实体的问题还可能让同一个文件的全部内容都进不来。注册 API 即使返回 201,也可能在处理阶段失败。只有知道该看哪里,才能修复。
VM 首次启动大约需要 4 分钟,第 1 步的安装需要从互联网下载,约 30 秒。扫描 GitHub 等外部系统的发现(discovery)provider 需要外部账号,因此不涉及。这里的“自动采集”指目录定期重新读取配置中写明的 location。
步骤
- 在
/usr/local/cba-ingest中以固定版本安装目录后端软件包。 - 确认配置中写明的文件 location 被自动采集。
- 记录缺少 owner 的实体不留痕迹地被丢弃。
- 接入日志模块,把处理错误输出为 warn 日志。
- 观察不被允许的 kind 会阻塞整个文件,并用按 location 设置的规则解决。
- 通过 API 注册 url location,并修复读取白名单的问题。
- 观察从文件中删掉的实体先成为孤儿,随后被清除。
- 删除 location,并编写采集故障报告。
参考
- 调用目录 API:
curl -s -H 'Authorization: Bearer ingest-lab-token-7f3a9c' http://127.0.0.1:7007/api/catalog/entities | jq -r '.[].metadata.name' - 重新启动后端:
cd /usr/local/cba-ingest && pkill -f 'node index.js'; setsid nohup node index.js >> backend.log 2>&1 < /dev/null & - 日志是追加写入的(
>>)。每次启动都以Loading config from行开头,所以当前进程的日志请看最后一个这样的行之后的内容。 - 就绪检查:
curl -s http://127.0.0.1:7007/.backstage/health/v1/readiness - 去掉日志中的颜色转义字符:
sed 's/\x1b\[[0-9;]*m//g' backend.log - 数据库是
:memory:,所以重启后通过 API 注册的 location 会消失(配置文件中的 location 会被重新读取)。 - 目录配置(rules、orphanStrategy、processingInterval、错误日志模块):https://backstage.io/docs/features/software-catalog/configuration
- 实体的生命周期(处理、孤儿、删除):https://backstage.io/docs/features/software-catalog/life-of-an-entity
- 目录 API(locations、entities):https://backstage.io/docs/features/software-catalog/software-catalog-api
- URL Reader 与 backend.reading.allow:https://backstage.io/docs/backend-system/core-services/url-reader
- 供外部调用的静态令牌(externalAccess):https://backstage.io/docs/auth/service-to-service-auth
- 实体格式(Component 的必填字段):https://backstage.io/docs/features/software-catalog/descriptor-format
以固定版本下载运行目录所需的材料
在 /usr/local/cba-ingest 中创建 npm 项目,并以不带范围符号的精确版本安装四个软件包(package.json 中也不带 ^)。npm 缓存设为 npm_config_cache=/usr/local/cba-ingest/.npmcache。四个软件包是 @backstage/backend-defaults@0.17.8、@backstage/plugin-catalog-backend@3.9.1、@backstage/plugin-catalog-backend-module-logs@0.1.25 和 better-sqlite3@12.4.1。
请使用 npm install --save-exact。日志模块只安装,到第 4 步再接入。better-sqlite3 13.x 没有适用于这个 Node 20 的预编译二进制文件,会尝试编译并失败。
配置中写明的文件被自动采集
在 /usr/local/cba-ingest/app-config.yaml 中写入下面的内容,并在 7007 端口启动只包含 catalog 插件的后端(index.js)(输出写入 /usr/local/cba-ingest/backend.log)。backend.baseUrl: http://localhost:7007、backend.listen.port: 7007,数据库为 better-sqlite3、':memory:',backend.auth.externalAccess 中写入 type 为 static、token 为 ingest-lab-token-7f3a9c、subject 为 ingest-cli 的条目,catalog.processingInterval: { seconds: 5 },catalog.rules: [{allow: [Component, Group, Location]}],catalog.locations 中写入 type: file、target: ./catalog/team.yaml。catalog/team.yaml 中放入 Group team-search,以及该团队拥有的 Component search-api 和 search-indexer。用 Authorization: Bearer ingest-lab-token-7f3a9c 调用 /api/catalog/entities,能看到三个实体即可。
目录 API 位于默认认证策略之后,不带令牌调用会返回 401。静态令牌在生产中应通过 ${환경변수}(占位符为环境变量名)传入,但本实验为了观察流程,直接写在文件中。请确认进入目录的实体的 backstage.io/managed-by-location 注解指向哪个文件。
缺少 owner 的实体不留痕迹地被丢弃
在 team.yaml 末尾追加一个没有 spec.owner 的 Component search-ui(type 为 website,lifecycle 为 production)。不重启,等待几秒后查看目录和日志,并在 /root/cba-ingest/silent.txt 中写三行——search_ui_in_catalog=(yes/no)、search_api_in_catalog=(yes/no)和 log_lines_mentioning_search_ui=(此刻 backend.log 中出现 search-ui 的行数)。直到本实验结束,都不要修改 search-ui(后面的步骤和评分会使用这个状态)。
处理周期已缩短为 5 秒,修改文件后很快就会被重新读取。请观察:一个实体验证失败时,同一文件中的其余内容会怎样;以及这个失败留在哪里(或者没有留下)。用 grep -c search-ui backend.log 统计行数。
把处理错误输出到日志
在 index.js 中 add @backstage/plugin-catalog-backend-module-logs,并重启后端。当 backend.log 中打印出关于 component:default/search-ui 的 warn 行时,把去掉颜色控制字符后的这一行保存到 /root/cba-ingest/owner-error.txt。不要修改 team.yaml。
目录只是把处理错误作为事件发出,把它写进日志的是另一个模块。接入模块之后,也要等处理周期运行一次才会出现这一行。用 sed 's/\x1b\[[0-9;]*m//g' backend.log | grep search-ui 查找。events backend not found 警告是因为没有 events 插件而出现的,与本任务无关。
因为一个 API,整个文件都进不来
在 /usr/local/cba-ingest/catalog/billing.yaml 中放入 Component billing-api(owner 为 team-search,providesApis 为 [billing-openapi])和 API billing-openapi(type 为 openapi,owner 为 team-search),在 catalog.locations 中追加 type: file、target: ./catalog/billing.yaml,然后重启。确认两个实体都没有进来,并把一条关于 api:default/billing-openapi 的 warn 行保存到 /root/cba-ingest/kind-error.txt。接着保持全局的 catalog.rules 不变,只在 billing.yaml 这个 location 上加上 rules: [{allow: [API]}] 并重启,让 billing-api 和 billing-openapi 都进入目录。
不被允许的 kind 并不是只丢弃那一个实体,而是让该 location 的整个处理结果失败——请与缺少 owner 的情况对比。如果把 API 加进全局规则,任何文件中的 API 都会被放行。请查看文档中按 location 设置的 rules。
注册返回 201,却什么都没有进来
在 /usr/local/cba-ingest/incoming/data.yaml 中放入 Component etl-runner 和 etl-scheduler(两者的 owner 都是 team-search),并用 python3 -m http.server 8088 --bind 127.0.0.1 在后台提供该目录的服务。① 尝试通过目录 API 注册 type: file、target 为 /usr/local/cba-ingest/incoming/data.yaml,观察响应(400);② 用 type: url、target 为 http://localhost:8088/data.yaml 发送 POST /api/catalog/locations,确认返回 201 但实体没有进来,然后把关于该 url 的一条 warn 行保存到 /root/cba-ingest/reading-error.txt。③ 在 backend.reading.allow 中加入 host: localhost:8088 并重启,然后对同一个 url 重新注册,让两个实体进入目录。把最后一次注册的 POST 响应 JSON 保存到 /root/cba-ingest/location.json。
通过 API 注册的 location 只接受 url 形式。读取 url 的是 UrlReader,没有集成(integration)的主机必须在允许列表中才能读取——这项检查发生在处理时而不是注册时,所以注册响应是成功的。数据库是 :memory:,重启后之前注册的 location 会消失,请用 GET /api/catalog/locations 确认。http.server 也要像 setsid nohup ... > 로그 2>&1 < /dev/null &(占位符为日志文件名)这样启动,以便 shell 结束后仍然存活。
从文件中删掉的实体先成为孤儿,随后被清除
从 incoming/data.yaml 中删除 etl-runner 文档(保留 etl-scheduler)。在目录中 etl-runner 被加上 backstage.io/orphan: "true" 注解的那一刻,把实体 JSON(GET /api/catalog/entities/by-name/component/default/etl-runner 的响应)保存到 /root/cba-ingest/orphan.json。然后等待,确认该实体被从目录中清除(by-name 返回 404),并在日志中找到 Deleted ... orphaned entities 行。
如果产出某个实体的 location 不再产出它,该实体就成为孤儿。是保留孤儿还是删除,由 catalog.orphanStrategy 决定,默认情况下由清理任务(日志中的 catalog_orphan_cleanup,周期 30 秒)删除。注解只会短暂出现,请以 1 秒间隔轮询,抓住它出现的那一刻。
删除 location,并编写采集故障报告
用 location.json 中的 id 发送 DELETE /api/catalog/locations/<id> 取消注册,并确认 etl-scheduler 从目录中消失。然后在 /root/cba-ingest/report.md 的前五行写入此刻测得的值——registered_location_status=(GET /api/catalog/locations/)、etl_scheduler_status=(by-name 查询)、search_ui_status=(by-name 查询)、api_location_count=(GET /api/catalog/locations 结果的数量——两个静态 location 是否出现在这里)、file_register_status=(尝试注册 type 为 file 时的响应)。在其下整理本实验中采集失败的四种原因,以及每一种的痕迹分别留在哪里。
所有值都是现在用带令牌的 curl 测得的。静态 location 由配置文件管理,所以不能通过 API 删除,也不会出现在列表中。四种原因是:缺少 owner、不被允许的 kind、读取允许列表,以及从文件中消失的实体(孤儿)。