Skip to content

Commit e5cacdd

Browse files
Log parquet cache metrics in DataFusion plans
1 parent 1fe6bcf commit e5cacdd

8 files changed

Lines changed: 399 additions & 10 deletions

File tree

quickwit/quickwit-datafusion/src/sources/metrics/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ pub(crate) mod factory;
2424
pub(crate) mod index_resolver;
2525
pub(crate) mod metastore_provider;
2626
pub(crate) mod optimizer;
27+
pub(crate) mod parquet_cache_metrics;
2728
pub(crate) mod predicate;
2829
pub(crate) mod sketch_udf;
2930
pub(crate) mod table_provider;
@@ -50,6 +51,7 @@ use quickwit_proto::metastore::{MetastoreError, MetastoreServiceClient};
5051
use self::factory::{METRICS_FILE_TYPE, MetricsTableProviderFactory, SKETCHES_FILE_TYPE};
5152
use self::index_resolver::{MetastoreIndexResolver, MetricsIndexResolver};
5253
use self::optimizer::SortedSeriesStreamingAggregateRule;
54+
pub use self::parquet_cache_metrics::instrument_parquet_range_cache_metrics;
5355
use self::sketch_udf::{create_dd_quantile_udf, create_dd_sketch_udaf};
5456
use self::table_provider::MetricsTableProvider;
5557

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
// Copyright 2021-Present Datadog, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
//! Per-query cache metrics for metrics parquet scans.
16+
17+
use std::ops::Range;
18+
use std::sync::Arc;
19+
20+
use bytes::Bytes;
21+
use datafusion::common::tree_node::{Transformed, TreeNode};
22+
use datafusion::datasource::source::DataSourceExec;
23+
use datafusion::error::Result as DFResult;
24+
use datafusion::parquet;
25+
use datafusion::parquet::arrow::arrow_reader::ArrowReaderOptions;
26+
use datafusion::parquet::arrow::async_reader::AsyncFileReader;
27+
use datafusion::parquet::file::metadata::ParquetMetaData;
28+
use datafusion::physical_plan::ExecutionPlan;
29+
use datafusion_datasource::PartitionedFile;
30+
use datafusion_datasource::file_scan_config::FileScanConfigBuilder;
31+
use datafusion_datasource_parquet::ParquetFileReaderFactory;
32+
use datafusion_datasource_parquet::source::ParquetSource;
33+
use datafusion_physical_plan::metrics::{
34+
Count, ExecutionPlanMetricsSet, MetricBuilder, MetricType,
35+
};
36+
use futures::FutureExt;
37+
use futures::future::BoxFuture;
38+
use quickwit_storage::{
39+
StorageCacheMetrics, StorageCacheMetricsSnapshot, with_storage_cache_metrics,
40+
};
41+
use tracing::warn;
42+
43+
pub(super) fn instrument_parquet_file_reader_factory(
44+
inner: Arc<dyn ParquetFileReaderFactory>,
45+
) -> Arc<dyn ParquetFileReaderFactory> {
46+
Arc::new(InstrumentedParquetFileReaderFactory { inner })
47+
}
48+
49+
pub fn instrument_parquet_range_cache_metrics(
50+
plan: Arc<dyn ExecutionPlan>,
51+
) -> Arc<dyn ExecutionPlan> {
52+
match Arc::clone(&plan).transform_up(|plan| {
53+
if let Some(rewritten) = instrument_parquet_scan(&plan) {
54+
Ok(Transformed::yes(rewritten))
55+
} else {
56+
Ok(Transformed::no(plan))
57+
}
58+
}) {
59+
Ok(transformed) => transformed.data,
60+
Err(error) => {
61+
warn!(%error, "failed to install parquet cache metrics on worker plan");
62+
plan
63+
}
64+
}
65+
}
66+
67+
fn instrument_parquet_scan(plan: &Arc<dyn ExecutionPlan>) -> Option<Arc<dyn ExecutionPlan>> {
68+
let data_source_exec = plan.as_any().downcast_ref::<DataSourceExec>()?;
69+
let (file_scan_config, parquet_source) =
70+
data_source_exec.downcast_to_file_source::<ParquetSource>()?;
71+
let reader_factory = parquet_source.parquet_file_reader_factory()?.clone();
72+
73+
let parquet_source = parquet_source
74+
.clone()
75+
.with_parquet_file_reader_factory(instrument_parquet_file_reader_factory(reader_factory));
76+
let file_scan_config = FileScanConfigBuilder::from(file_scan_config.clone())
77+
.with_source(Arc::new(parquet_source))
78+
.build();
79+
let rewritten: Arc<dyn ExecutionPlan> = DataSourceExec::from_data_source(file_scan_config);
80+
Some(rewritten)
81+
}
82+
83+
#[derive(Debug)]
84+
struct InstrumentedParquetFileReaderFactory {
85+
inner: Arc<dyn ParquetFileReaderFactory>,
86+
}
87+
88+
impl ParquetFileReaderFactory for InstrumentedParquetFileReaderFactory {
89+
fn create_reader(
90+
&self,
91+
partition_index: usize,
92+
partitioned_file: PartitionedFile,
93+
metadata_size_hint: Option<usize>,
94+
metrics: &ExecutionPlanMetricsSet,
95+
) -> DFResult<Box<dyn AsyncFileReader + Send>> {
96+
let cache_metrics = Arc::new(StorageCacheMetrics::default());
97+
let counters = StorageCacheMetricCounters::new(partition_index, metrics);
98+
let reader = self.inner.create_reader(
99+
partition_index,
100+
partitioned_file,
101+
metadata_size_hint,
102+
metrics,
103+
)?;
104+
Ok(Box::new(StorageCacheObservedReader {
105+
inner: reader,
106+
cache_metrics,
107+
counters,
108+
}))
109+
}
110+
}
111+
112+
#[derive(Clone)]
113+
struct StorageCacheMetricCounters {
114+
range_hit_bytes: Count,
115+
range_miss_bytes: Count,
116+
range_hit_count: Count,
117+
range_miss_count: Count,
118+
footer_hit_count: Count,
119+
footer_miss_count: Count,
120+
}
121+
122+
impl StorageCacheMetricCounters {
123+
fn new(partition_index: usize, metrics: &ExecutionPlanMetricsSet) -> Self {
124+
let builder = || MetricBuilder::new(metrics).with_type(MetricType::SUMMARY);
125+
Self {
126+
range_hit_bytes: builder().counter("parquet_range_cache_hit_bytes", partition_index),
127+
range_miss_bytes: builder().counter("parquet_range_cache_miss_bytes", partition_index),
128+
range_hit_count: builder().counter("parquet_range_cache_hit_count", partition_index),
129+
range_miss_count: builder().counter("parquet_range_cache_miss_count", partition_index),
130+
footer_hit_count: builder().counter("parquet_footer_cache_hit_count", partition_index),
131+
footer_miss_count: builder()
132+
.counter("parquet_footer_cache_miss_count", partition_index),
133+
}
134+
}
135+
136+
fn record_range_delta(&self, delta: StorageCacheMetricsSnapshot) {
137+
self.range_hit_bytes.add(delta.hit_bytes);
138+
self.range_miss_bytes.add(delta.miss_bytes);
139+
self.range_hit_count.add(delta.hit_count);
140+
self.range_miss_count.add(delta.miss_count);
141+
}
142+
143+
fn record_footer_observation(&self, delta: StorageCacheMetricsSnapshot) {
144+
if delta.hit_count == 0 && delta.miss_count == 0 {
145+
self.footer_hit_count.add(1);
146+
} else {
147+
self.footer_miss_count.add(1);
148+
}
149+
}
150+
}
151+
152+
struct StorageCacheObservedReader {
153+
inner: Box<dyn AsyncFileReader + Send>,
154+
cache_metrics: Arc<StorageCacheMetrics>,
155+
counters: StorageCacheMetricCounters,
156+
}
157+
158+
fn observe_storage_cache_activity<'a, T>(
159+
cache_metrics: Arc<StorageCacheMetrics>,
160+
counters: StorageCacheMetricCounters,
161+
before: StorageCacheMetricsSnapshot,
162+
future: BoxFuture<'a, parquet::errors::Result<T>>,
163+
) -> BoxFuture<'a, parquet::errors::Result<T>>
164+
where
165+
T: Send + 'a,
166+
{
167+
async move {
168+
let result = with_storage_cache_metrics(Arc::clone(&cache_metrics), future).await;
169+
let delta = cache_metrics.snapshot().saturating_delta_since(before);
170+
counters.record_range_delta(delta);
171+
result
172+
}
173+
.boxed()
174+
}
175+
176+
fn observe_footer_cache_activity<'a>(
177+
cache_metrics: Arc<StorageCacheMetrics>,
178+
counters: StorageCacheMetricCounters,
179+
before: StorageCacheMetricsSnapshot,
180+
future: BoxFuture<'a, parquet::errors::Result<Arc<ParquetMetaData>>>,
181+
) -> BoxFuture<'a, parquet::errors::Result<Arc<ParquetMetaData>>> {
182+
async move {
183+
let result = with_storage_cache_metrics(Arc::clone(&cache_metrics), future).await;
184+
let delta = cache_metrics.snapshot().saturating_delta_since(before);
185+
counters.record_range_delta(delta);
186+
if result.is_ok() {
187+
counters.record_footer_observation(delta);
188+
}
189+
result
190+
}
191+
.boxed()
192+
}
193+
194+
impl AsyncFileReader for StorageCacheObservedReader {
195+
fn get_bytes(&mut self, range: Range<u64>) -> BoxFuture<'_, parquet::errors::Result<Bytes>> {
196+
let before = self.cache_metrics.snapshot();
197+
let cache_metrics = Arc::clone(&self.cache_metrics);
198+
let counters = self.counters.clone();
199+
let future = self.inner.get_bytes(range);
200+
observe_storage_cache_activity(cache_metrics, counters, before, future)
201+
}
202+
203+
fn get_byte_ranges(
204+
&mut self,
205+
ranges: Vec<Range<u64>>,
206+
) -> BoxFuture<'_, parquet::errors::Result<Vec<Bytes>>>
207+
where
208+
Self: Send,
209+
{
210+
let before = self.cache_metrics.snapshot();
211+
let cache_metrics = Arc::clone(&self.cache_metrics);
212+
let counters = self.counters.clone();
213+
let future = self.inner.get_byte_ranges(ranges);
214+
observe_storage_cache_activity(cache_metrics, counters, before, future)
215+
}
216+
217+
fn get_metadata<'a>(
218+
&'a mut self,
219+
options: Option<&'a ArrowReaderOptions>,
220+
) -> BoxFuture<'a, parquet::errors::Result<Arc<ParquetMetaData>>> {
221+
let before = self.cache_metrics.snapshot();
222+
let cache_metrics = Arc::clone(&self.cache_metrics);
223+
let counters = self.counters.clone();
224+
let future = self.inner.get_metadata(options);
225+
observe_footer_cache_activity(cache_metrics, counters, before, future)
226+
}
227+
}

