쿼리 캐싱(Query caching)

쿼리 캐싱(Query caching)

Apache Druid에서 캐싱을 활성화하면 자주 접근하는 데이터의 쿼리 시간을 개선할 수 있어요. 이 문서에서는 세그먼트별(per-segment) 캐싱과 전체 쿼리(whole-query) 캐싱 두 가지 캐시 타입을 설명하고, 기본 캐싱 동작과 캐싱 전략을 다듬는 데 도움이 되는 지침을 제공합니다.

출처: 문서

본문

Apache Druid에서 캐싱을 활성화하면 자주 접근하는 데이터의 쿼리 시간을 개선할 수 있어요. 이 문서는 Druid의 서로 다른 캐싱 타입을 정의하고, 기본 캐싱 동작을 설명하며, 캐싱 전략을 다듬는 데 도움이 되는 지침과 예시를 제공합니다.

Druid 아키텍처에 익숙하지 않다면 캐싱을 진행하기 전에 다음 주제를 먼저 검토하세요:

  • Druid Design
  • Segments
  • Query execution

쿼리 캐싱 구성에 대한 지침은 Using query caching 문서를 참고하세요. 히트율과 제거(eviction) 횟수를 포함한 캐시 모니터링은 Druid 메트릭에서 사용할 수 있어요. 쿼리 수준 캐싱은 Historical의 데이터 수준 캐싱에 추가되는 것입니다.

캐시 타입

Druid는 두 가지 유형의 쿼리 캐싱을 지원합니다:

  • 세그먼트별 캐싱(Per-segment caching)은 특정 세그먼트에 대한 부분 쿼리 결과를 저장합니다. 기본적으로 활성화되어 있어요.
  • 전체 쿼리 캐싱(Whole-query caching)은 최종 쿼리 결과를 저장합니다.

Druid는 오래된(stale) 결과를 반환하지 않도록 기반 데이터가 변경되는 순간 캐시를 무효화합니다. 이는 실시간 데이터 세그먼트를 포함해 기반 데이터 세그먼트가 매우 가변적인 table 데이터소스에서 특히 중요해요.

Druid는 캐시 데이터를 로컬 JVM 힙이나 외부 분산 키/값 저장소(예: memcached)에 저장할 수 있습니다. 기본값은 Caffeine 기반의 로컬 캐시입니다. 기본 최대 캐시 저장 크기는 JVM 최대 런타임 메모리의 1GiB와 10퍼센트 중 작은 값이며, 캐시 만료는 없습니다. 캐시 저장소 구성 방법은 Cache configuration 문서를 참고하세요. caffeine을 사용할 때 캐시는 JVM 힙 안에 있으며 직접 측정할 수 있어요. 힙 사용량은 구성된 최대 크기까지 커지며, 그 후 가장 오래 사용되지 않은(LRU) 세그먼트 결과가 제거되고 새 결과로 대체됩니다.

세그먼트별 캐싱

Druid 캐싱의 주요 형태는 *세그먼트별 결과 캐시(per-segment results cache)*입니다. 이 캐시는 부분 쿼리 결과를 세그먼트별로 저장하며, Historical 서비스에서 기본적으로 활성화되어 있어요. 세그먼트별 결과 캐시를 사용하면 Druid가 변경되지 않는 세그먼트에 대해 낮은 제거율의 캐시를 유지할 수 있는데, 특히 Historical 프로세스가 딥 스토리지에서 로컬 세그먼트 캐시로 가져오는 세그먼트에 중요합니다. 반면 실시간 세그먼트는 계속 쿼리 시간에 결과를 계산합니다.

Druid는 세그먼트별 캐시 결과와 유사한 기본 형태와 유사한 필터, 집계 등을 가진 이후 쿼리의 결과를 병합할 수 있어요. 예를 들어 쿼리가 다른 시간 기간을 다룬다는 점만 제외하면 동일한 경우가 그렇습니다.

세그먼트별 캐싱은 useCache와 populateCache 파라미터로 제어됩니다. 세그먼트별 캐싱을 실시간 데이터와 함께 사용하세요. 예를 들어 쿼리가 Historical에 로드된 세그먼트의 interval과 함께 Kafka에서 활발히 도착하는 데이터를 요청하는 경우가 그렇습니다. Druid는 Historical 세그먼트의 캐시 결과와 스트림의 실시간 결과를 병합할 수 있어요. 반면 전체 쿼리 캐싱은 이 시나리오에서 유용하지 않은데, 실시간 수집의 새 데이터가 캐시된 전체 결과를 계속 무효화시키기 때문입니다.

전체 쿼리 캐싱

*전체 쿼리 캐싱(whole-query caching)*을 사용하면 Druid가 개별 쿼리의 전체 결과를 캐시하므로, Broker가 더 이상 데이터 프로세스의 세그먼트별 결과를 병합할 필요가 없어집니다. 수집이 세그먼트 수준에서 캐시를 무효화할 위험이 적을 때 전체 쿼리 캐싱을 Broker에서 사용해 쿼리 효율을 높이세요. 특히 실시간 수집을 하지 않는 경우에 적용됩니다. 예를 들어 쿼리가 주로 배치 수집된 데이터를 사용하는 경우, 기반 세그먼트가 거의 변하지 않는데도 Druid가 매 쿼리마다 세그먼트별 결과를 계속 취득하므로 세그먼트별 캐싱은 덜 효율적이에요.

