캐시된 쿼리 가이드라인
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 쿼리 문제(또는 유사한 종류의 쿼리 일괄 처리 문제)를 나타낼 수 있습니다.
예시
예를 들어, “그룹 구성원” 페이지를 디버깅하겠습니다. 성능 막대의 왼쪽 모서리에서 데이터베이스 쿼리는 전체 데이터베이스 쿼리 수와 실행된 캐시된 쿼리 수를 보여줍니다:
이 페이지에는 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'"
]
스택 추적은 그룹 구성원마다 group.has_owner?(current_user)
를 반복적으로 실행하기 때문에 N+1 문제를 보여줍니다. 이 문제를 해결하려면 반복되는 코드 줄을 루프 외부로 이동하여 각 렌더링된 멤버에 결과를 전달하십시오:
- 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
: 303ms
수정으로 인해 할당된 메모리와 캐시된 쿼리 수가 감소했습니다. 이러한 요소들이 전체 실행 시간을 개선하는 데 도움이 됩니다:
- 총 할당량: 5313899 바이트 (65290 객체), 1810KB(25%) 감소
- 총 유지된 객체: 685593 바이트 (5278 객체), 72KB(9%) 감소
-
db_count
: 95 (34% 감소) -
db_cached_count
: 6 (89% 감소) -
db_duration
: 162ms (87% 빠름)