캐시된 쿼리 가이드라인

Rails는 SQL 쿼리 캐시를 제공하며, 이는 요청 기간 동안 데이터베이스 쿼리 결과를 캐시하는 데 사용됩니다. Rails가 동일한 요청 내에서 동일한 쿼리를 다시 만나면, 데이터베이스 쿼리를 다시 실행하는 대신 캐시된 결과 세트를 사용합니다.

쿼리 결과는 단일 요청 기간 동안에만 캐시되며, 여러 요청 간에 지속되지 않습니다.

왜 캐시된 쿼리가 좋지 않은지

캐시된 쿼리는 데이터베이스의 부담을 줄여주지만 여전히 다음과 같은 문제가 있습니다:

  • 메모리를 소비합니다.
  • Rails가 각 ActiveRecord 객체를 다시 초기화해야 합니다.
  • Rails가 객체의 각 관계를 다시 만들어야 합니다.
  • 캐시된 쿼리 디렉터리을 검사하는 데 추가 CPU 사이클을 필요로 합니다.

캐시된 쿼리는 데이터베이스 관점에서 더 저렴하지만, 메모리 관점에서는 더 비용이 많이 들 수 있습니다. 이러한 캐시된 쿼리는 N+1 쿼리 문제를 감출 수 있으므로, 일반적인 N+1 쿼리와 동일하게 다루어야 합니다.

캐시된 쿼리에 의해 N+1 쿼리가 감춰진 경우, 동일한 쿼리가 N번 실행됩니다. 데이터베이스에 N번 접근하는 것이 아니라 캐시된 결과가 N번 반환됩니다. 이것은 여전히 비용이 많이든다는 점에서 각 시간마다 객체를 다시 초기화해야 합니다. 그 대가가 큰 메모리 자원이 소모됩니다. 대신 가능한 한 동일한 인메모리 객체를 사용해야 합니다.

새로운 기능을 도입할 때 다음을 해야 합니다:

  • N+1 쿼리를 피합니다.
  • 쿼리 수를 최소화합니다.
  • 특히, 캐시된 쿼리가 N+1 문제를 감추지 않도록 특별히 주의합니다.

캐시된 쿼리를 감지하는 방법

키바나를 사용하여 잠재적 가해자 검출

GitLab.com은 pubsub-redis-inf-gprd* 색인의 db_cached_count에 실행된 캐시된 쿼리 수로 로그 항목을 남깁니다. 예를 들어, db_cached_count가 100보다 큰 엔드포인트는 캐시된 쿼리에 감춰진 N+1 문제를 나타낼 수 있습니다. 이 엔드포인트를 자세히 조사하여 중복된 캐시된 쿼리를 실행하는지 확인해야 합니다.

캐시된 쿼리와 관련된 더 많은 키바나 시각화에 대한 자세한 내용은 이슈 #259007, ‘잠재적 N+1 CACHED SQL 호출을 감지하는 데 도움이 될 메트릭스를 제공’을 참조하세요.

성능 바를 사용하여 의심스러운 엔드포인트 검사

기능을 구축할 때 성능 바를 사용하여 캐시된 쿼리를 포함한 데이터베이스 쿼리 디렉터리을 확인합니다. 성능 바는 실행된 전체 쿼리와 캐시된 쿼리 수가 100보다 큰 경우 경고를 표시합니다.

사용 가능한 통계에 대한 자세한 정보는 성능 바를 참조하세요.

어떤 것을 찾아야 하는가

Kibana를 사용하여 실행된 캐시된 쿼리의 개수를 확인할 수 있습니다. 큰 db_cached_count를 가진 엔드포인트는 많은 중복된 캐시된 쿼리를 나타낼 수 있으며, 이는 캐시된 쿼리에 의해 감춰진 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: 303 ms

수정 후, 할당된 메모리와 캐시된 쿼리 수가 감소했습니다. 이러한 요소는 전체 실행 시간을 개선하는 데 도움이 됩니다:

  • 총 할당된: 5313899 바이트 (65290 객체), 1810KB(25%) 감소
  • 총 유지된: 685593 바이트 (5278 객체), 72KB(9%) 감소
  • db_count: 95 (34% 감소)
  • db_cached_count: 6 (89% 감소)
  • db_duration: 162 ms (87% 빨라짐)

더 많은 정보