캐시된 쿼리 가이드라인

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가 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'"
]

스택 트레이스는 각 그룹 멤버에 대해 코드를 반복적으로 실행하므로 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: 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% 빨라짐)

자세한 정보