mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-16 09:58:21 +00:00
perf(ecstore): reduce inline PUT commit overhead (#6033)
* perf(metrics): attribute PUT stage costs Co-Authored-By: heihutu <heihutu@gmail.com> * perf(ecstore): move PUT metadata during shuffle Co-Authored-By: heihutu <heihutu@gmail.com> * perf(s3): reuse PUT object lock state Co-Authored-By: heihutu <heihutu@gmail.com> * perf(ecstore): trim PUT metadata fanout clones Build per-disk PUT metadata only for committed writer slots, move the response metadata out of the fanout vector, and preserve fresh FileInfo shuffle semantics. Co-Authored-By: heihutu <heihutu@gmail.com> * perf(metrics): make PUT stage attribution opt-in Co-Authored-By: heihutu <heihutu@gmail.com> * perf(ecstore): commit inline PUT shards directly Co-Authored-By: heihutu <heihutu@gmail.com> * perf(ecstore): streamline rename staging cleanup Use the directory-specific removal operation for rename_data staging parents. This avoids a guaranteed failed file-removal probe on Unix-like hosts and lets Windows remove the empty directory directly while preserving best-effort non-empty handling. Co-Authored-By: heihutu <heihutu@gmail.com> * test(ecstore): cover inline PUT rename failures Cache the detailed stage metrics gate once per PUT and exercise exact-quorum and quorum-minus-one failures after inline shard encoding. Co-Authored-By: heihutu <heihutu@gmail.com> --------- Co-authored-by: heihutu <heihutu@gmail.com>
This commit is contained in:
@@ -194,7 +194,7 @@ use std::str::FromStr;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::sync::{Arc, Mutex, OnceLock};
|
||||
use std::time::Duration;
|
||||
use std::time::{Duration, Instant};
|
||||
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
|
||||
use tokio::io::{AsyncRead, ReadBuf};
|
||||
use tokio::sync::{OwnedSemaphorePermit, RwLock};
|
||||
@@ -5596,7 +5596,8 @@ impl DefaultObjectUsecase {
|
||||
// Bucket-quota admission runs exactly once, and only now that the authoritative object length is known. `size` is the same basis the settle phase records via ObjectInfo.size (actual, pre-compression/pre-encryption logical size), NOT the aws-chunked wire Content-Length. When no quota is configured this stays a zero-extra-I/O fast path; once a hard quota is set, checker/config/usage faults fail closed with a retryable error.
|
||||
self.check_bucket_quota(&bucket, quota_operation, size as u64).await?;
|
||||
|
||||
let ingress_stage_start = std::time::Instant::now();
|
||||
let put_stage_metrics_enabled = rustfs_io_metrics::put_stage_metrics_enabled();
|
||||
let ingress_stage_start = put_stage_metrics_enabled.then(Instant::now);
|
||||
let should_compress =
|
||||
is_disk_compressible(&req.headers, &key) && size > MIN_DISK_COMPRESSIBLE_SIZE as i64 && !ciphertext_passthrough;
|
||||
let server_side_encryption_requested =
|
||||
@@ -5660,9 +5661,13 @@ impl DefaultObjectUsecase {
|
||||
let Some(store) = self.object_store() else {
|
||||
return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not init".to_string()));
|
||||
};
|
||||
let bucket_validate_stage_start = put_stage_metrics_enabled.then(Instant::now);
|
||||
validate_bucket_exists(&store, &bucket).await?;
|
||||
rustfs_io_metrics::record_put_object_stage_duration_from("app_bucket_validate", bucket_validate_stage_start);
|
||||
|
||||
let sse_config_stage_start = put_stage_metrics_enabled.then(Instant::now);
|
||||
let bucket_sse_config = metadata_sys::get_sse_config(&bucket).await.ok();
|
||||
rustfs_io_metrics::record_put_object_stage_duration_from("app_sse_config_lookup", sse_config_stage_start);
|
||||
debug!(
|
||||
target: "rustfs::app::object_usecase",
|
||||
component = "app",
|
||||
@@ -5728,7 +5733,9 @@ impl DefaultObjectUsecase {
|
||||
|
||||
let mut metadata = metadata.unwrap_or_default();
|
||||
let has_explicit_object_lock_retention = object_lock_mode.is_some() || object_lock_retain_until_date.is_some();
|
||||
let object_lock_config_stage_start = put_stage_metrics_enabled.then(Instant::now);
|
||||
let object_lock_config_state = load_bucket_object_lock_config_state(&bucket).await?;
|
||||
rustfs_io_metrics::record_put_object_stage_duration_from("app_object_lock_config_lookup", object_lock_config_stage_start);
|
||||
apply_put_request_metadata(
|
||||
&mut metadata,
|
||||
&req.headers,
|
||||
@@ -5750,6 +5757,7 @@ impl DefaultObjectUsecase {
|
||||
has_explicit_object_lock_retention,
|
||||
)?;
|
||||
|
||||
let put_opts_stage_start = put_stage_metrics_enabled.then(Instant::now);
|
||||
let mut opts: ObjectOptions = put_opts_with_replication_authorization(
|
||||
&bucket,
|
||||
&key,
|
||||
@@ -5760,6 +5768,7 @@ impl DefaultObjectUsecase {
|
||||
)
|
||||
.await
|
||||
.map_err(ApiError::from)?;
|
||||
rustfs_io_metrics::record_put_object_stage_duration_from("app_put_opts_build", put_opts_stage_start);
|
||||
apply_bucket_generation_guard(&req, &bucket, &mut opts)?;
|
||||
apply_put_request_object_lock_opts(
|
||||
&bucket,
|
||||
@@ -5778,10 +5787,11 @@ impl DefaultObjectUsecase {
|
||||
// replication), the lookup is skipped and accounting is backfilled from
|
||||
// the dst xl.meta that rename_data already reads, saving a full-disk
|
||||
// metadata fanout per PUT.
|
||||
let prelookup_required = version_id.is_some() || object_lock_checks_required(&bucket).await;
|
||||
let prelookup_required = version_id.is_some() || object_lock_checks_required_for_state(&object_lock_config_state);
|
||||
// Outer None = prelookup skipped (accounting comes from the commit
|
||||
// backfill); Some(inner) = the previous current size as observed by the
|
||||
// lookup, with the pre-#1009 semantics kept bit-for-bit.
|
||||
let prelookup_stage_start = (prelookup_required && put_stage_metrics_enabled).then(Instant::now);
|
||||
let prelookup_previous_current_size: Option<Option<u64>> = if prelookup_required {
|
||||
let current_opts: ObjectOptions = internal_object_info_lookup_opts(
|
||||
get_opts(&bucket, &key, version_id.clone(), None, &req.headers)
|
||||
@@ -5807,6 +5817,7 @@ impl DefaultObjectUsecase {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
rustfs_io_metrics::record_put_object_stage_duration_from("app_prelookup", prelookup_stage_start);
|
||||
|
||||
let actual_size = size;
|
||||
|
||||
@@ -5895,15 +5906,13 @@ impl DefaultObjectUsecase {
|
||||
put_extra_checksum_headers = additional_checksum_echo_pairs(&opts.want_checksum);
|
||||
}
|
||||
rustfs_io_metrics::record_put_object_path(put_path);
|
||||
rustfs_io_metrics::record_put_object_stage_duration(
|
||||
"ingress_prepare",
|
||||
ingress_stage_start.elapsed().as_secs_f64() * 1000.0,
|
||||
);
|
||||
rustfs_io_metrics::record_put_object_stage_duration_from("ingress_prepare", ingress_stage_start);
|
||||
|
||||
let mut helper = OperationHelper::new(&req, event_name, S3Operation::PutObject);
|
||||
let ssekms_context = extract_ssekms_context_from_headers(&req.headers)?;
|
||||
|
||||
// Apply encryption using unified SSE API.
|
||||
let encryption_stage_start = put_stage_metrics_enabled.then(Instant::now);
|
||||
let write_principal = SseKmsPrincipal::from_request(&req);
|
||||
let encryption_request = EncryptionRequest {
|
||||
bucket: &bucket,
|
||||
@@ -5947,6 +5956,7 @@ impl DefaultObjectUsecase {
|
||||
}
|
||||
|
||||
reader = write_plan.apply(reader, actual_size).map_err(ApiError::from)?;
|
||||
rustfs_io_metrics::record_put_object_stage_duration_from("app_encryption_prepare", encryption_stage_start);
|
||||
|
||||
let mut reader = PutObjReader::new(reader);
|
||||
|
||||
@@ -5963,9 +5973,11 @@ impl DefaultObjectUsecase {
|
||||
// post-commit schedule (see the reuse site further down), so a
|
||||
// replication-config hot update can no longer split the two phases
|
||||
// (https://github.com/rustfs/backlog/issues/1320).
|
||||
let replication_decision_stage_start = put_stage_metrics_enabled.then(Instant::now);
|
||||
let dsc =
|
||||
must_replicate_object(&bucket, &key, &mt2, "".to_string(), opts.delete_marker_replication_status(), opts.clone())
|
||||
.await;
|
||||
rustfs_io_metrics::record_put_object_stage_duration_from("app_replication_decision", replication_decision_stage_start);
|
||||
|
||||
if dsc.replicate_any() {
|
||||
insert_str(&mut opts.user_defined, SUFFIX_REPLICATION_TIMESTAMP, jiff::Zoned::now().to_string());
|
||||
@@ -5977,7 +5989,12 @@ impl DefaultObjectUsecase {
|
||||
}
|
||||
|
||||
let cache_adapter = self.object_data_cache();
|
||||
let cache_invalidate_before_stage_start = put_stage_metrics_enabled.then(Instant::now);
|
||||
let _ = invalidate_object_data_cache_before_mutation(&cache_adapter, &bucket, &key).await;
|
||||
rustfs_io_metrics::record_put_object_stage_duration_from(
|
||||
"app_cache_invalidate_before",
|
||||
cache_invalidate_before_stage_start,
|
||||
);
|
||||
|
||||
let store_put_watchdog = tokio_util::sync::CancellationToken::new();
|
||||
spawn_traced({
|
||||
@@ -6017,6 +6034,7 @@ impl DefaultObjectUsecase {
|
||||
let object_traffic_progress = object_traffic_health
|
||||
.as_deref()
|
||||
.and_then(ObjectTrafficHealth::track_write_storage);
|
||||
let store_put_stage_start = put_stage_metrics_enabled.then(Instant::now);
|
||||
let (obj_info, backfilled_old_current_size) = match store
|
||||
.put_object_with_old_current_size(&bucket, &key, &mut reader, &opts)
|
||||
.await
|
||||
@@ -6042,6 +6060,7 @@ impl DefaultObjectUsecase {
|
||||
}
|
||||
Err(err) => {
|
||||
store_put_watchdog.cancel();
|
||||
rustfs_io_metrics::record_put_object_stage_duration_from("app_store_put", store_put_stage_start);
|
||||
warn!(
|
||||
target: "rustfs::app::object_usecase",
|
||||
event = EVENT_PUT_OBJECT_STORE_RETURNED,
|
||||
@@ -6063,10 +6082,12 @@ impl DefaultObjectUsecase {
|
||||
return result;
|
||||
}
|
||||
};
|
||||
rustfs_io_metrics::record_put_object_stage_duration_from("app_store_put", store_put_stage_start);
|
||||
drop(object_traffic_progress);
|
||||
#[cfg(test)]
|
||||
wait_for_put_post_store_test_hook(&bucket).await;
|
||||
|
||||
let post_store_stage_start = put_stage_metrics_enabled.then(Instant::now);
|
||||
maybe_enqueue_transition_immediate(&obj_info, LcEventSrc::S3PutObject).await;
|
||||
let _ = invalidate_object_data_cache_after_put_success(&cache_adapter, &bucket, &key).await;
|
||||
|
||||
@@ -6165,10 +6186,13 @@ impl DefaultObjectUsecase {
|
||||
let result = Ok(response);
|
||||
let _ = helper.complete(&result);
|
||||
rustfs_scanner::record_dirty_usage_bucket(&bucket);
|
||||
rustfs_io_metrics::record_put_object_stage_duration_from("app_post_store_bookkeeping", post_store_stage_start);
|
||||
|
||||
// Record write operation for capacity management (inline to avoid per-request tokio::spawn overhead)
|
||||
let capacity_update_stage_start = put_stage_metrics_enabled.then(Instant::now);
|
||||
let manager = get_capacity_manager();
|
||||
manager.record_write_operation().await;
|
||||
rustfs_io_metrics::record_put_object_stage_duration_from("app_capacity_update", capacity_update_stage_start);
|
||||
|
||||
// Record PutObject metrics via zero-copy-metrics
|
||||
{
|
||||
@@ -9572,6 +9596,13 @@ pub(super) async fn object_lock_checks_required(bucket: &str) -> bool {
|
||||
.map_or(true, |metadata| metadata.object_locking())
|
||||
}
|
||||
|
||||
fn object_lock_checks_required_for_state(state: &metadata_sys::ObjectLockConfigState) -> bool {
|
||||
match state {
|
||||
metadata_sys::ObjectLockConfigState::Configured { .. } | metadata_sys::ObjectLockConfigState::Fabricated => true,
|
||||
metadata_sys::ObjectLockConfigState::ConfirmedAbsent => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// rustfs/backlog#1009: map the rename_data old-size backfill onto the
|
||||
/// `previous_current_size` value the usage-accounting helpers expect. Outer
|
||||
/// `None` = unknown (no quorum agreement, or a peer predates the field) — the
|
||||
@@ -10198,6 +10229,23 @@ mod tests {
|
||||
assert_eq!(err.message(), Some(ERR_OBJECT_LOCK_RETENTION_HEADERS_MUST_BE_PAIRED));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_lock_checks_required_reuses_authoritative_state() {
|
||||
assert!(!object_lock_checks_required_for_state(
|
||||
&metadata_sys::ObjectLockConfigState::ConfirmedAbsent
|
||||
));
|
||||
|
||||
let configured = metadata_sys::ObjectLockConfigState::Configured {
|
||||
config: ObjectLockConfiguration {
|
||||
object_lock_enabled: Some(ObjectLockEnabled::from_static(ObjectLockEnabled::ENABLED)),
|
||||
rule: None,
|
||||
},
|
||||
updated_at: OffsetDateTime::now_utc(),
|
||||
};
|
||||
assert!(object_lock_checks_required_for_state(&configured));
|
||||
assert!(object_lock_checks_required_for_state(&metadata_sys::ObjectLockConfigState::Fabricated));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_put_like_object_lock_metadata_rejects_retain_until_date_without_mode() {
|
||||
let retain_until = Timestamp::from(OffsetDateTime::now_utc().add(time::Duration::days(1)));
|
||||
|
||||
Reference in New Issue
Block a user