캐시된 쿼리 가이드라인
Rails는 데이터베이스 쿼리의 결과를 요청 기간 동안 캐시하는 SQL 쿼리 캐시를 제공합니다.
Rails가 같은 요청 내에서 동일한 쿼리를 다시 만나면 데이터베이스에 대해 쿼리를 다시 실행하는 대신 캐시된 결과 집합을 사용합니다.
쿼리 결과는 단일 요청의 기간 동안만 캐시되며, 여러 요청에 걸쳐 지속되지 않습니다.
캐시된 쿼리가 좋지 않은 이유
캐시된 쿼리는 데이터베이스의 부하를 줄이는 데 도움을 주지만 여전히:
- 메모리를 소비합니다.
- 각
ActiveRecord
객체를 재인스턴스화해야 합니다. - 객체의 각 관계를 재인스턴스화해야 합니다.
- 캐시된 쿼리 목록을 살펴보는 데 추가 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 CACHED SQL 호출을 탐지하는 데 도움이 되는 메트릭 제공’을 참조하세요.
성능 바를 사용하여 의심스러운 엔드포인트 검사하기
기능을 빌드할 때는
성능 바를 사용하여 캐시된 쿼리를 포함한 데이터베이스 쿼리 목록을 확인하세요.
성능 바는 총 실행된 쿼리와 캐시된 쿼리의 수가 100보다 클 경우 경고를 표시합니다.
사용할 수 있는 통계에 대한 더 많은 정보는
성능 바를 참조하세요.
무엇을 찾아야 하는가
Kibana를 사용하여 실행된 캐시된 쿼리가 많은 경우를 찾아보세요.
db_cached_count
가 큰 엔드포인트는 많은 중복 캐시된 쿼리를 시사할 수 있으며, 이는 종종 숨겨진 N+1 문제를 나타냅니다.
특정 엔드포인트를 조사할 때는
성능 바를 사용하여 유사하고 캐시된 쿼리를 식별하세요.
이것은 또한 N+1 쿼리 문제(또는 유사한 쿼리 배치 문제)를 나타낼 수 있습니다.
예시
예를 들어, “그룹 구성원” 페이지를 디버깅해 보겠습니다. 성능 바의 왼쪽 모서리에서 Database queries는 총 데이터베이스 쿼리 수와 실행된 캐시된 쿼리 수를 보여줍니다:
페이지에는 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
액션主로 둘러싸세요. 수정 전 애플리케이션은 다음 통계를 가지고 있었습니다:
- 총 할당: 7133601 바이트 (84858 개체)
- 총 유지: 757595 바이트 (6070 개체)
-
db_count
: 144 -
db_cached_count
: 55 -
db_duration
: 303 ms
수정으로 할당된 메모리와 캐시된 쿼리 수가 줄어들었습니다. 이러한 요소들은 전체 실행 시간을 개선하는 데 도움이 됩니다:
- 총 할당: 5313899 바이트 (65290 개체), 1810 KB (25%) 감소
- 총 유지: 685593 바이트 (5278 개체), 72 KB (9%) 감소
-
db_count
: 95 (34% 감소) -
db_cached_count
: 6 (89% 감소) -
db_duration
: 162 ms (87% 빨라짐)