缓存查询指南
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 查询问题(或类似的查询批处理问题)。
一个示例
例如,让我们调试"群组成员"页面。在性能栏的左角,数据库查询 显示了数据库查询总数和执行缓存查询的数量:
该页面包含 55 个缓存查询。选择该数字会显示一个包含查询更多详细信息的模态窗口。缓存查询在查询下方标记有 cached 标签。你可以在这个模态窗口中看到多个重复的缓存查询:
选择省略号 ( ) 来展开实际的堆栈跟踪:
[
"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 个缓存查询:
如何衡量变更的影响
使用 内存分析器 来分析你的代码。对于 这个示例,将分析器包装在 Groups::GroupMembersController#index 操作周围。修复前,应用程序具有以下统计信息:
- Total allocated: 7133601 bytes (84858 objects)
- Total retained: 757595 bytes (6070 objects)
db_count: 144db_cached_count: 55db_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)