Skip to content

Commit 1ac566f

Browse files
authored
Oximeter: add queue depth metric. (#10736)
In #10683, we added a metric counting the number of samples evicted from the oximeter collector's database_batcher queue. Operators can use that metric to detect that samples are being lost, but not that samples are potentially about to be lost. In other words, we have to wait for something to go wrong to take action. This patch adds a queue depth metric to the collector as well: a histogram recording the current length of the queue each time we push a new batch of samples onto it. Operators can use this metric to detect when the queue depth is approaching its maximum length. Part of #10552. Note: builds on #10683. Can just review the second commit, or review both commits here and ignore #10683. And I promise I'm almost done thinking about #10552. This is one of the last papercuts before I'll feel like this is sorted.
1 parent f006c42 commit 1ac566f

6 files changed

Lines changed: 245 additions & 21 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

oximeter/collector/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ nexus-client.workspace = true
5050
expectorate.workspace = true
5151
httpmock.workspace = true
5252
omicron-test-utils.workspace = true
53+
oximeter-test-utils.workspace = true
5354
openapi-lint.workspace = true
5455
openapiv3.workspace = true
5556
proptest.workspace = true

oximeter/collector/src/agent.rs

Lines changed: 86 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ use nexus_client::Client as NexusClient;
2121
use nexus_client::types::IdSortMode;
2222
use omicron_common::backoff;
2323
use omicron_common::backoff::BackoffError;
24+
use oximeter::Sample;
25+
use oximeter::types::ProducerResultsItem;
2426
use oximeter_db::Client;
2527
use oximeter_db::DbWrite;
2628
use oximeter_types::producer::ProducerDetails;
@@ -45,6 +47,7 @@ use std::sync::Mutex;
4547
use std::sync::MutexGuard;
4648
use std::time::Duration;
4749
use tokio::sync::mpsc;
50+
use tokio::time::interval;
4851
use uuid::Uuid;
4952

5053
/// The internal agent the oximeter server uses to collect metrics from producers.
@@ -93,7 +96,7 @@ impl OximeterAgent {
9396
"collector_ip" => address.ip().to_string(),
9497
));
9598
let insertion_log = log.new(o!("component" => "results-sink"));
96-
let instertion_log_cluster =
99+
let insertion_log_cluster =
97100
log.new(o!("component" => "results-sink-cluster"));
98101

99102
// Determine the version of the database.
@@ -139,17 +142,23 @@ impl OximeterAgent {
139142
collector_port: address.port(),
140143
};
141144

