news 2026/9/11 13:12:59

Backstage Catalog 数据库查询性能测试:Query Performance Battery 实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Backstage Catalog 数据库查询性能测试:Query Performance Battery 实战指南

Backstage Catalog 数据库查询性能测试:Query Performance Battery 实战指南

【免费下载链接】backstageBackstage is an open framework for building developer portals项目地址: https://gitcode.com/GitHub_Trending/ba/backstage

本篇指南围绕 Backstage Catalog 的数据库查询性能测试方法展开:在改动 catalog 数据库查询、索引或 schema 前后,如何运行仓库中内置的Query Performance Battery(查询性能测试集),将结果与既有基线对比,并据此判断是否存在性能回归。读完本文,你将掌握 11 个覆盖 Catalog 核心读路径的测试场景、每种场景的健康查询计划(healthy plan)与反模式识别方法,以及如何在生产规模副本上安全地执行EXPLAIN (ANALYZE, BUFFERS)并维护基线文档。

测试的完整定义位于 queries.md,基线记录位于 baseline.md,二者配合使用,也可通过/catalog-db-performance技能自动执行。

为什么 Catalog 需要专门的数据库性能测试

Backstage Catalog 的元数据存储规模非常可观。以仓库基线中记录的生产规模副本为例:

  • search表约13.2M 行、堆体积 11+ GB—— 对它做一次全表顺序扫描(Seq Scan)代价是灾难性的;
  • relations表约3.5M 行、714 MB 堆
  • final_entities474K 行refresh_state约 476K 行,refresh_state_references约 478K 行。

在这样的规模下,索引缺失、计划形状劣化(如出现 Materialized CTE、临时文件落盘)都会直接放大为秒级甚至分钟级的查询延迟。性能测试集的价值正在于此:用一组固定的、贴近真实用户行为的场景,快速暴露"改动 schema 或索引后查询计划是否退化"的问题。注意queries.md中明确标注了这些数据规模数字是编写测试集时的真实观测,不同环境的绝对耗时不可直接横向比较,关注的是计划形状变化与比例性回归

测试集组成:场景、基线与运行方式

整个性能测试体系由三个文件组成:

文件作用
queries.md定义 11 个测试场景:用户动作、对应的方法调用、参考 SQL、健康计划、反模式清单、全局反模式
baseline.md记录最近一次运行的执行时间、规划时间、计划形状、Buffer 统计与反模式检测结果
README.md简要说明:场景与基线用于检测 catalog 数据库性能回归,可经/catalog-db-performance技能或手动psql运行

从仓库结构看,该测试集挂在plugins/catalog-backend/src/tests/performance/目录下,与集成测试、迁移测试并列,说明它被定位为 catalog-backend 的常规质量保障手段之一。

运行前的准备工作

1. 获取数据库连接信息

数据库连接参数(host、port、user、database name)是环境相关的,不会存储在仓库中。运行前需要向维护者或 DBA 索取针对目标副本的连接细节。

2. 了解两条执行路径

queries.md提供了两种执行方式:

  • 首选方法(Preferred method):实例化DefaultEntitiesCatalog(或直接调用 REST 端点),按每个场景给出的参数发起调用,并在数据库侧用EXPLAIN (ANALYZE, BUFFERS)捕获计划。可以通过 knex 的 debug 日志或数据库代理来截获计划。这种方式测试的是代码实际生成的查询,结论最可靠。
  • 备选方法(Alternative):直接用psql对生产规模副本执行参考 SQL。需要留意:参考 SQL 是编写时对代码所生成查询的快照,下结论前必须先核对它是否仍与当前代码一致

3. 关键约束

  • 不要把数据库连接信息写进仓库;
  • 每个查询使用30 秒超时
  • 部分查询使用占位 entity ref(如component:default/my-service),目标数据库中可能不存在该实体——返回 0 行完全正常,计划形状才是判断依据(这一点在基线的场景 6/8/9 中均有体现,见后文)。

