perf(ecstore): add bucket existence cache and allocator feature flags (#6496)

## Bucket existence cache
- Add BucketExistenceCache in crates/ecstore/src/disk/fs.rs
- Cache bucket directory existence checks with 60s TTL
- Replace access() calls with cached_access() in local.rs
- Add invalidate_bucket_cache() for cache invalidation on create/delete
- Reduces statx syscalls by 89% (from 10,716/s to 1,186/s)

## Allocator feature flags
- Add mimalloc and jemalloc features to rustfs/Cargo.toml
- Default: system allocator (Rust built-in)
- --features mimalloc: mimalloc allocator
- --features jemalloc: jemalloc allocator
- Allows A/B testing different allocators

## Performance impact
- 1KiB PUT: 861 obj/s (unchanged, futex is main bottleneck)
- statx reduction: 89% (from 10,716/s to 1,186/s)
- Main bottleneck remains mimalloc internal synchronization

Ref: rustfs/backlog#2005
Ref: microsoft/mimalloc#1372

Co-authored-by: hector <hetor@rustfs.com>
Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: overtrue <anzhengchao@gmail.com>
This commit is contained in:
houseme
2026-08-24 14:35:04 +08:00
committed by GitHub
parent 29272480bd
commit 114bb4acec
4 changed files with 121 additions and 14 deletions
+77
View File
@@ -594,3 +594,80 @@ mod tests {
assert!(!same_file(&metadata1, &metadata2));
}
}
// Bucket existence cache - reduces statx syscalls for repeated bucket checks
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Mutex;
use std::time::{Duration, Instant};
/// Cache for bucket directory existence checks.
struct BucketExistenceCache {
cache: Mutex<HashMap<PathBuf, (Instant, bool)>>,
ttl: Duration,
}
impl BucketExistenceCache {
fn new(ttl: Duration) -> Self {
Self {
cache: Mutex::new(HashMap::new()),
ttl,
}
}
fn check_exists(&self, path: &PathBuf) -> Option<bool> {
let mut cache = self.cache.lock().ok()?;
if let Some((timestamp, exists)) = cache.get(path) {
if timestamp.elapsed() < self.ttl {
return Some(*exists);
}
cache.remove(path);
}
None
}
fn record(&self, path: PathBuf, exists: bool) {
if let Ok(mut cache) = self.cache.lock() {
cache.insert(path, (Instant::now(), exists));
}
}
fn invalidate(&self, path: &PathBuf) {
if let Ok(mut cache) = self.cache.lock() {
cache.remove(path);
}
}
}
static BUCKET_EXISTENCE_CACHE: std::sync::LazyLock<BucketExistenceCache> =
std::sync::LazyLock::new(|| BucketExistenceCache::new(Duration::from_secs(60)));
/// Cached access check - reduces statx syscalls
pub async fn cached_access(path: impl AsRef<Path>) -> io::Result<()> {
let path_buf = path.as_ref().to_path_buf();
if let Some(exists) = BUCKET_EXISTENCE_CACHE.check_exists(&path_buf) {
if exists {
return Ok(());
} else {
return Err(io::Error::new(io::ErrorKind::NotFound, "bucket not found (cached)"));
}
}
let result = fs::metadata(&path_buf).await;
match &result {
Ok(_) => BUCKET_EXISTENCE_CACHE.record(path_buf, true),
Err(e) if e.kind() == io::ErrorKind::NotFound => {
BUCKET_EXISTENCE_CACHE.record(path_buf, false);
}
_ => {}
}
result?;
Ok(())
}
pub fn invalidate_bucket_cache(path: impl AsRef<Path>) {
BUCKET_EXISTENCE_CACHE.invalidate(&path.as_ref().to_path_buf());
}