mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-09 22:59:59 +00:00
feat(listobjects): add guarded metadata-fast listing (#4311)
* feat(listobjects): add phase 0 observability metrics * perf(listobjects): reduce gather metadata decoding * test(listobjects): add adversarial listing coverage * feat(listobjects): add source-aware cursor fields * feat(listobjects): define index source modes * feat(listobjects): model index rebuild health * feat(listobjects): add opt-in index fallback gate * feat(listobjects): verify index candidate pages * docs(listobjects): add rollout runbook * docs(listobjects): untrack baseline fixtures * feat(listobjects): add opt-in key-only provider * feat(listobjects): add index verification metrics * perf(listobjects): gate list metrics on get stage switch * feat(listobjects): bind provider health generation * feat(listobjects): add persistent key-only provider * feat(listobjects): bind persistent provider lifecycle * feat(listobjects): track persistent index mutations * feat(listobjects): persist index mutation checkpoints * fix(listobjects): aggregate benchmark metric snapshots * feat(listobjects): store journal in system namespace * chore(listobjects): report fallback reasons in bench * test(listobjects): verify metadata-fast stale fallbacks * feat(listobjects): serve metadata-fast snapshots behind guardrails * test(listobjects): add metadata-fast chaos bench * fix(listobjects): attribute metadata-fast fallback metrics * test(listobjects): add metadata-fast fallback chaos probes * test(listobjects): add quorum journal chaos guardrails * fix(listobjects): satisfy pre-pr clippy gate * docs(listobjects): untrack rollout runbook * fix(ecstore): remove redundant heal error conversion * test(filemeta): satisfy replication info clippy ---- Co-Authored-By: heihutu <heihutu@gmail.com>
This commit is contained in:
@@ -13,11 +13,14 @@
|
||||
// limitations under the License.
|
||||
|
||||
use rustfs_io_metrics::{
|
||||
record_cgroup_memory_split, record_cpu_usage, record_memory_usage, record_process_memory_split,
|
||||
snapshot_process_resource_and_system,
|
||||
AllocatorMemoryObservation, record_allocator_memory_observation, record_cgroup_memory_split, record_cpu_usage,
|
||||
record_memory_usage, record_process_memory_split, snapshot_process_resource_and_system,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
use std::ffi::CStr;
|
||||
use std::path::Path;
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
use std::time::Duration;
|
||||
@@ -127,6 +130,12 @@ struct CgroupMemorySnapshot {
|
||||
inactive_file_bytes: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
struct AllocatorMemorySnapshot {
|
||||
backend: &'static str,
|
||||
observation: AllocatorMemoryObservation,
|
||||
}
|
||||
|
||||
fn memory_system() -> &'static Mutex<System> {
|
||||
MEMORY_SYSTEM.get_or_init(|| Mutex::new(System::new()))
|
||||
}
|
||||
@@ -204,6 +213,94 @@ fn read_cgroup_memory_snapshot() -> Option<CgroupMemorySnapshot> {
|
||||
read_cgroup_v2().or_else(read_cgroup_v1)
|
||||
}
|
||||
|
||||
fn numeric_json_value(value: &Value) -> Option<u64> {
|
||||
match value {
|
||||
Value::Number(number) => number
|
||||
.as_u64()
|
||||
.or_else(|| number.as_i64().and_then(|value| u64::try_from(value).ok())),
|
||||
Value::String(value) => value.parse::<u64>().ok(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn numeric_json_field(value: &Value, field: &str) -> Option<u64> {
|
||||
match value {
|
||||
Value::Object(fields) => fields
|
||||
.get(field)
|
||||
.and_then(numeric_json_value)
|
||||
.or_else(|| fields.values().find_map(|value| numeric_json_field(value, field))),
|
||||
Value::Array(values) => values.iter().find_map(|value| numeric_json_field(value, field)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn mimalloc_stat_field(value: &Value, metric: &str, field: &str) -> Option<u64> {
|
||||
match value {
|
||||
Value::Object(fields) => {
|
||||
if let Some(metric_value) = fields.get(metric)
|
||||
&& let Some(value) = numeric_json_value(metric_value).or_else(|| numeric_json_field(metric_value, field))
|
||||
{
|
||||
return Some(value);
|
||||
}
|
||||
|
||||
fields.values().find_map(|value| mimalloc_stat_field(value, metric, field))
|
||||
}
|
||||
Value::Array(values) => values.iter().find_map(|value| mimalloc_stat_field(value, metric, field)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn mimalloc_stat_current(value: &Value, metric: &str) -> Option<u64> {
|
||||
mimalloc_stat_field(value, metric, "current")
|
||||
}
|
||||
|
||||
fn parse_mimalloc_stats_json(stats_json: &str) -> Option<AllocatorMemoryObservation> {
|
||||
let value = serde_json::from_str::<Value>(stats_json).ok()?;
|
||||
let observation = AllocatorMemoryObservation {
|
||||
reserved_bytes: mimalloc_stat_current(&value, "reserved"),
|
||||
committed_bytes: mimalloc_stat_current(&value, "committed"),
|
||||
page_committed_bytes: mimalloc_stat_current(&value, "page_committed"),
|
||||
malloc_requested_bytes: mimalloc_stat_current(&value, "malloc_requested"),
|
||||
malloc_requested_peak_bytes: mimalloc_stat_field(&value, "malloc_requested", "peak"),
|
||||
malloc_requested_total_bytes: mimalloc_stat_field(&value, "malloc_requested", "total"),
|
||||
heap_count: mimalloc_stat_current(&value, "heaps").or_else(|| mimalloc_stat_current(&value, "heap_count")),
|
||||
};
|
||||
|
||||
if observation == AllocatorMemoryObservation::default() {
|
||||
None
|
||||
} else {
|
||||
Some(observation)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
#[allow(unsafe_code)]
|
||||
fn read_allocator_memory_snapshot() -> Option<AllocatorMemorySnapshot> {
|
||||
// SAFETY: `mi_stats_get_json` returns a null-terminated JSON buffer owned by
|
||||
// mimalloc when called with a null input buffer. The mimalloc API requires
|
||||
// freeing that buffer with `mi_free`, which is done before returning.
|
||||
let stats = unsafe {
|
||||
let stats_ptr = libmimalloc_sys::mi_stats_get_json(0, std::ptr::null_mut());
|
||||
if stats_ptr.is_null() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let stats = CStr::from_ptr(stats_ptr).to_str().ok().map(str::to_owned);
|
||||
libmimalloc_sys::mi_free(stats_ptr.cast());
|
||||
stats?
|
||||
};
|
||||
let observation = parse_mimalloc_stats_json(&stats)?;
|
||||
Some(AllocatorMemorySnapshot {
|
||||
backend: crate::allocator_reclaim::allocator_backend(),
|
||||
observation,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn read_allocator_memory_snapshot() -> Option<AllocatorMemorySnapshot> {
|
||||
None
|
||||
}
|
||||
|
||||
fn configured_memory_observability_interval_secs() -> u64 {
|
||||
rustfs_utils::get_env_u64(ENV_MEMORY_OBSERVABILITY_INTERVAL_SECS, DEFAULT_MEMORY_OBSERVABILITY_INTERVAL_SECS).max(1)
|
||||
}
|
||||
@@ -276,11 +373,12 @@ async fn record_memory_snapshot() {
|
||||
let (resource, process) = snapshot_process_resource_and_system();
|
||||
let total_memory = refresh_total_memory();
|
||||
let cgroup = read_cgroup_memory_snapshot();
|
||||
(resource, process, total_memory, cgroup)
|
||||
let allocator = read_allocator_memory_snapshot();
|
||||
(resource, process, total_memory, cgroup, allocator)
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok((resource, process, total_memory, cgroup)) => {
|
||||
Ok((resource, process, total_memory, cgroup, allocator)) => {
|
||||
record_memory_usage(process.resident_memory_bytes, total_memory);
|
||||
record_cpu_usage(resource.cpu_percent);
|
||||
record_process_memory_split(process.resident_memory_bytes, process.virtual_memory_bytes);
|
||||
@@ -295,6 +393,10 @@ async fn record_memory_snapshot() {
|
||||
cgroup.inactive_file_bytes,
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(allocator) = allocator {
|
||||
record_allocator_memory_observation(allocator.backend, allocator.observation);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
debug!(error = ?err, "memory observability sampler task failed");
|
||||
@@ -330,7 +432,7 @@ mod tests {
|
||||
CgroupMemorySnapshot, MEMORY_OBSERVABILITY_SERVICE_NAME, MemoryObservabilityCancellationSource,
|
||||
MemoryObservabilityController, MemoryObservabilityDesiredState, MemoryObservabilityServiceState,
|
||||
MemoryObservabilityShutdownHandle, MemoryObservabilityWorkerMutation, build_memory_observability_controller_snapshot,
|
||||
build_memory_observability_status_snapshot, parse_kv_stats, read_optional_u64,
|
||||
build_memory_observability_status_snapshot, parse_kv_stats, parse_mimalloc_stats_json, read_optional_u64,
|
||||
};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
@@ -366,6 +468,39 @@ mod tests {
|
||||
assert_eq!(snapshot.inactive_file_bytes, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_mimalloc_stats_json_extracts_allocator_attribution() {
|
||||
let parsed = parse_mimalloc_stats_json(
|
||||
r#"{
|
||||
"process": {
|
||||
"reserved": { "current": 1048576 },
|
||||
"committed": { "current": "524288" },
|
||||
"page_committed": { "current": 262144 },
|
||||
"malloc_requested": {
|
||||
"current": 131072,
|
||||
"peak": 196608,
|
||||
"total": 10485760
|
||||
},
|
||||
"heaps": { "current": 8 }
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.expect("mimalloc stats should parse");
|
||||
|
||||
assert_eq!(parsed.reserved_bytes, Some(1_048_576));
|
||||
assert_eq!(parsed.committed_bytes, Some(524_288));
|
||||
assert_eq!(parsed.page_committed_bytes, Some(262_144));
|
||||
assert_eq!(parsed.malloc_requested_bytes, Some(131_072));
|
||||
assert_eq!(parsed.malloc_requested_peak_bytes, Some(196_608));
|
||||
assert_eq!(parsed.malloc_requested_total_bytes, Some(10_485_760));
|
||||
assert_eq!(parsed.heap_count, Some(8));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_mimalloc_stats_json_rejects_unrecognized_payload() {
|
||||
assert_eq!(parse_mimalloc_stats_json(r#"{ "allocator": "unknown" }"#), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn memory_observability_snapshot_reports_disabled_when_metrics_are_disabled() {
|
||||
let snapshot = build_memory_observability_status_snapshot(false, 15, false);
|
||||
|
||||
@@ -78,6 +78,7 @@ impl Default for FS {
|
||||
impl FS {
|
||||
pub fn new() -> Self {
|
||||
rustfs_io_metrics::init_s3_metrics();
|
||||
rustfs_io_metrics::init_list_objects_metrics();
|
||||
Self {}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user