4. 每个场景需要记录的四类指标

  1. 执行时间(Execution time);
  2. 规划时间(Planning time);
  3. 计划形状(Plan shape)——顶层节点与所用索引名;
  4. Buffer 统计——EXPLAIN (ANALYZE, BUFFERS)输出的shared hittemp read/written等。

同时要对照该场景的反模式清单以及queries.md底部的全局反模式清单进行检查。

11 个测试场景详解

每个场景对应一个真实的用户操作或后台处理动作,queries.md为每个场景给出了方法调用、参考 SQL、健康计划与反模式。以下是完整清单与要点:

场景 1:分页实体列表(kind=component,按名称排序)

  • 用户动作:打开默认的 catalog 表格视图。
  • 方法调用catalog.queryEntities({ filter: { kind: 'component' }, orderFields: [{ field: 'metadata.name', order: 'asc' }], limit: 20, credentials })
  • 参考 SQL:以search_key_value_entity_idx上的 Index Scan 驱动,按search.value排序,LIMIT 21(20 条 + 1 条溢出判断)。
  • 健康计划search_key_value_entity_idx提供排序序的 Index Scan,LIMIT 在取到 21 行后短路。执行时间应<5ms
  • 反模式:Materialized CTE(意味着查询形状被迫全量求值)、Sort 节点压在 Seq Scan 之上(意味着索引没有提供顺序)、执行时间 >50ms。

场景 2:计数查询(kind=component)

  • 用户动作:catalog 表格页脚的totalItems计数。
  • 方法调用:与场景 1 相同,计数值来自响应的totalItems字段;计数与列表查询并行执行。
  • 参考 SQLCOUNT(*)加 EXISTS 过滤,与列表查询共享同类型的索引访问路径。
  • 健康计划:在search_key_value_entity_idx上做索引扫描并对 EXISTS 过滤使用嵌套循环。这是大结果集下天然的昂贵操作——执行时间就是任何需要计数的查询的下限。
  • 反模式search表出现 Seq Scan(索引缺失);执行时间随实体数超线性增长

场景 3:分页实体列表(无过滤,LIMIT 21)

  • 用户动作:不加任何过滤条件的"显示全部"视图。这是分页的最坏情况——LIMIT 短路至关重要
  • 方法调用catalog.queryEntities({ limit: 20, credentials })
  • 参考 SQL:直接扫final_entities,按entity_ref排序后LIMIT 21
  • 健康计划final_entities_entity_ref_uniq上的 Index Scan,执行时间<1ms
  • 反模式:Sort 节点(索引未提供顺序)、final_entities上 Seq Scan。

场景 4:Facets 查询(kind=template,facet=spec.type)

  • 用户动作:侧边栏中小结果集的 facet 计数。
  • 方法调用catalog.facets({ filter: { kind: 'template' }, facets: ['spec.type'], credentials })
  • 参考 SQL:外层在search上按 facet 键聚合,内层通过 EXISTS 过滤出kind=template的实体。
  • 健康计划:facet 聚合使用search_facets_covering_idxsearch_key_value_entity_idx;过滤子查询使用索引支撑的 EXISTS。
  • 反模式:外层对search做 Seq Scan;小结果集却用 Hash Join 而非 Nested Loop。

场景 5:Facets 查询(kind=component)——大结果集

  • 用户动作:与场景 4 相同,但过滤集很大(数万个组件),检验计划在规模下是否仍然高效。
  • 方法调用:同场景 4,仅把kind换成'component'
  • 参考 SQL:同场景 4,仅过滤值不同。
  • 健康计划:与场景 4 类似,但大过滤集下可以使用 Hash Join;执行时间与匹配实体数成正比。
  • 反模式search表(外层或内层)Seq Scan;临时文件落盘(检查Buffers: temp)。

场景 6:按 ref 查询单个实体

  • 用户动作:按名称打开单个实体页面。
  • 方法调用catalog.entitiesBatch({ entityRefs: ['component:default/my-service'], credentials })
  • 参考 SQLWHERE final_entities.entity_ref = 'component:default/my-service'
  • 健康计划final_entities_entity_ref_uniq上的 Index Scan,执行时间<1ms
  • 反模式:Seq Scan——灾难性的,说明唯一索引缺失。

