Help us learn about your current experience with the documentation. Take the survey.

缓存查询指南

Rails 提供了一个 SQL 查询缓存,用于在请求期间缓存数据库查询的结果。当 Rails 在同一请求中再次遇到相同的查询时,它会使用缓存的结果集,而不是再次向数据库运行查询。

查询结果仅在单个请求期间缓存,不会在多个请求之间持久化。

为什么缓存查询被认为是不好的

缓存查询通过减少数据库负载来帮助,但它们仍然:

  • 消耗内存。
  • 需要 Rails 重新实例化每个 ActiveRecord 对象。
  • 需要 Rails 重新实例化对象的每个关联关系。
  • 让我们花费额外的 CPU 周期来查看缓存查询列表。

虽然从数据库的角度来看,缓存查询成本更低,但从内存的角度来看,它们可能更昂贵。它们可能会掩盖 N+1 查询问题,所以你应该像对待常规的 N+1 查询一样对待它们。

在缓存查询掩盖的 N+1 查询情况下,同一个查询被执行了 N 次。它不会 N 次访问数据库,而是 N 次返回缓存的结果。这仍然很昂贵,因为每次都需要重新初始化对象,这对 CPU 和内存资源来说代价更大。相反,你应该尽可能使用相同的内存中的对象。

当你引入新功能时,应该:

如何检测缓存查询

使用 Kibana 检测潜在问题

在 GitLab.com 中,日志条目在 pubsub-redis-inf-gprd* 索引中记录了执行缓存查询的数量,作为 db_cached_count。你可以根据执行了大量缓存查询的端点进行过滤。例如,一个 db_cached_count 大于 100 的端点可能表明存在被缓存查询掩盖的 N+1 问题。你应该进一步调查这个端点,确定它是否确实在执行重复的缓存查询。

有关缓存查询的更多 Kibana 可视化,请阅读 问题 #259007,‘提供帮助我们检测潜在 N+1 缓存 SQL 调用的指标’

使用性能栏检查可疑端点

在构建功能时,使用 性能栏 查看数据库查询列表,包括缓存查询。当总执行查询和缓存查询的数量大于 100 时,性能栏会显示警告。

有关可用统计信息的更多信息,请参阅 性能栏

需要注意什么

使用 Kibana,你可以查找大量执行的缓存查询。具有较大 db_cached_count 的端点可能表明存在大量重复的缓存查询,这通常意味着存在被掩盖的 N+1 问题。

当你调查特定端点时,使用 性能栏 来识别相似和缓存查询,这也可能表明存在 N+1 查询问题(或类似的查询批处理问题)。

一个示例

例如,让我们调试"群组成员"页面。在性能栏的左角,数据库查询 显示了数据库查询总数和执行缓存查询的数量:

Performance Bar Database Queries

该页面包含 55 个缓存查询。选择该数字会显示一个包含查询更多详细信息的模态窗口。缓存查询在查询下方标记有 cached 标签。你可以在这个模态窗口中看到多个重复的缓存查询:

Performance Bar Cached Queries Modal

选择省略号 ( ellipsis_h ) 来展开实际的堆栈跟踪:

[
  "app/models/group.rb:305:in `has_owner?'",
  "ee/app/views/shared/members/ee/_license_badge.html.haml:1",
  "app/helpers/application_helper.rb:19:in `render_if_exists'",
  "app/views/shared/members/_member.html.haml:31",
  "app/views/groups/group_members/index.html.haml:75",
  "app/controllers/application_controller.rb:134:in `render'",
  "ee/lib/gitlab/ip_address_state.rb:10:in `with'",
  "ee/app/controllers/ee/application_controller.rb:44:in `set_current_ip_address'",
  "app/controllers/application_controller.rb:493:in `set_current_admin'",
  "lib/gitlab/session.rb:11:in `with_session'",
  "app/controllers/application_controller.rb:484:in `set_session_storage'",
  "app/controllers/application_controller.rb:478:in `set_locale'",
  "lib/gitlab/error_tracking.rb:52:in `with_context'",
  "app/controllers/application_controller.rb:543:in `sentry_context'",
  "app/controllers/application_controller.rb:471:in `block in set_current_context'",
  "lib/gitlab/application_context.rb:54:in `block in use'",
  "lib/gitlab/application_context.rb:54:in `use'",
  "lib/gitlab/application_context.rb:21:in `with_context'",
  "app/controllers/application_controller.rb:463:in `set_current_context'",
  "lib/gitlab/jira/middleware.rb:19:in `call'"
]

堆栈跟踪显示了一个 N+1 问题,因为代码对每个群组成员重复执行 group.has_owner?(current_user)。要解决这个问题,将重复的代码行移到循环外部,将结果传递给每个渲染的成员:

- current_user_is_group_owner = @group && @group.has_owner?(current_user)

= render  partial: 'shared/members/member',
          collection: @members, as: :member,
          locals: { membership_source: @group,
                    group: @group,
                    current_user_is_group_owner: current_user_is_group_owner }

修复缓存查询 后,性能栏现在只显示 6 个缓存查询:

Performance Bar Fixed Cached Queries

如何衡量变更的影响

使用 内存分析器 来分析你的代码。对于 这个示例,将分析器包装在 Groups::GroupMembersController#index 操作周围。修复前,应用程序具有以下统计信息:

  • Total allocated: 7133601 bytes (84858 objects)
  • Total retained: 757595 bytes (6070 objects)
  • db_count: 144
  • db_cached_count: 55
  • db_duration: 303 ms

修复减少了分配的内存和缓存查询的数量。这些因素有助于改善整体执行时间:

  • Total allocated: 5313899 bytes (65290 objects), 1810 KB (25%) less
  • Total retained: 685593 bytes (5278 objects), 72 KB (9%) less
  • db_count: 95 (34% less)
  • db_cached_count: 6 (89% less)
  • db_duration: 162 ms (87% faster)

更多信息