145+
let collector_stats = self_stats::CollectorStats::new(Utc::now());
146+
142147
// Spawn the task for aggregating and inserting all metrics to a
143148
// single node ClickHouse installation.
144-
tokio::spawn(async move {
145-
crate::results_sink::database_batcher(
146-
insertion_log,
147-
client,
148-
db_config.batch_size,
149-
Duration::from_secs(db_config.batch_interval),
150-
collection_task_wrapper.single_rx,
151-
)
152-
.await
149+
tokio::spawn({
150+
let sink_stats = collector_stats.single_stats.clone();
151+
async move {
152+
crate::results_sink::database_batcher(
153+
insertion_log,
154+
client,
155+
db_config.batch_size,
156+
Duration::from_secs(db_config.batch_interval),
157+
collection_task_wrapper.single_rx,
158+
sink_stats,
159+
)
160+
.await
161+
}
153162
});
154163

155164
// Our internal testing rack will be running a ClickHouse cluster
@@ -184,28 +193,35 @@ impl OximeterAgent {
184193

185194
// Spawn the task for aggregating and inserting all metrics to a
186195
// replicated cluster ClickHouse installation
187-
tokio::spawn(async move {
196+
tokio::spawn({
188197
results_sink::database_batcher(
189-
instertion_log_cluster,
198+
insertion_log_cluster,
190199
cluster_client,
191200
db_config.batch_size,
192201
Duration::from_secs(db_config.batch_interval),
193202
collection_task_wrapper.cluster_rx,
203+
collector_stats.cluster_stats.clone(),
194204
)
195-
.await
196205
});
197206

198207
let self_ = Self {
199208
id,
200-
log,
209+
log: log.clone(),
201210
collection_target,
202-
result_sender: collection_task_wrapper.wrapper_tx,
211+
result_sender: collection_task_wrapper.wrapper_tx.clone(),
203212
collection_tasks: Arc::new(Mutex::new(BTreeMap::new())),
204213
refresh_interval,
205214
refresh_task: Arc::new(Mutex::new(None)),
206215
last_refresh_time: Arc::new(Mutex::new(None)),
207216
};
208217

218+
tokio::spawn(emit_self_stats(
219+
collector_stats,
220+
collection_target,
221+
collection_task_wrapper.wrapper_tx,
222+
log,
223+
));
224+
209225
Ok(self_)
210226
}
211227

@@ -269,6 +285,11 @@ impl OximeterAgent {
269285
client.init_replicated_db().await?;
270286
}
271287

288+
let sink_stats = Arc::new(self_stats::CollectorSinkStats::new(
289+
"standalone".into(),
290+
Utc::now(),
291+
));
292+
272293
// Spawn the task for aggregating and inserting all metrics
273294
tokio::spawn(async move {
274295
results_sink::database_batcher(
@@ -277,6 +298,7 @@ impl OximeterAgent {
277298
db_config.batch_size,
278299
Duration::from_secs(db_config.batch_interval),
279300
collection_task_wrapper.single_rx,
301+
sink_stats,
280302
)
281303
.await
282304
});
@@ -524,6 +546,55 @@ impl CollectionTaskSenderWrapper {
524546
}
525547
}
526548

549+
// Emit stats about the collector agent.
550+
//
551+
// Note: If oximeter isn't able to write metrics to ClickHouse,
552+
// self-stats about oximeter's inability to write metrics will
553+
// also never reach ClickHouse. However, we may still be able to
554+
// emit these metrics if the write path to ClickHouse is partially
555+
// degraded. And because the relevant counters live in oximeter's
556+
// memory, even if ClickHouse becomes fully unavailable and then
557+
// recovers, the metrics will also eventually reach the database.
558+
//
559+
// TODO(#10552): Emit a metric about failed database writes as well.
560+
async fn emit_self_stats(
561+
collector_stats: self_stats::CollectorStats,
562+
collection_target: self_stats::OximeterCollector,
563+
wrapper_tx: CollectionTaskSenderWrapper,
564+
log: Logger,
565+
) {
566+
let mut sink_stats_ticker = interval(self_stats::COLLECTION_INTERVAL);
567+
loop {
568+
sink_stats_ticker.tick().await;
569+
570+
let mut samples: Vec<Sample> = Vec::new();
571+
572+
for sink_stats in
573+
[&collector_stats.single_stats, &collector_stats.cluster_stats]
574+
{
575+
match sink_stats.samples(&collection_target) {
576+
Ok(s) => samples.extend(s),
577+
Err(e) => error!(
578+
log,
579+
"failed to build database sink self-stats";
580+
"sink" => sink_stats.label.clone(),
581+
InlineErrorChain::new(&e),
582+
),
583+
};
584+
}
585+
586+
let _ = wrapper_tx
587+
.send(
588+
CollectionTaskOutput {
589+
results: vec![ProducerResultsItem::Ok(samples)],
590+
was_forced_collection: false,
591+
},
592+
&log,
593+
)
594+
.await;
595+
}
596+
}
597+
527598
#[derive(Debug)]
528599
pub struct CollectionTaskWrapper {
529600
wrapper_tx: CollectionTaskSenderWrapper,

oximeter/collector/src/results_sink.rs

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
1010
use crate::collection_task::CollectionTaskOutput;
1111
use crate::probes;
12+
use crate::self_stats;
1213
use oximeter::Sample;
14+
use oximeter::histogram::Record;
1315
use oximeter::queue::BoundedQueue;
1416
use oximeter::types::ProducerResultsItem;
1517
use oximeter_db::Client;
@@ -41,6 +43,7 @@ pub async fn database_batcher(
4143
batch_size: usize,
4244
batch_interval: Duration,
4345
mut rx: mpsc::Receiver<CollectionTaskOutput>,
46+
sink_stats: Arc<self_stats::CollectorSinkStats>,
4447
) {
4548
// Construct a handoff point between the batch task here, and the database
4649
// insertion task.
@@ -109,7 +112,7 @@ pub async fn database_batcher(
109112
probes::results__sink__item__processed!(|| n_samples);
110113

111114
// Append the current batch to the handoff buffer.
112-
let n_dropped =
115+
let (n_dropped, queue_depth) =
113116
batch_tx.send_and_notify(batch, was_forced_collection);
114117
if n_dropped > 0 {
115118
probes::dropped__old__samples!(|| n_dropped);
@@ -118,7 +121,13 @@ pub async fn database_batcher(
118121
"sample buffer full, dropped oldest samples";
119122
"n_dropped" => n_dropped,
120123
);
124+
*sink_stats.samples_dropped.lock().unwrap() += n_dropped.try_into().unwrap();
121125
}
126+
// Note: Histogram::u64::sample should never error.
127+
// The `sample` method only returns a `HistogramError`
128+
// if its argument is non-finite, and `u64` values are
129+
// always finite.
130+
sink_stats.queue_depth.lock().unwrap().sample(queue_depth.try_into().unwrap()).unwrap();
122131
}
123132
None => {
124133
warn!(log, "result queue closed, exiting");
@@ -195,7 +204,7 @@ impl<T> BatchSender<T> {
195204
&self,
196205
samples: Vec<T>,
197206
was_forced_collection: bool,
198-
) -> usize {
207+
) -> (usize, usize) {
199208
let mut batch = self.handoff.batch.lock().unwrap();
200209

201210
let n_dropped = batch.extend(samples);
@@ -204,7 +213,7 @@ impl<T> BatchSender<T> {
204213
if was_forced_collection || batch.len() >= self.batch_size {
205214
self.notify_inserter();
206215
}
207-
n_dropped
216+
(n_dropped, batch.len())
208217
}
209218
}
210219

@@ -311,6 +320,8 @@ pub async fn logger(log: Logger, mut rx: mpsc::Receiver<CollectionTaskOutput>) {
311320
#[cfg(test)]
312321
mod tests {
313322
use super::*;
323+
use chrono::Utc;
324+
use omicron_test_utils::dev::test_setup_log;
314325
use proptest::prelude::*;
315326
use tokio::time::timeout;
316327

@@ -322,7 +333,7 @@ mod tests {
322333
let (batch_tx, batch_rx) = batch_handoff::<()>(BATCH_SIZE);
323334
let n_samples = 3;
324335
let samples = vec![(); n_samples];
325-
let n_dropped = batch_tx.send_and_notify(samples, true);
336+
let (n_dropped, _) = batch_tx.send_and_notify(samples, true);
326337
assert_eq!(n_dropped, 0);
327338
assert_eq!(batch_tx.handoff.batch.lock().unwrap().len(), n_samples);
328339

@@ -348,7 +359,7 @@ mod tests {
348359
// Insert the first batch of samples.
349360
let (batch_tx, batch_rx) = batch_handoff::<usize>(BATCH_SIZE);
350361
let samples = (0..current_size).collect();
351-
let n_dropped = batch_tx.send_and_notify(samples, false);
362+
let (n_dropped, _) = batch_tx.send_and_notify(samples, false);
352363
let expected_n_dropped = current_size.saturating_sub(MAX_BUFFER_SIZE);
353364
let expected_n_samples = current_size.min(MAX_BUFFER_SIZE);
354365
assert_eq!(n_dropped, expected_n_dropped);
@@ -384,7 +395,7 @@ mod tests {
384395
// Push the new set of samples, which start at the end of the first
385396
// batch, even if we've dropped some.
386397
let new_samples = (current_size..(current_size + new_size)).collect();
387-
let n_dropped = batch_tx.send_and_notify(new_samples, false);
398+
let (n_dropped, _) = batch_tx.send_and_notify(new_samples, false);
388399

389400
// We expect to drop all the samples beyond the maximum buffer size.
390401
// We need to include what we have in the buffer already (some of
@@ -422,4 +433,51 @@ mod tests {
422433
}
423434
}
424435
}
436+
437+
#[tokio::test]
438+
async fn batcher_increments_dropped_counter() {
439+
let logctx = test_setup_log("batcher_increments_dropped_counter");
440+
let log = &logctx.log;
441+
442+
// Construct a database batcher with a dummy ClickHouse client.
443+
let client = Client::new("[::1]:0".parse().unwrap(), log);
444+
let (tx, rx) = mpsc::channel(1);
445+
let sink_stats = Arc::new(self_stats::CollectorSinkStats::new(
446+
"test".into(),
447+
Utc::now(),
448+
));
449+
let batcher = tokio::spawn(database_batcher(
450+
log.clone(),
451+
client,
452+
BATCH_SIZE,
453+
Duration::from_secs(3600),
454+
rx,
455+
sink_stats.clone(),
456+
));
457+
458+
// Insert `want_dropped` too many samples into the batcher.
459+
let want_dropped = 50;
460+
let samples = (0..MAX_BUFFER_SIZE + want_dropped)
461+
.map(|_| oximeter_test_utils::make_sample())
462+
.collect();
463+
tx.send(CollectionTaskOutput {
464+
results: vec![ProducerResultsItem::Ok(samples)],
465+
was_forced_collection: false,
466+
})
467+
.await
468+
.unwrap();
469+
470+
// Drop the sender so that the batcher loop exits.
471+
drop(tx);
472+
batcher.await.unwrap();
473+
474+
let samples_dropped = sink_stats.samples_dropped.lock().unwrap();
475+
assert_eq!(samples_dropped.value(), want_dropped as u64);
476+
477+
let queue_depth = sink_stats.queue_depth.lock().unwrap();
478+
assert_eq!(queue_depth.n_samples(), 1);
479+
assert_eq!(queue_depth.max(), MAX_BUFFER_SIZE as u64);
480+
481+
logctx.cleanup_successful();
482+
}
425483
}

0 commit comments

Comments
 (0)