캐싱을 활성화할 위치

세그먼트별 캐시는 다음과 같은 위치에서 사용할 수 있습니다:

  • Historical에서, 기본값입니다. 더 큰 프로덕션 클러스터에서는 Broker가 모든 쿼리 결과를 병합하지 않도록 Historical에서 세그먼트 수준 캐시 채우기를 활성화하세요. Broker 대신 Historical에서 캐시 채우기를 활성화하면 Historical이 자체 로컬 결과를 병합하고 Broker에 덜 부담을 줍니다.
  • Peon이나 Indexer 서비스의 수집 태스크에서. 더 큰 프로덕션 클러스터에서는 Broker가 모든 쿼리 결과를 병합하지 않도록 태스크 서비스에서만 세그먼트 수준 캐시 채우기를 활성화해야 해요. Broker 대신 태스크 실행 서비스에서 캐시 채우기를 활성화하면 태스크 실행 서비스가 자체 로컬 결과를 병합하고 Broker에 덜 부담을 줍니다.

태스크 실행자 서비스는 데이터를 로컬로 저장하는 캐시만 지원합니다. 예를 들어 caffeine 캐시가 그렇습니다. 이 제한은 캐시가 수집 태스크가 생성한 중간 부분 세그먼트 수준에서 결과를 저장하기 때문에 존재해요. 이 중간 부분 세그먼트는 태스크 리플리카 간에 동일하지 않을 수 있습니다. 따라서 태스크 실행자 서비스는 memcached 같은 원격 캐시 타입을 무시합니다.

  • 서버가 5개 미만인 소규모 프로덕션 클러스터의 Broker에서.

대규모 프로덕션 클러스터의 Broker에서는 세그먼트별 캐시 사용을 피하세요. Broker 캐시가 활성화되고(druid.broker.cache.populateCache가 true) 쿼리 컨텍스트에서 populateCache가 false가 아니면, 개별 Historical은 개별 세그먼트 수준 결과를 병합하지 않고 이를 리드 Broker로 다시 전달합니다. 그러면 Broker가 모든 세그먼트의 큰 병합을 스스로 수행해야 합니다.

전체 쿼리 캐시는 Broker에서만 사용할 수 있습니다.

캐싱을 위한 성능 고려 사항

캐싱은 같은 시스템에서 동시성을 높여주므로, 동시적이고 혼합된 워크로드의 처리량을 다루는 Druid 클러스터의 쿼리에 눈에 띄는 성능 개선을 가져올 수 있어요. 단일 쿼리나 페이지 로드의 응답 시간을 개선하려 한다면 캐싱은 무시해야 합니다. 일반적으로 단일 태스크의 응답 시간은 캐시가 비어 있어도(chilled cache) 성능 목표를 충족해야 합니다.

쿼리 처리 중 세그먼트별 캐시는 쿼리를 가로채서 결과를 직접 Broker로 보냅니다. 이렇게 하면 쿼리가 데이터 서버 처리 스레드를 우회합니다. Broker에서 최소한의 처리가 필요한 쿼리의 경우 캐시된 쿼리는 매우 빠릅니다. Broker에서 수행되는 작업이 쿼리 병목을 유발한다면 캐싱을 활성화해도 눈에 띄는 쿼리 개선은 없습니다.

세그먼트 캐싱에서 가장 큰 성능 이득은 topN과 시간 계열(time series) 쿼리에 적용되는 경향이 있어요. groupBy 쿼리의 경우 병목이 broker의 병합 단계에 있다면 영향이 더 작습니다. 조인이 있든 없든 같은 적용돼요.

캐싱이 쿼리 성능을 높이지 않는 시나리오

캐싱이 모든 유형의 쿼리 성능 문제를 해결하지는 않아요. 각 캐시 타입에 대해 캐싱이 별 도움이 되지 않을 가능성이 높은 시나리오가 있습니다.

세그먼트별 캐싱은 다음에서 동작하지 않습니다:

  • 하위 쿼리(sub-query)를 포함하는 쿼리. 다만 하위 쿼리의 출력은 캐시될 수 있어요. 하위 쿼리 실행에 대한 자세한 내용은 Query execution 문서를 참고하세요.
  • 조인(join)이 있는 쿼리는 broker에서 어떤 캐싱도 지원하지 않습니다.
  • GroupBy 쿼리는 broker에서 세그먼트 수준 캐싱을 지원하지 않습니다.
  • 쿼리 컨텍스트에서 bySegment가 설정된 쿼리는 broker에서 캐시되지 않습니다.

전체 쿼리 캐싱은 다음에서 동작하지 않습니다:

  • 인라인 데이터소스(inline datasource)나 lookup 데이터소스를 포함하는 쿼리.
  • 조인이 있는 쿼리.
  • union 데이터소스가 있는 쿼리.

더 알아보기 (Learn more)

자세한 내용은 다음 주제들을 참고하세요:

  • Using query caching: 캐싱을 구성하고 사용하는 방법.
  • Druid Design: Druid 프로세스에 대해 알아보기.
  • Segments: Druid가 데이터를 저장하는 방법.
  • Query execution: Druid 서비스가 쿼리 문장을 처리하는 방법.