perf(ecstore): reuse erasure codecs on GET paths (#6074)

* perf(ecstore): share legacy SIMD workspaces

Reuse legacy Reed-Solomon encoder and decoder workspaces across Erasure instances with the same shard layout while keeping active codecs request-exclusive.

Co-Authored-By: heihutu <heihutu@gmail.com>

* perf(ecstore): reuse GET erasure shells and scratch buffers

Co-Authored-By: heihutu <heihutu@gmail.com>

* test(ecstore): satisfy concurrent codec lint

Co-Authored-By: heihutu <heihutu@gmail.com>

* perf(ecstore): bound cached legacy workspaces

Co-Authored-By: heihutu <heihutu@gmail.com>

* perf(ecstore): cap retained legacy codec memory

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
This commit is contained in:
houseme
2026-08-14 01:51:42 +08:00
committed by GitHub
parent e16c07b9cd
commit 6178083985
5 changed files with 472 additions and 72 deletions
+140
View File
@@ -2792,6 +2792,9 @@ pub struct SetDisks {
get_object_metadata_cache: moka::future::Cache<GetObjectMetadataCacheKey, Arc<GetObjectMetadataCacheEntry>>,
get_object_metadata_cache_hash_builder: std::collections::hash_map::RandomState,
get_object_metadata_cache_generations: Arc<[AtomicU64]>,
/// GET codecs keyed by every persisted layout dimension that affects
/// decoding. Clones of a set share the memoized shells.
erasure_cache: Arc<ErasureCache>,
pub lockers: Vec<Arc<dyn LockClient>>,
shared_lockers: Arc<[Arc<dyn LockClient>]>,
local_lock_manager: Arc<rustfs_lock::GlobalLockManager>,
@@ -2814,6 +2817,137 @@ pub struct SetDisks {
storage_class_config_override: Arc<std::sync::RwLock<Option<Arc<storageclass::Config>>>>,
}
const ERASURE_CACHE_MAX_ENTRIES: usize = 32;
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
struct ErasureCacheKey {
data_shards: usize,
parity_shards: usize,
block_size: usize,
uses_legacy: bool,
}
struct ErasureCache {
entries: parking_lot::RwLock<HashMap<ErasureCacheKey, Arc<coding::Erasure>>>,
}
impl std::fmt::Debug for ErasureCache {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("ErasureCache")
.field("entries", &self.entries.read().len())
.finish()
}
}
impl ErasureCache {
fn new() -> Self {
Self {
entries: parking_lot::RwLock::new(HashMap::new()),
}
}
fn get_or_try_insert(
&self,
key: ErasureCacheKey,
) -> std::result::Result<Arc<coding::Erasure>, coding::ErasureConstructionError> {
if let Some(erasure) = self.entries.read().get(&key) {
return Ok(Arc::clone(erasure));
}
// Serialize first construction for a key so concurrent cold GETs still
// create exactly one shell. Codec construction never awaits.
let mut entries = self.entries.write();
if let Some(erasure) = entries.get(&key) {
return Ok(Arc::clone(erasure));
}
let erasure = Arc::new(coding::Erasure::try_new_with_options(
key.data_shards,
key.parity_shards,
key.block_size,
key.uses_legacy,
)?);
if entries.len() < ERASURE_CACHE_MAX_ENTRIES {
entries.insert(key, Arc::clone(&erasure));
}
Ok(erasure)
}
fn get_for_file_info(&self, fi: &FileInfo) -> Result<Arc<coding::Erasure>> {
self.get_or_try_insert(ErasureCacheKey {
data_shards: fi.erasure.data_blocks,
parity_shards: fi.erasure.parity_blocks,
block_size: fi.erasure.block_size,
uses_legacy: fi.uses_legacy_checksum,
})
.map_err(Error::from)
}
}
#[cfg(test)]
mod erasure_cache_tests {
use super::*;
#[test]
fn reuses_shells_and_keeps_every_layout_dimension_in_the_key() {
let cache = ErasureCache::new();
let base = ErasureCacheKey {
data_shards: 4,
parity_shards: 2,
block_size: 1_048_576,
uses_legacy: false,
};
let first = cache.get_or_try_insert(base).expect("modern shell should construct");
let reused = cache.get_or_try_insert(base).expect("same modern shell should be cached");
assert!(Arc::ptr_eq(&first, &reused));
for distinct in [
ErasureCacheKey { data_shards: 3, ..base },
ErasureCacheKey {
parity_shards: 1,
..base
},
ErasureCacheKey {
block_size: 524_288,
..base
},
ErasureCacheKey {
uses_legacy: true,
..base
},
] {
let shell = cache.get_or_try_insert(distinct).expect("distinct shell should construct");
assert!(!Arc::ptr_eq(&first, &shell));
}
assert_eq!(cache.entries.read().len(), 5);
}
#[test]
fn does_not_cache_invalid_layouts_or_grow_past_the_bound() {
let cache = ErasureCache::new();
let invalid = ErasureCacheKey {
data_shards: 4,
parity_shards: 2,
block_size: 0,
uses_legacy: false,
};
assert!(cache.get_or_try_insert(invalid).is_err());
assert!(cache.entries.read().is_empty());
for block_size in 1..=(ERASURE_CACHE_MAX_ENTRIES + 1) {
cache
.get_or_try_insert(ErasureCacheKey {
data_shards: 4,
parity_shards: 2,
block_size,
uses_legacy: false,
})
.expect("bounded cache fixture should construct");
}
assert_eq!(cache.entries.read().len(), ERASURE_CACHE_MAX_ENTRIES);
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct GetObjectMetadataCacheKey {
bucket: Arc<str>,
@@ -3212,6 +3346,7 @@ impl SetDisks {
.map(|_| AtomicU64::new(0))
.collect::<Vec<_>>(),
),
erasure_cache: Arc::new(ErasureCache::new()),
lockers,
shared_lockers,
// Sourced from the instance context so each instance owns its lock
@@ -9816,6 +9951,7 @@ mod tests {
let body = SetDisks::try_get_object_direct_data_shards_with_fileinfo(
"bucket",
"object",
Arc::new(ErasureCache::new()),
&fi,
&disk_files,
&disks,
@@ -9879,6 +10015,7 @@ mod tests {
let body = SetDisks::try_get_object_direct_data_shards_with_fileinfo(
"bucket",
"object",
Arc::new(ErasureCache::new()),
&fi,
&disk_files,
&vec![Some(disk); erasure.total_shard_count()],
@@ -9959,6 +10096,7 @@ mod tests {
let body = SetDisks::try_get_object_direct_data_shards_with_fileinfo(
bucket,
object,
Arc::new(ErasureCache::new()),
&fi,
&files,
&disks,
@@ -10044,6 +10182,7 @@ mod tests {
SetDisks::get_object_with_fileinfo(
bucket,
object,
Arc::new(ErasureCache::new()),
range_offset,
range_length as i64,
&mut writer,
@@ -10155,6 +10294,7 @@ mod tests {
SetDisks::get_object_with_fileinfo(
bucket,
object,
Arc::new(ErasureCache::new()),
0,
total_size as i64,
&mut writer,