场景 7:全文过滤(LIKE '%player%',kind=component)

  • 用户动作:在 catalog 表格的搜索框中输入关键词。前导通配符使得索引无法提供排序序的短路。
  • 方法调用catalog.queryEntities({ filter: { kind: 'component' }, orderFields: [{ field: 'metadata.name', order: 'asc' }], fullTextFilter: { term: 'player' }, limit: 20, credentials })
  • 参考 SQL:在场景 1 的基础上追加AND search.value LIKE '%player%'
  • 健康计划search_key_value_entity_idx上按key = 'metadata.name'做 Index Scan,LIKE 作为 Filter。LIKE 本身(前导通配符)无法走索引,但查询其余部分必须由索引驱动。
  • 反模式search表 Seq Scan——LIKE 应当是索引扫描上的过滤条件,而不是触发顺序扫描的元凶。

场景 8:关系遍历(实体谱系 ancestry)

  • 用户动作/entities/by-name/.../ancestry端点。
  • 方法调用catalog.entityAncestry('component:default/my-service', { credentials })
  • 参考 SQL(迭代遍历的一步):refresh_state_referencestarget_entity_ref过滤后与final_entities连接,LIMIT 10
  • 健康计划refresh_state_references_target_entity_ref_idx上的 Index Scan + 对final_entities_entity_ref_uniq的 Nested Loop Index Scan。
  • 反模式refresh_state_references上 Seq Scan(target 索引缺失);relations上 Seq Scan(target_entity_ref索引缺失)。

场景 9:Stitching:入向引用计数

  • 上下文:每次 stitch 时都会执行,用于判定实体是否为孤儿(orphan)。不是用户可见动作,但对处理吞吐量至关重要。
  • 参考 SQLSELECT count(*) FROM refresh_state_references WHERE target_entity_ref = ...
  • 健康计划refresh_state_references_target_entity_ref_idx上的Index Only Scan,执行时间<1ms
  • 反模式:Seq Scan(索引缺失)。

场景 10:对抗性用例:无过滤全量计数

  • 用户动作:统计整个 catalog 的实体数(无任何过滤),确立计数性能的上限。
  • 方法调用catalog.queryEntities({ limit: 0, credentials }),响应中的totalItems即全量计数。
  • 参考 SQLsearchfinal_entities连接后COUNT(*)
  • 健康计划search_key_value_entity_idx上的索引扫描,执行时间与 catalog 总规模成正比。
  • 反模式:任一表 Seq Scan;50 万实体 catalog 上执行时间 >30s

场景 11:孤儿检测反连接(anti-join)

  • 上下文:周期性的孤儿清理(deleteOrphanedEntities),默认每30 秒运行一次。不是用户可见动作,但是持续性的后台负载。
  • 参考 SQLrefresh_stateLEFT OUTER JOINrefresh_state_references后取target_entity_ref IS NULL的行,LIMIT 100
  • 健康计划:利用refresh_state_references.target_entity_ref上的索引做反连接,执行时间<500ms
  • 反模式refresh_state_references上 Seq Scan(这是最需要避免被扫描的主表);Hash Join 把整张 references 表拉进内存。

全局反模式:任何场景都不允许出现

queries.md底部定义了五条全局反模式,它们在任何查询中出现都意味着严重问题:

  1. search表 Seq Scan—— search 表体积 11+ GB,任何顺序扫描都是灾难性的;
  2. relations表 Seq Scan—— 714 MB 堆、3.5M 行,必须走索引;
  3. Materialized CTE—— 会阻止 LIMIT 短路,曾是分页查询缓慢的原始根因;
  4. 临时文件落盘EXPLAIN输出中的Buffers: temp)—— 说明查询正在物化一个很大的中间结果;
  5. 内层 Seq Scan 的 Nested Loop—— 通常意味着内表连接列上缺索引。

基线解读:以 baseline.md 为例理解对比方法