quickwit/quickwit-datafusion/src/sources/metrics/table_provider.rs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ use regex_automata::dfa::{Automaton, dense};
5050
use regex_automata::{Anchored, Input};
5151
use tracing::debug;
5252

53+
use super::parquet_cache_metrics::instrument_parquet_file_reader_factory;
5354
use super::predicate;
5455

5556
const METRICS_SORT_ORDER: &[&str] = &[
@@ -278,9 +279,8 @@ impl TableProvider for MetricsTableProvider {
278279
let object_store = state
279280
.runtime_env()
280281
.object_store(self.object_store_url.clone())?;
281-
let reader_factory = Arc::new(CachedParquetFileReaderFactory::new(
282-
object_store,
283-
metadata_cache,
282+
let reader_factory = instrument_parquet_file_reader_factory(Arc::new(
283+
CachedParquetFileReaderFactory::new(object_store, metadata_cache),
284284
));
285285
let parquet_source = ParquetSource::new(table_schema)
286286
.with_bloom_filter_on_read(true)
@@ -663,9 +663,8 @@ mod tests {
663663
ParquetSplitId, TAG_ENV, TAG_HOST, TAG_SERVICE, TimeRange,
664664
};
665665

666-
use crate::sources::metrics::test_utils::TestSplitProvider;
667-
668666
use super::*;
667+
use crate::sources::metrics::test_utils::TestSplitProvider;
669668

670669
fn schema_with_columns(columns: &[&str]) -> SchemaRef {
671670
let fields = columns

quickwit/quickwit-serve/src/datafusion_api/setup.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ use quickwit_datafusion::grpc::DataFusionServiceGrpcImpl;
3737
use quickwit_datafusion::proto::data_fusion_service_server::{
3838
DataFusionServiceServer, SERVICE_NAME as DATAFUSION_SERVICE_NAME,
3939
};
40-
use quickwit_datafusion::sources::metrics::MetricsDataSource;
40+
use quickwit_datafusion::sources::metrics::{
41+
MetricsDataSource, instrument_parquet_range_cache_metrics,
42+
};
4143
use quickwit_datafusion::{
4244
DataFusionService, DataFusionSessionBuilder, QuickwitObjectStoreRegistry,
4345
QuickwitWorkerResolver, build_worker,
@@ -265,7 +267,8 @@ impl DataFusionMount {
265267
.max_decoding_message_size(self.max_message_size_bytes)
266268
.max_encoding_message_size(self.max_message_size_bytes);
267269

268-
let worker = build_worker(session_builder);
270+
let mut worker = build_worker(session_builder);
271+
worker.add_on_plan_hook(instrument_parquet_range_cache_metrics);
269272

270273
router
271274
.add_service(query_server)

quickwit/quickwit-storage/src/cache/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ mod byte_range_cache;
1717
mod memory_sized_cache;
1818
mod quickwit_cache;
1919
mod slice_address;
20+
mod storage_cache_metrics;
2021
mod storage_with_cache;
2122
mod stored_item;
2223

@@ -30,6 +31,10 @@ pub use storage_with_cache::StorageWithCache;
3031

3132
pub use self::byte_range_cache::ByteRangeCache;
3233
pub use self::memory_sized_cache::MemorySizedCache;
34+
pub use self::storage_cache_metrics::{
35+
StorageCacheMetrics, StorageCacheMetricsSnapshot, with_storage_cache_metrics,
36+
};
37+
pub(crate) use self::storage_cache_metrics::{record_storage_cache_hit, record_storage_cache_miss};
3338
use crate::{OwnedBytes, Storage};
3439

3540
/// Wraps the given directory with a slice cache that is actually global

0 commit comments

Comments
 (0)