mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-18 10:43:15 +00:00
29fbdc2dbf
* feat(ecstore): add object lock diagnostics Add configurable namespace lock diagnostics for object operations so production contention can be traced by operation, owner, and key. Wrap object read/write lock acquisition in diagnostic guards across get, head, put, delete, copy, and multipart flows, and log slow acquisition and long hold durations behind new RUSTFS_OBJECT_LOCK_DIAG_* settings. Verification: - make pre-commit * feat(obs): expose object lock diagnostics metrics Add Prometheus metrics and Grafana panels for object namespace lock diagnostics, covering slow acquire counts, slow hold counts, acquire duration, hold duration, and the diagnostics-enabled state. Adopt PR review feedback by keeping diagnostic guards alive through the guarded operation so long-hold warnings and metrics are emitted, reusing shared env helpers, and reducing default-path overhead when diagnostics are disabled. Verification: - cargo check -p rustfs-ecstore -p rustfs-io-metrics - cargo test -p rustfs-ecstore store::object -- --nocapture - make pre-commit * perf(obs): reduce object lock diag overhead Avoid repeated environment parsing on hot object-lock paths by caching the diagnostics-enabled flag, and stop allocating label strings for object lock metrics by recording static labels directly. Strengthen io-metrics tests by using a local recorder and asserting that the expected object lock diagnostic counters, gauges, and histograms are emitted. Verification: - cargo check -p rustfs-ecstore -p rustfs-io-metrics - cargo test -p rustfs-io-metrics -- --nocapture - make pre-commit
358 lines
11 KiB
Rust
358 lines
11 KiB
Rust
// Copyright 2024 RustFS Team
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
//! Lock optimization metrics recording functions.
|
|
|
|
use std::time::Duration;
|
|
|
|
/// Record lock optimization enabled.
|
|
#[inline(always)]
|
|
pub fn record_lock_optimization_enabled(enabled: bool) {
|
|
use metrics::gauge;
|
|
gauge!("rustfs_lock_optimization_enabled").set(if enabled { 1.0 } else { 0.0 });
|
|
}
|
|
|
|
/// Record spin attempt.
|
|
#[inline(always)]
|
|
pub fn record_spin_attempt(success: bool) {
|
|
use metrics::counter;
|
|
if success {
|
|
counter!("rustfs_lock_spin_successes").increment(1);
|
|
} else {
|
|
counter!("rustfs_lock_spin_failures").increment(1);
|
|
}
|
|
}
|
|
|
|
/// Record adaptive spin count change.
|
|
#[inline(always)]
|
|
pub fn record_spin_count_change(new_count: usize) {
|
|
use metrics::gauge;
|
|
gauge!("rustfs_lock_spin_count").set(new_count as f64);
|
|
}
|
|
|
|
/// Record lock hold time.
|
|
#[inline(always)]
|
|
pub fn record_lock_hold_time(hold_time: Duration) {
|
|
use metrics::histogram;
|
|
histogram!("rustfs_lock_hold_time_secs").record(hold_time.as_secs_f64());
|
|
}
|
|
|
|
/// Record early release.
|
|
#[inline(always)]
|
|
pub fn record_early_release() {
|
|
use metrics::counter;
|
|
counter!("rustfs_lock_early_releases").increment(1);
|
|
}
|
|
|
|
/// Record contention event.
|
|
#[inline(always)]
|
|
pub fn record_contention_event() {
|
|
use metrics::counter;
|
|
counter!("rustfs_lock_contentions").increment(1);
|
|
}
|
|
|
|
/// Record object namespace lock diagnostics being enabled.
|
|
#[inline(always)]
|
|
pub fn record_object_lock_diag_enabled(enabled: bool) {
|
|
use metrics::gauge;
|
|
gauge!("rustfs_object_lock_diag_enabled").set(if enabled { 1.0 } else { 0.0 });
|
|
}
|
|
|
|
/// Record object namespace lock acquire duration.
|
|
#[inline(always)]
|
|
pub fn record_object_lock_diag_acquire_duration(op: &'static str, mode: &'static str, duration: Duration) {
|
|
use metrics::histogram;
|
|
histogram!(
|
|
"rustfs_object_lock_diag_acquire_duration_seconds",
|
|
"op" => op,
|
|
"mode" => mode
|
|
)
|
|
.record(duration.as_secs_f64());
|
|
}
|
|
|
|
/// Record object namespace lock hold duration.
|
|
#[inline(always)]
|
|
pub fn record_object_lock_diag_hold_duration(op: &'static str, mode: &'static str, duration: Duration) {
|
|
use metrics::histogram;
|
|
histogram!(
|
|
"rustfs_object_lock_diag_hold_duration_seconds",
|
|
"op" => op,
|
|
"mode" => mode
|
|
)
|
|
.record(duration.as_secs_f64());
|
|
}
|
|
|
|
/// Record an object namespace lock slow-acquire event.
|
|
#[inline(always)]
|
|
pub fn record_object_lock_diag_slow_acquire(op: &'static str, mode: &'static str) {
|
|
use metrics::counter;
|
|
counter!(
|
|
"rustfs_object_lock_diag_slow_acquire_total",
|
|
"op" => op,
|
|
"mode" => mode
|
|
)
|
|
.increment(1);
|
|
}
|
|
|
|
/// Record an object namespace lock slow-hold event.
|
|
#[inline(always)]
|
|
pub fn record_object_lock_diag_slow_hold(op: &'static str, mode: &'static str) {
|
|
use metrics::counter;
|
|
counter!(
|
|
"rustfs_object_lock_diag_slow_hold_total",
|
|
"op" => op,
|
|
"mode" => mode
|
|
)
|
|
.increment(1);
|
|
}
|
|
|
|
/// Lock statistics summary.
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct LockMetricsSummary {
|
|
/// Total acquisitions.
|
|
pub acquisitions: u64,
|
|
/// Total releases.
|
|
pub releases: u64,
|
|
/// Spin successes.
|
|
pub spin_successes: u64,
|
|
/// Spin failures.
|
|
pub spin_failures: u64,
|
|
/// Early releases.
|
|
pub early_releases: u64,
|
|
/// Contentions.
|
|
pub contentions: u64,
|
|
}
|
|
|
|
impl LockMetricsSummary {
|
|
/// Create new summary.
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
/// Get spin success rate.
|
|
pub fn spin_success_rate(&self) -> f64 {
|
|
let total = self.spin_successes + self.spin_failures;
|
|
if total == 0 {
|
|
0.0
|
|
} else {
|
|
self.spin_successes as f64 / total as f64
|
|
}
|
|
}
|
|
|
|
/// Get contention rate.
|
|
pub fn contention_rate(&self) -> f64 {
|
|
if self.acquisitions == 0 {
|
|
0.0
|
|
} else {
|
|
self.contentions as f64 / self.acquisitions as f64
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use metrics::{Counter, CounterFn, Gauge, GaugeFn, Histogram, HistogramFn, Key, KeyName, Metadata, SharedString, Unit};
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
#[derive(Clone, Default)]
|
|
struct SeenMetricsRecorder {
|
|
counters: Arc<Mutex<Vec<Key>>>,
|
|
gauges: Arc<Mutex<Vec<Key>>>,
|
|
histograms: Arc<Mutex<Vec<Key>>>,
|
|
}
|
|
|
|
impl SeenMetricsRecorder {
|
|
fn saw_counter_named(&self, name: &str) -> bool {
|
|
self.counters
|
|
.lock()
|
|
.expect("counter key collection should be lockable")
|
|
.iter()
|
|
.any(|key| key.name() == name)
|
|
}
|
|
|
|
fn saw_gauge_named(&self, name: &str) -> bool {
|
|
self.gauges
|
|
.lock()
|
|
.expect("gauge key collection should be lockable")
|
|
.iter()
|
|
.any(|key| key.name() == name)
|
|
}
|
|
|
|
fn saw_histogram_named(&self, name: &str) -> bool {
|
|
self.histograms
|
|
.lock()
|
|
.expect("histogram key collection should be lockable")
|
|
.iter()
|
|
.any(|key| key.name() == name)
|
|
}
|
|
}
|
|
|
|
impl metrics::Recorder for SeenMetricsRecorder {
|
|
fn describe_counter(&self, _key: KeyName, _unit: Option<Unit>, _description: SharedString) {}
|
|
|
|
fn describe_gauge(&self, _key: KeyName, _unit: Option<Unit>, _description: SharedString) {}
|
|
|
|
fn describe_histogram(&self, _key: KeyName, _unit: Option<Unit>, _description: SharedString) {}
|
|
|
|
fn register_counter(&self, key: &Key, _metadata: &Metadata<'_>) -> Counter {
|
|
self.counters
|
|
.lock()
|
|
.expect("counter key collection should be lockable")
|
|
.push(key.clone());
|
|
Counter::from_arc(Arc::new(NoopCounter))
|
|
}
|
|
|
|
fn register_gauge(&self, key: &Key, _metadata: &Metadata<'_>) -> Gauge {
|
|
self.gauges
|
|
.lock()
|
|
.expect("gauge key collection should be lockable")
|
|
.push(key.clone());
|
|
Gauge::from_arc(Arc::new(NoopGauge))
|
|
}
|
|
|
|
fn register_histogram(&self, key: &Key, _metadata: &Metadata<'_>) -> Histogram {
|
|
self.histograms
|
|
.lock()
|
|
.expect("histogram key collection should be lockable")
|
|
.push(key.clone());
|
|
Histogram::from_arc(Arc::new(NoopHistogram))
|
|
}
|
|
}
|
|
|
|
struct NoopCounter;
|
|
|
|
impl CounterFn for NoopCounter {
|
|
fn increment(&self, _value: u64) {}
|
|
|
|
fn absolute(&self, _value: u64) {}
|
|
}
|
|
|
|
struct NoopGauge;
|
|
|
|
impl GaugeFn for NoopGauge {
|
|
fn increment(&self, _value: f64) {}
|
|
|
|
fn decrement(&self, _value: f64) {}
|
|
|
|
fn set(&self, _value: f64) {}
|
|
}
|
|
|
|
struct NoopHistogram;
|
|
|
|
impl HistogramFn for NoopHistogram {
|
|
fn record(&self, _value: f64) {}
|
|
}
|
|
|
|
#[test]
|
|
fn test_record_lock_optimization_enabled() {
|
|
record_lock_optimization_enabled(true);
|
|
record_lock_optimization_enabled(false);
|
|
}
|
|
|
|
#[test]
|
|
fn test_record_spin_attempt() {
|
|
record_spin_attempt(true);
|
|
record_spin_attempt(false);
|
|
}
|
|
|
|
#[test]
|
|
fn test_record_spin_count_change() {
|
|
record_spin_count_change(100);
|
|
record_spin_count_change(200);
|
|
}
|
|
|
|
#[test]
|
|
fn test_record_lock_hold_time() {
|
|
record_lock_hold_time(Duration::from_millis(10));
|
|
record_lock_hold_time(Duration::from_millis(100));
|
|
}
|
|
|
|
#[test]
|
|
fn test_record_early_release() {
|
|
record_early_release();
|
|
}
|
|
|
|
#[test]
|
|
fn test_record_contention_event() {
|
|
record_contention_event();
|
|
}
|
|
|
|
#[test]
|
|
fn test_record_object_lock_diag_enabled() {
|
|
let recorder = SeenMetricsRecorder::default();
|
|
metrics::with_local_recorder(&recorder, || {
|
|
record_object_lock_diag_enabled(true);
|
|
record_object_lock_diag_enabled(false);
|
|
});
|
|
assert!(
|
|
recorder.saw_gauge_named("rustfs_object_lock_diag_enabled"),
|
|
"expected object lock diagnostics enabled gauge to be emitted"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_record_object_lock_diag_acquire_duration() {
|
|
let recorder = SeenMetricsRecorder::default();
|
|
metrics::with_local_recorder(&recorder, || {
|
|
record_object_lock_diag_acquire_duration("get_object", "read", Duration::from_millis(10));
|
|
});
|
|
assert!(
|
|
recorder.saw_histogram_named("rustfs_object_lock_diag_acquire_duration_seconds"),
|
|
"expected object lock diagnostics acquire histogram to be emitted"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_record_object_lock_diag_hold_duration() {
|
|
let recorder = SeenMetricsRecorder::default();
|
|
metrics::with_local_recorder(&recorder, || {
|
|
record_object_lock_diag_hold_duration("put_object_commit", "write", Duration::from_millis(20));
|
|
});
|
|
assert!(
|
|
recorder.saw_histogram_named("rustfs_object_lock_diag_hold_duration_seconds"),
|
|
"expected object lock diagnostics hold histogram to be emitted"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_record_object_lock_diag_slow_events() {
|
|
let recorder = SeenMetricsRecorder::default();
|
|
metrics::with_local_recorder(&recorder, || {
|
|
record_object_lock_diag_slow_acquire("get_object_info", "read");
|
|
record_object_lock_diag_slow_hold("complete_multipart_upload_commit", "write");
|
|
});
|
|
assert!(
|
|
recorder.saw_counter_named("rustfs_object_lock_diag_slow_acquire_total"),
|
|
"expected object lock diagnostics slow-acquire counter to be emitted"
|
|
);
|
|
assert!(
|
|
recorder.saw_counter_named("rustfs_object_lock_diag_slow_hold_total"),
|
|
"expected object lock diagnostics slow-hold counter to be emitted"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_lock_metrics_summary() {
|
|
let mut summary = LockMetricsSummary::new();
|
|
summary.acquisitions = 100;
|
|
summary.spin_successes = 80;
|
|
summary.spin_failures = 20;
|
|
summary.contentions = 10;
|
|
|
|
assert!((summary.spin_success_rate() - 0.8).abs() < 0.01);
|
|
assert!((summary.contention_rate() - 0.1).abs() < 0.01);
|
|
}
|
|
}
|