基线文档 baseline.md 记录了 2026-05-18 在生产规模副本上的完整运行结果。当时的 catalog 规模为:约 474Kfinal_entities、约 13.2Msearch行、约 3.5Mrelations、约 478Krefresh_state_references、约 476Krefresh_state

本次运行摘要(部分场景)

场景执行时间结论
1. 分页列表(kind=component)12.5 msOK —— 较上次改善
3. 分页列表(无过滤)0.1 msExcellent
6. 按 ref 查询0.1 msExcellent
7. 全文过滤(LIKE)903.5 ms可接受 —— 但相对上次有回归(见下)
10. 全量计数1317.4 msOK —— 改善
11. 孤儿检测255.7 ms已修复—— Hash Anti Join 取代 Nested Loop(原为 >30s 超时)

与上一次基线(2026-05-16)的对比要点

  • Catalog 规模变化:实体从约 545K 缩至 474K,refresh_state从 984K 缩至 476K(几乎减半),refresh_state_references从 547K 缩至 478K。规模变化会显著影响触及这些表的场景。
  • 主要改善:场景 1 分页列表 39% 提速(计划从串行改为 2 worker 的 Gather Merge);场景 2 计数 45% 提速;场景 10 全量计数 41% 提速(主要受益于 catalog 缩小);场景 11 孤儿检测从>30s 超时降到 255ms —— 计划器改为 Parallel Hash Anti Join,这很可能得益于refresh_state表缩小后跨过了计划器成本模型的阈值。
  • 需要关注的回归:场景 7(LIKE 全文过滤)903ms 对 566ms,慢了 60%。两次运行都不得不扫描全部组件(前导通配符不可避免),但上次计划通过 Memoize 更早地应用 LIKE 过滤,本次改为 HashAggregate 方式先求值全部约 55K 个组件再过滤。此外组件数本身也从约 46K 涨到了约 55K。值得持续监控
  • 非查询原因的变化:场景 4(facets kind=template)3.7ms 对 1.1ms 慢 3.3 倍,但模板数从 9 个涨到 196 个,计划形状健康,并非查询回归
  • 零影响场景:场景 6/8/9 计划形状与耗时完全一致。

这个对比过程展示了正确的基线比较方法:先排除数据规模变化的影响,再聚焦计划形状改变与比例性回归——这正是 skill 中"关注 plan shape 变化和比例性回归"这一原则的具体体现。

何时运行性能测试

catalog-db-performanceskill 明确列出了四类触发时机:

  • 修改 catalog 数据库查询的前后
  • 添加/删除/修改索引的前后
  • schema 迁移的前后
  • 周期性运行以建立新鲜基线

底层实现佐证:索引与代码如何支撑健康计划

测试集中反复出现的健康计划(如search_key_value_entity_idxfinal_entities_entity_ref_uniqrefresh_state_references_target_entity_ref_idxsearch_facets_covering_idx)并非假设,而是由实际迁移脚本创建的真实索引。

迁移 20260510000000_search_indices_and_dedup.js 的头部注释详细说明了 search 表的去重与覆盖索引策略(覆盖索引说明):

  • search_entity_key_value_idx—— 在(entity_id, key, value)上建立UNIQUE约束,保证去重后的唯一性;
  • search_key_value_entity_idx—— 在(key, value, entity_id)上建立的覆盖索引,是场景 1/2/4/7/10 计划的核心驱动(索引定义);
  • search_facets_covering_idx—— 在(key, original_value, entity_id)上的部分覆盖索引(WHERE original_value IS NOT NULL),服务于 facets 聚合;
  • 同时删除被取代的旧索引search_key_value_idxsearch_key_original_value_idx

该迁移在 PostgreSQL 上使用CREATE INDEX CONCURRENTLY,避免阻塞读写,但对 13M+ 行的表可能需要数分钟;迁移本身是幂等的,且每个步骤都会检查当前状态、清理中断留下的 INVALID 索引。注释还特别提醒:如果 Kubernetes liveness 探针在索引构建完成前杀掉 Pod,构建会反复从头开始,大型安装建议在部署前手动执行其中的 SQL

