Staff 的 SiteSerializer 中 category_types 在配置架构下为每个 SiteSetting 执行一次数据库查询

我在调查员工用户的初始页面加载性能时,注意到了一种类似于“每个设置一次查询”(N-per-setting)的模式。

在我的站点上,普通非员工账户加载 /latest 的速度明显快于我的管理员/员工账户。在使用 Rack Mini Profiler 对员工请求进行性能分析时,我发现 SiteSerializer#category_types 似乎是通过逐个检查每个 SiteSetting 来确定其是否已被覆盖,从而构建类别类型配置元数据的。

相关路径似乎是:

SiteSerializer#category_types
→ Categories::TypeRegistry.list
→ 类别类型 metadata
→ resolved_configuration_schema
→ site_setting_overridden?
→ SiteSetting.provider.find

当前代码:

特别是,site_setting_overridden? 调用了:

SiteSetting.provider.find(setting_name.to_sym).present?

而 resolved_configuration_schema 在遍历配置架构中的 SiteSettings 时调用了该方法。

随后,SiteSettings::DbProvider#find 会对指定的设置执行单独的查询:

在性能分析器输出中,我观察到如下单独的查询:

SELECT name, data_type, value
FROM site_settings
WHERE name = 'discourse_post_event_allowed_on_groups';

SELECT name, data_type, value
FROM site_settings
WHERE name = 'use_local_event_date';

SELECT name, data_type, value
FROM site_settings
WHERE name = 'show_filter_by_solved_status';

SELECT name, data_type, value
FROM site_settings
WHERE name = 'topic_voting_show_who_voted';

在捕获的请求中,有 16 个查询符合这种 site_settings WHERE name = ... 模式。这些查询针对的是不同的设置名称,因此我并不是说同一个查找只是被简单地重复执行;相反,这看起来像是针对类别类型配置架构中代表的每个 SiteSetting 执行一次数据库查找。

这对员工用户尤其相关,因为 category_types 仅包含在员工的数据中:

我的性能分析还显示,插件提供的类别配置也参与了此路径,包括 Discourse Solved、日历/事件相关设置以及 Topic Voting 设置。

作为对比,在相同的 Discourse 安装环境和连接下,/latest 的预热加载时间大约为:

管理员/员工账户:
DOMContentLoaded:大约 0.77–1.03 秒
初始 HTML:解码后大约 2.08 MB

普通账户:
DOMContentLoaded:大约 0.51 秒
解码后的资源总量:大约 1.50 MB

在后续测试中,普通账户的 PWA 加载速度也至少与 AMD Developer Community PWA 一样快,因此这似乎不是一般的源站/CDN 性能问题。

我还遇到了一次不寻常的员工安全模式(Safe Mode)请求,其中 layouts/application 产生了 1,362 个 SQL 查询,耗时约 2.9 秒。我不认为上述每个 SiteSetting 的查询能解释所有这些查询,因此我将此情况单独处理,而不是声称这是导致整个异常值的原因。

对于 resolved_configuration_schema 使用的已覆盖设置状态,是否值得将其一次性获取/批量处理,而不是为每个 SiteSetting 单独调用 SiteSetting.provider.find?

例如,DbProvider 已经有一个 all 方法,尽管我并不是说直接使用 all 一定是正确的实现方式——主要是想知道在这里避免每个设置一次数据库查找是否值得。

我检查了当前 main 分支的相同路径,看起来每个设置的 provider.find 行为仍然存在。

我已在当前的 main 分支上复现了该问题,并开启了一个 PR,用于批量处理这些 SiteSetting 覆盖项的查询:

在研究该问题的过程中,我发现查询模式实际上分为两个层面。

原始实现针对类别类型配置架构中代表的每个 SiteSetting 调用一次:

SiteSetting.provider.find(...)

一种初步的批处理方案可以将此减少为每个类别类型调用一次 provider.all,但由于 Categories::TypeRegistry.list 会为每个已注册的类型单独调用 metadata,因此当多个类别类型提供 SiteSettings 时,这仍然会导致多次批量查询。

该 PR 改为在 TypeRegistry.list 中一次性加载已持久化的 SiteSetting 覆盖项,并将这些信息传递给所有正在序列化的类别类型。直接的元数据/架构解析保留了一个单一的批量提供者回退机制。

因此,对于我最初报告的员工站点负载路径,预期的查询形状从大约:

每个配置的 SiteSetting 执行一次 SELECT ... WHERE name = ?

变为:

为 TypeRegistry.list 执行一次针对 site_settings 的批量 SELECT

现有的已覆盖/未覆盖行为保持不变。

我为这两种情况添加了回归测试覆盖:

  • 单个类别类型内的多个 SiteSettings;
  • 多个类别类型共享同一个批量覆盖项查询。

在本地,相关的类别和序列化器测试均已通过:

149 个示例,0 个失败

该 PR 也已在上游 CI 中成功完成:14 项检查通过,0 项失败,0 项待定(另有 2 项检查被跳过)。

因此,这似乎解决了初始帖子中性能分析器捕获到的特定 N-per-setting 行为,而不是我在初始帖子末尾提到的另一个较大的 SQL 查询异常值。