mirror of
https://github.com/rustfs/rustfs.git
synced 2026-07-26 08:18:18 +00:00
feat(lock): Optimize lock management performance in high-concurrency scenarios (#523)
Increase the size of the notification pool to reduce the thundering herd effect under high concurrency Implement an adaptive timeout mechanism that dynamically adjusts based on system load and priority Add a lock protection mechanism to prevent premature cleanup of active locks Add lock acquisition methods for high-priority and critical-priority locks Improve the cleanup strategy to be more conservative under high load Add detailed debug logs to assist in diagnosing lock issues Signed-off-by: junxiang Mu <1948535941@qq.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -27,7 +27,7 @@ use crate::fast_lock::{
|
||||
/// High-performance object lock manager
|
||||
#[derive(Debug)]
|
||||
pub struct FastObjectLockManager {
|
||||
shards: Vec<Arc<LockShard>>,
|
||||
pub shards: Vec<Arc<LockShard>>,
|
||||
shard_mask: usize,
|
||||
config: LockConfig,
|
||||
metrics: Arc<GlobalMetrics>,
|
||||
@@ -66,7 +66,12 @@ impl FastObjectLockManager {
|
||||
pub async fn acquire_lock(&self, request: ObjectLockRequest) -> Result<FastLockGuard, LockResult> {
|
||||
let shard = self.get_shard(&request.key);
|
||||
match shard.acquire_lock(&request).await {
|
||||
Ok(()) => Ok(FastLockGuard::new(request.key, request.mode, request.owner, shard.clone())),
|
||||
Ok(()) => {
|
||||
let guard = FastLockGuard::new(request.key, request.mode, request.owner, shard.clone());
|
||||
// Register guard to prevent premature cleanup
|
||||
shard.register_guard(guard.guard_id());
|
||||
Ok(guard)
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
@@ -117,6 +122,54 @@ impl FastObjectLockManager {
|
||||
self.acquire_lock(request).await
|
||||
}
|
||||
|
||||
/// Acquire high-priority read lock - optimized for database queries
|
||||
pub async fn acquire_high_priority_read_lock(
|
||||
&self,
|
||||
bucket: impl Into<Arc<str>>,
|
||||
object: impl Into<Arc<str>>,
|
||||
owner: impl Into<Arc<str>>,
|
||||
) -> Result<FastLockGuard, LockResult> {
|
||||
let request =
|
||||
ObjectLockRequest::new_read(bucket, object, owner).with_priority(crate::fast_lock::types::LockPriority::High);
|
||||
self.acquire_lock(request).await
|
||||
}
|
||||
|
||||
/// Acquire high-priority write lock - optimized for database queries
|
||||
pub async fn acquire_high_priority_write_lock(
|
||||
&self,
|
||||
bucket: impl Into<Arc<str>>,
|
||||
object: impl Into<Arc<str>>,
|
||||
owner: impl Into<Arc<str>>,
|
||||
) -> Result<FastLockGuard, LockResult> {
|
||||
let request =
|
||||
ObjectLockRequest::new_write(bucket, object, owner).with_priority(crate::fast_lock::types::LockPriority::High);
|
||||
self.acquire_lock(request).await
|
||||
}
|
||||
|
||||
/// Acquire critical priority read lock - for system operations
|
||||
pub async fn acquire_critical_read_lock(
|
||||
&self,
|
||||
bucket: impl Into<Arc<str>>,
|
||||
object: impl Into<Arc<str>>,
|
||||
owner: impl Into<Arc<str>>,
|
||||
) -> Result<FastLockGuard, LockResult> {
|
||||
let request =
|
||||
ObjectLockRequest::new_read(bucket, object, owner).with_priority(crate::fast_lock::types::LockPriority::Critical);
|
||||
self.acquire_lock(request).await
|
||||
}
|
||||
|
||||
/// Acquire critical priority write lock - for system operations
|
||||
pub async fn acquire_critical_write_lock(
|
||||
&self,
|
||||
bucket: impl Into<Arc<str>>,
|
||||
object: impl Into<Arc<str>>,
|
||||
owner: impl Into<Arc<str>>,
|
||||
) -> Result<FastLockGuard, LockResult> {
|
||||
let request =
|
||||
ObjectLockRequest::new_write(bucket, object, owner).with_priority(crate::fast_lock::types::LockPriority::Critical);
|
||||
self.acquire_lock(request).await
|
||||
}
|
||||
|
||||
/// Acquire multiple locks atomically - optimized version
|
||||
pub async fn acquire_locks_batch(&self, batch_request: BatchLockRequest) -> BatchLockResult {
|
||||
// Pre-sort requests by (shard_id, key) to avoid deadlocks
|
||||
@@ -304,7 +357,7 @@ impl FastObjectLockManager {
|
||||
}
|
||||
|
||||
/// Get shard for object key
|
||||
fn get_shard(&self, key: &crate::fast_lock::types::ObjectKey) -> &Arc<LockShard> {
|
||||
pub fn get_shard(&self, key: &crate::fast_lock::types::ObjectKey) -> &Arc<LockShard> {
|
||||
let index = key.shard_index(self.shard_mask);
|
||||
&self.shards[index]
|
||||
}
|
||||
@@ -362,6 +415,18 @@ impl Drop for FastObjectLockManager {
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for FastObjectLockManager {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
shards: self.shards.clone(),
|
||||
shard_mask: self.shard_mask,
|
||||
config: self.config.clone(),
|
||||
metrics: self.metrics.clone(),
|
||||
cleanup_handle: RwLock::new(None), // Don't clone the cleanup task
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl LockManager for FastObjectLockManager {
|
||||
async fn acquire_lock(&self, request: ObjectLockRequest) -> Result<FastLockGuard, LockResult> {
|
||||
|
||||
@@ -53,8 +53,11 @@ pub const DEFAULT_SHARD_COUNT: usize = 1024;
|
||||
/// Default lock timeout
|
||||
pub const DEFAULT_LOCK_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
|
||||
|
||||
/// Default acquire timeout
|
||||
pub const DEFAULT_ACQUIRE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
|
||||
/// Default acquire timeout - increased for database workloads
|
||||
pub const DEFAULT_ACQUIRE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
|
||||
|
||||
/// Maximum acquire timeout for high-load scenarios
|
||||
pub const MAX_ACQUIRE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(60);
|
||||
|
||||
/// Lock cleanup interval
|
||||
pub const CLEANUP_INTERVAL: std::time::Duration = std::time::Duration::from_secs(60);
|
||||
|
||||
@@ -18,7 +18,8 @@ use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering};
|
||||
use tokio::sync::Notify;
|
||||
|
||||
/// Optimized notification pool to reduce memory overhead and thundering herd effects
|
||||
static NOTIFY_POOL: Lazy<Vec<Arc<Notify>>> = Lazy::new(|| (0..64).map(|_| Arc::new(Notify::new())).collect());
|
||||
/// Increased pool size for better performance under high concurrency
|
||||
static NOTIFY_POOL: Lazy<Vec<Arc<Notify>>> = Lazy::new(|| (0..128).map(|_| Arc::new(Notify::new())).collect());
|
||||
|
||||
/// Optimized notification system for object locks
|
||||
#[derive(Debug)]
|
||||
|
||||
@@ -24,6 +24,7 @@ use crate::fast_lock::{
|
||||
state::ObjectLockState,
|
||||
types::{LockMode, LockResult, ObjectKey, ObjectLockRequest},
|
||||
};
|
||||
use std::collections::HashSet;
|
||||
|
||||
/// Lock shard to reduce global contention
|
||||
#[derive(Debug)]
|
||||
@@ -36,6 +37,8 @@ pub struct LockShard {
|
||||
metrics: ShardMetrics,
|
||||
/// Shard ID for debugging
|
||||
_shard_id: usize,
|
||||
/// Active guard IDs to prevent cleanup of locks with live guards
|
||||
active_guards: parking_lot::Mutex<HashSet<u64>>,
|
||||
}
|
||||
|
||||
impl LockShard {
|
||||
@@ -45,6 +48,7 @@ impl LockShard {
|
||||
object_pool: ObjectStatePool::new(),
|
||||
metrics: ShardMetrics::new(),
|
||||
_shard_id: shard_id,
|
||||
active_guards: parking_lot::Mutex::new(HashSet::new()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +127,12 @@ impl LockShard {
|
||||
|
||||
/// Slow path with async waiting
|
||||
async fn acquire_lock_slow_path(&self, request: &ObjectLockRequest, start_time: Instant) -> Result<(), LockResult> {
|
||||
let deadline = start_time + request.acquire_timeout;
|
||||
// Use adaptive timeout based on current load and request priority
|
||||
let adaptive_timeout = self.calculate_adaptive_timeout(request);
|
||||
let deadline = start_time + adaptive_timeout;
|
||||
|
||||
let mut retry_count = 0u32;
|
||||
const MAX_RETRIES: u32 = 10;
|
||||
|
||||
loop {
|
||||
// Get or create object state
|
||||
@@ -157,8 +166,22 @@ impl LockShard {
|
||||
return Err(LockResult::Timeout);
|
||||
}
|
||||
|
||||
// Wait for notification using optimized notify system
|
||||
// Use intelligent wait strategy: mix of notification wait and exponential backoff
|
||||
let remaining = deadline - Instant::now();
|
||||
|
||||
if retry_count < MAX_RETRIES && remaining > Duration::from_millis(10) {
|
||||
// For early retries, use a brief exponential backoff instead of full notification wait
|
||||
let backoff_ms = std::cmp::min(10 << retry_count, 100); // 10ms, 20ms, 40ms, 80ms, 100ms max
|
||||
let backoff_duration = Duration::from_millis(backoff_ms);
|
||||
|
||||
if backoff_duration < remaining {
|
||||
tokio::time::sleep(backoff_duration).await;
|
||||
retry_count += 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// If we've exhausted quick retries or have little time left, use notification wait
|
||||
let wait_result = match request.mode {
|
||||
LockMode::Shared => {
|
||||
state.atomic_state.inc_readers_waiting();
|
||||
@@ -179,7 +202,7 @@ impl LockShard {
|
||||
return Err(LockResult::Timeout);
|
||||
}
|
||||
|
||||
// Continue the loop to try acquisition again
|
||||
retry_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,10 +226,30 @@ impl LockShard {
|
||||
should_cleanup = !state.is_locked() && !state.atomic_state.has_waiters();
|
||||
} else {
|
||||
should_cleanup = false;
|
||||
// Additional diagnostics for release failures
|
||||
let current_mode = state.current_mode();
|
||||
let is_locked = state.is_locked();
|
||||
let has_waiters = state.atomic_state.has_waiters();
|
||||
|
||||
tracing::debug!(
|
||||
"Lock release failed in shard: key={}, owner={}, mode={:?}, current_mode={:?}, is_locked={}, has_waiters={}",
|
||||
key,
|
||||
owner,
|
||||
mode,
|
||||
current_mode,
|
||||
is_locked,
|
||||
has_waiters
|
||||
);
|
||||
}
|
||||
} else {
|
||||
result = false;
|
||||
should_cleanup = false;
|
||||
tracing::debug!(
|
||||
"Lock release failed - key not found in shard: key={}, owner={}, mode={:?}",
|
||||
key,
|
||||
owner,
|
||||
mode
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,6 +261,134 @@ impl LockShard {
|
||||
result
|
||||
}
|
||||
|
||||
/// Release lock with guard ID tracking for double-release prevention
|
||||
pub fn release_lock_with_guard(&self, key: &ObjectKey, owner: &Arc<str>, mode: LockMode, guard_id: u64) -> bool {
|
||||
// First, try to remove the guard from active set
|
||||
let guard_was_active = {
|
||||
let mut guards = self.active_guards.lock();
|
||||
guards.remove(&guard_id)
|
||||
};
|
||||
|
||||
// If guard was not active, this is a double-release attempt
|
||||
if !guard_was_active {
|
||||
tracing::debug!(
|
||||
"Double-release attempt blocked: key={}, owner={}, mode={:?}, guard_id={}",
|
||||
key,
|
||||
owner,
|
||||
mode,
|
||||
guard_id
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Proceed with normal release
|
||||
let should_cleanup;
|
||||
let result;
|
||||
|
||||
{
|
||||
let objects = self.objects.read();
|
||||
if let Some(state) = objects.get(key) {
|
||||
result = match mode {
|
||||
LockMode::Shared => state.release_shared(owner),
|
||||
LockMode::Exclusive => state.release_exclusive(owner),
|
||||
};
|
||||
|
||||
if result {
|
||||
self.metrics.record_release();
|
||||
should_cleanup = !state.is_locked() && !state.atomic_state.has_waiters();
|
||||
} else {
|
||||
should_cleanup = false;
|
||||
}
|
||||
} else {
|
||||
result = false;
|
||||
should_cleanup = false;
|
||||
}
|
||||
}
|
||||
|
||||
if should_cleanup {
|
||||
self.schedule_cleanup(key.clone());
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Register a guard to prevent premature cleanup
|
||||
pub fn register_guard(&self, guard_id: u64) {
|
||||
let mut guards = self.active_guards.lock();
|
||||
guards.insert(guard_id);
|
||||
}
|
||||
|
||||
/// Unregister a guard (called when guard is dropped)
|
||||
pub fn unregister_guard(&self, guard_id: u64) {
|
||||
let mut guards = self.active_guards.lock();
|
||||
guards.remove(&guard_id);
|
||||
}
|
||||
|
||||
/// Get count of active guards (for testing)
|
||||
#[cfg(test)]
|
||||
pub fn active_guard_count(&self) -> usize {
|
||||
let guards = self.active_guards.lock();
|
||||
guards.len()
|
||||
}
|
||||
|
||||
/// Check if a guard is active (for testing)
|
||||
#[cfg(test)]
|
||||
pub fn is_guard_active(&self, guard_id: u64) -> bool {
|
||||
let guards = self.active_guards.lock();
|
||||
guards.contains(&guard_id)
|
||||
}
|
||||
|
||||
/// Calculate adaptive timeout based on current system load and request priority
|
||||
fn calculate_adaptive_timeout(&self, request: &ObjectLockRequest) -> Duration {
|
||||
let base_timeout = request.acquire_timeout;
|
||||
|
||||
// Get current shard load metrics
|
||||
let lock_count = {
|
||||
let objects = self.objects.read();
|
||||
objects.len()
|
||||
};
|
||||
|
||||
let active_guard_count = {
|
||||
let guards = self.active_guards.lock();
|
||||
guards.len()
|
||||
};
|
||||
|
||||
// Calculate load factor with more generous thresholds for database workloads
|
||||
let total_load = (lock_count + active_guard_count) as f64;
|
||||
let load_factor = total_load / 500.0; // Lowered threshold for faster scaling
|
||||
|
||||
// More aggressive priority multipliers for database scenarios
|
||||
let priority_multiplier = match request.priority {
|
||||
crate::fast_lock::types::LockPriority::Critical => 3.0, // Increased
|
||||
crate::fast_lock::types::LockPriority::High => 2.0, // Increased
|
||||
crate::fast_lock::types::LockPriority::Normal => 1.2, // Slightly increased base
|
||||
crate::fast_lock::types::LockPriority::Low => 0.9,
|
||||
};
|
||||
|
||||
// More generous load-based scaling
|
||||
let load_multiplier = if load_factor > 2.0 {
|
||||
// Very high load: drastically extend timeout
|
||||
1.0 + (load_factor * 2.0)
|
||||
} else if load_factor > 1.0 {
|
||||
// High load: significantly extend timeout
|
||||
1.0 + (load_factor * 1.8)
|
||||
} else if load_factor > 0.3 {
|
||||
// Medium load: moderately extend timeout
|
||||
1.0 + (load_factor * 1.2)
|
||||
} else {
|
||||
// Low load: still give some buffer
|
||||
1.1
|
||||
};
|
||||
|
||||
let total_multiplier = priority_multiplier * load_multiplier;
|
||||
let adaptive_timeout_secs =
|
||||
(base_timeout.as_secs_f64() * total_multiplier).min(crate::fast_lock::MAX_ACQUIRE_TIMEOUT.as_secs_f64());
|
||||
|
||||
// Ensure minimum reasonable timeout even for low priority
|
||||
let min_timeout_secs = base_timeout.as_secs_f64() * 0.8;
|
||||
Duration::from_secs_f64(adaptive_timeout_secs.max(min_timeout_secs))
|
||||
}
|
||||
|
||||
/// Batch acquire locks with ordering to prevent deadlocks
|
||||
pub async fn acquire_locks_batch(
|
||||
&self,
|
||||
@@ -324,22 +495,44 @@ impl LockShard {
|
||||
pub fn adaptive_cleanup(&self) -> usize {
|
||||
let current_load = self.current_load_factor();
|
||||
let lock_count = self.lock_count();
|
||||
let active_guard_count = self.active_guards.lock().len();
|
||||
|
||||
// Be much more conservative if there are active guards or very high load
|
||||
if active_guard_count > 0 && current_load > 0.8 {
|
||||
tracing::debug!(
|
||||
"Skipping aggressive cleanup due to {} active guards and high load ({:.2})",
|
||||
active_guard_count,
|
||||
current_load
|
||||
);
|
||||
// Only clean very old entries when under high load with active guards
|
||||
return self.cleanup_expired_batch(3, 1_200_000); // 20 minutes, smaller batches
|
||||
}
|
||||
|
||||
// Under extreme load, skip cleanup entirely to reduce contention
|
||||
if current_load > 1.5 && active_guard_count > 10 {
|
||||
tracing::debug!(
|
||||
"Skipping all cleanup due to extreme load ({:.2}) and {} active guards",
|
||||
current_load,
|
||||
active_guard_count
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Dynamically adjust cleanup strategy based on load
|
||||
let cleanup_batch_size = match current_load {
|
||||
load if load > 0.9 => lock_count / 20, // High load: small batch cleanup
|
||||
load if load > 0.7 => lock_count / 10, // Medium load: moderate cleanup
|
||||
_ => lock_count / 5, // Low load: aggressive cleanup
|
||||
load if load > 0.9 => lock_count / 50, // Much smaller batches for high load
|
||||
load if load > 0.7 => lock_count / 20, // Smaller batches for medium load
|
||||
_ => lock_count / 10, // More conservative even for low load
|
||||
};
|
||||
|
||||
// Use longer timeout for high load scenarios
|
||||
// Use much longer timeouts to prevent premature cleanup
|
||||
let cleanup_threshold_millis = match current_load {
|
||||
load if load > 0.8 => 300_000, // 5 minutes for high load
|
||||
load if load > 0.5 => 180_000, // 3 minutes for medium load
|
||||
_ => 60_000, // 1 minute for low load
|
||||
load if load > 0.8 => 600_000, // 10 minutes for high load
|
||||
load if load > 0.5 => 300_000, // 5 minutes for medium load
|
||||
_ => 120_000, // 2 minutes for low load
|
||||
};
|
||||
|
||||
self.cleanup_expired_batch(cleanup_batch_size.max(10), cleanup_threshold_millis)
|
||||
self.cleanup_expired_batch_protected(cleanup_batch_size.max(5), cleanup_threshold_millis)
|
||||
}
|
||||
|
||||
/// Cleanup expired and unused locks
|
||||
@@ -378,6 +571,19 @@ impl LockShard {
|
||||
cleaned
|
||||
}
|
||||
|
||||
/// Protected batch cleanup that respects active guards
|
||||
fn cleanup_expired_batch_protected(&self, max_batch_size: usize, cleanup_threshold_millis: u64) -> usize {
|
||||
let active_guards = self.active_guards.lock();
|
||||
let guard_count = active_guards.len();
|
||||
drop(active_guards); // Release lock early
|
||||
|
||||
if guard_count > 0 {
|
||||
tracing::debug!("Cleanup with {} active guards, being conservative", guard_count);
|
||||
}
|
||||
|
||||
self.cleanup_expired_batch(max_batch_size, cleanup_threshold_millis)
|
||||
}
|
||||
|
||||
/// Batch cleanup with limited processing to avoid blocking
|
||||
fn cleanup_expired_batch(&self, max_batch_size: usize, cleanup_threshold_millis: u64) -> usize {
|
||||
let mut cleaned = 0;
|
||||
|
||||
@@ -373,11 +373,23 @@ impl ObjectLockState {
|
||||
}
|
||||
true
|
||||
} else {
|
||||
// Inconsistency - re-add owner
|
||||
// Inconsistency detected - atomic state shows no shared lock but owner was found
|
||||
tracing::warn!(
|
||||
"Atomic state inconsistency during shared lock release: owner={}, remaining_owners={}",
|
||||
owner,
|
||||
shared.len()
|
||||
);
|
||||
// Re-add owner to maintain consistency
|
||||
shared.push(owner.clone());
|
||||
false
|
||||
}
|
||||
} else {
|
||||
// Owner not found in shared owners list
|
||||
tracing::debug!(
|
||||
"Shared lock release failed - owner not found: owner={}, current_owners={:?}",
|
||||
owner,
|
||||
shared.iter().map(|s| s.as_ref()).collect::<Vec<_>>()
|
||||
);
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -401,9 +413,21 @@ impl ObjectLockState {
|
||||
}
|
||||
true
|
||||
} else {
|
||||
// Atomic state inconsistency - current owner matches but atomic release failed
|
||||
tracing::warn!(
|
||||
"Atomic state inconsistency during exclusive lock release: owner={}, atomic_state={:b}",
|
||||
owner,
|
||||
self.atomic_state.state.load(Ordering::Acquire)
|
||||
);
|
||||
false
|
||||
}
|
||||
} else {
|
||||
// Owner mismatch
|
||||
tracing::debug!(
|
||||
"Exclusive lock release failed - owner mismatch: expected_owner={}, actual_owner={:?}",
|
||||
owner,
|
||||
current.as_ref().map(|s| s.as_ref())
|
||||
);
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user