场景 11 涉及的孤儿检测,其实现位于 deleteOrphanedEntities.ts。该函数在事务中循环执行反连接查询:先用 CTEorphans找出没有任何入向引用的refresh_state行(leftOuterJoin+whereNull),再连接relations把孤儿实体的子实体一并纳入候选,随后批量删除并调用markForStitching标记受影响的实体重新 stitch。基线中"Hash Anti Join 取代 Nested Loop、从超时降到 255ms"的修复,正是作用于这段查询路径。此外,早期迁移 20210302150147_refresh_state.js 中已定义了refresh_state_entity_ref_uniqrefresh_state_references_target_entity_ref_idx,为场景 8/9/11 提供了索引基础。

回归判定标准与报告要点

每次运行结束后,需要与基线对比并标记以下三类问题:

  1. 执行时间回归 >50%
  2. 计划形状变化(换了不同的索引、出现了新的 Sort/Seq Scan 节点);
  3. 出现了上次运行没有的新反模式

同时务必记住:catalog 规模差异会影响绝对耗时,因此重点应放在计划形状变化与比例性回归上。最终向用户汇报时,需要给出:哪些场景改善、哪些场景回归、是否检测到全局反模式,并把新结果以相同格式写回 baseline.md,在底部追加对比小节记录显著变化。

总结

Query Performance Battery 为 Backstage Catalog 的数据库层提供了一套低成本、高覆盖的回归检测手段:11 个场景覆盖列表分页、计数、facet、单实体查询、全文过滤、关系遍历、stitching 与后台清理等全部核心读路径,每一条都配对了健康计划与反模式判据;配合生产规模副本上的基线记录,开发者可以在改动查询、索引或 schema 的每个环节快速确认"没有把数据库搞坏"。对于 plan shape 异常或反模式的出现,还可顺着迁移脚本与catalog-backend的数据库操作源码进一步定位根因。

【免费下载链接】backstageBackstage is an open framework for building developer portals项目地址: https://gitcode.com/GitHub_Trending/ba/backstage

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 13:11:40

WPF数据可视化实战:高性能动态图表与仪表盘开发

1. WPF数据可视化项目概述在工业控制、物联网监控和业务分析系统中&#xff0c;数据可视化始终是核心需求。最近我完成了一个基于WPF的实时数据监控项目&#xff0c;主要实现了动态折线图和仪表盘两大核心组件。这个方案完美替代了传统WinForm图表控件&#xff0c;在医疗监护设…

作者头像 李华
网站建设 2026/9/11 13:09:31

如何用 Vosk 三步搞定离线语音识别:完整指南

如何用 Vosk 三步搞定离线语音识别&#xff1a;完整指南 【免费下载链接】vosk-api Offline speech recognition API for Android, iOS, Raspberry Pi and servers with Python, Java, C# and Node 项目地址: https://gitcode.com/GitHub_Trending/vo/vosk-api Vosk 是一…

作者头像 李华
网站建设 2026/9/11 13:08:40

车载蓝牙六大协议协同开发实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 13:04:11

Locust压测实战指南:从脚本编写到分布式压测的完整攻略

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 13:04:01

5 个场景讲透 electerm:一台电脑管完所有远程连接

5 个场景讲透 electerm&#xff1a;一台电脑管完所有远程连接 【免费下载链接】electerm &#x1f4fb;Free and open-sourced terminal/ssh/sftp/ftp/telnet/serialport/RDP/VNC/Spice client(Linux, Mac, Windows, Android, HarmonyOS, iOS) 项目地址: https://gitcode.com…

作者头像 李华
网站建设 2026/9/11 13:02:17

微网群分布式优化调度:目标级联法原理与Matlab实现

1. 项目背景与核心价值微网群分布式优化调度是当前能源互联网领域的前沿研究方向。随着可再生能源渗透率不断提高&#xff0c;传统集中式调度方法在计算效率、隐私保护和扩展性等方面面临严峻挑战。目标级联法&#xff08;Analytical Target Cascading, ATC&#xff09;作为一种…

作者头像 李华