feat(perf): Add configurable bitrot skip for reads (#2110)

Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: 安正超 <anzhengchao@gmail.com>
This commit is contained in:
evan slack
2026-03-10 20:59:00 -06:00
committed by GitHub
parent f00d01ec2d
commit 4b480727d6
9 changed files with 47 additions and 18 deletions

View File

@@ -167,3 +167,16 @@ pub const DEFAULT_OBJECT_CACHE_TTI_SECS: u64 = 120;
///
/// Default is set to 5 hits.
pub const DEFAULT_OBJECT_HOT_MIN_HITS_TO_EXTEND: usize = 5;
/// Skip bitrot hash verification on GetObject reads.
///
/// When enabled, GetObject reads skip the per-shard hash
/// computation and comparison, reducing CPU usage on the read path.
/// The background scanner still performs full integrity verification.
/// Does not affect writes, heals, or scanner operations.
///
/// Default is false (verify on every read, matching pre-existing behavior).
pub const ENV_OBJECT_GET_SKIP_BITROT_VERIFY: &str = "RUSTFS_OBJECT_GET_SKIP_BITROT_VERIFY";
/// Default: bitrot verification is enabled on GetObject reads (do not skip).
pub const DEFAULT_OBJECT_GET_SKIP_BITROT_VERIFY: bool = false;

View File

@@ -39,6 +39,7 @@ pub async fn create_bitrot_reader(
length: usize,
shard_size: usize,
checksum_algo: HashAlgorithm,
skip_verify: bool,
) -> disk::error::Result<Option<BitrotReader<Box<dyn AsyncRead + Send + Sync + Unpin>>>> {
// Calculate the total length to read, including the checksum overhead
let length = length.div_ceil(shard_size) * checksum_algo.size() + length;
@@ -47,13 +48,18 @@ pub async fn create_bitrot_reader(
// Use inline data
let mut rd = Cursor::new(data.to_vec());
rd.set_position(offset as u64);
let reader = BitrotReader::new(Box::new(rd) as Box<dyn AsyncRead + Send + Sync + Unpin>, shard_size, checksum_algo);
let reader = BitrotReader::new(
Box::new(rd) as Box<dyn AsyncRead + Send + Sync + Unpin>,
shard_size,
checksum_algo,
skip_verify,
);
Ok(Some(reader))
} else if let Some(disk) = disk {
// Read from disk
match disk.read_file_stream(bucket, path, offset, length - offset).await {
Ok(rd) => {
let reader = BitrotReader::new(rd, shard_size, checksum_algo);
let reader = BitrotReader::new(rd, shard_size, checksum_algo, skip_verify);
Ok(Some(reader))
}
Err(e) => Err(e),
@@ -116,7 +122,7 @@ mod tests {
let checksum_algo = HashAlgorithm::HighwayHash256;
let result =
create_bitrot_reader(Some(test_data), None, "test-bucket", "test-path", 0, 0, shard_size, checksum_algo).await;
create_bitrot_reader(Some(test_data), None, "test-bucket", "test-path", 0, 0, shard_size, checksum_algo, false).await;
assert!(result.is_ok());
assert!(result.unwrap().is_some());
@@ -127,7 +133,8 @@ mod tests {
let shard_size = 16;
let checksum_algo = HashAlgorithm::HighwayHash256;
let result = create_bitrot_reader(None, None, "test-bucket", "test-path", 0, 1024, shard_size, checksum_algo).await;
let result =
create_bitrot_reader(None, None, "test-bucket", "test-path", 0, 1024, shard_size, checksum_algo, false).await;
assert!(result.is_ok());
assert!(result.unwrap().is_none());

View File

@@ -28,10 +28,7 @@ pin_project! {
shard_size: usize,
buf: Vec<u8>,
hash_buf: Vec<u8>,
// hash_read: usize,
// data_buf: Vec<u8>,
// data_read: usize,
// hash_checked: bool,
skip_verify: bool,
id: Uuid,
}
}
@@ -41,7 +38,7 @@ where
R: AsyncRead + Unpin + Send + Sync,
{
/// Create a new BitrotReader.
pub fn new(inner: R, shard_size: usize, algo: HashAlgorithm) -> Self {
pub fn new(inner: R, shard_size: usize, algo: HashAlgorithm, skip_verify: bool) -> Self {
let hash_size = algo.size();
Self {
inner,
@@ -49,10 +46,7 @@ where
shard_size,
buf: Vec::new(),
hash_buf: vec![0u8; hash_size],
// hash_read: 0,
// data_buf: Vec::new(),
// data_read: 0,
// hash_checked: false,
skip_verify,
id: Uuid::new_v4(),
}
}
@@ -90,7 +84,7 @@ where
data_len += n;
}
if hash_size > 0 {
if hash_size > 0 && !self.skip_verify {
let actual_hash = self.hash_algo.hash_encode(&out[..data_len]);
if actual_hash.as_ref() != self.hash_buf.as_slice() {
error!("bitrot reader hash mismatch, id={} data_len={}, out_len={}", self.id, data_len, out.len());
@@ -388,7 +382,7 @@ mod tests {
// Read
let reader = bitrot_writer.into_inner();
let reader = Cursor::new(reader.into_inner());
let mut bitrot_reader = BitrotReader::new(reader, shard_size, HashAlgorithm::HighwayHash256);
let mut bitrot_reader = BitrotReader::new(reader, shard_size, HashAlgorithm::HighwayHash256, false);
let mut out = Vec::new();
let mut n = 0;
while n < data_size {
@@ -420,7 +414,7 @@ mod tests {
let pos = written.len() - 1;
written[pos] ^= 0xFF;
let reader = Cursor::new(written);
let mut bitrot_reader = BitrotReader::new(reader, shard_size, HashAlgorithm::HighwayHash256);
let mut bitrot_reader = BitrotReader::new(reader, shard_size, HashAlgorithm::HighwayHash256, false);
let count = data_size.div_ceil(shard_size);
@@ -464,7 +458,7 @@ mod tests {
let reader = bitrot_writer.into_inner();
let reader = Cursor::new(reader.into_inner());
let mut bitrot_reader = BitrotReader::new(reader, shard_size, HashAlgorithm::None);
let mut bitrot_reader = BitrotReader::new(reader, shard_size, HashAlgorithm::None, false);
let mut out = Vec::new();
let mut n = 0;
while n < data_size {

View File

@@ -454,6 +454,6 @@ mod tests {
}
let reader_cursor = Cursor::new(buf);
BitrotReader::new(reader_cursor, shard_size, hash_algo.clone())
BitrotReader::new(reader_cursor, shard_size, hash_algo.clone(), false)
}
}

View File

@@ -494,6 +494,7 @@ impl ObjectIO for SetDisks {
let object = object.to_owned();
let set_index = self.set_index;
let pool_index = self.pool_index;
let skip_verify = opts.skip_verify_bitrot;
// Move the read-lock guard into the task so it lives for the duration of the read
// let _guard_to_hold = _read_lock_guard; // moved into closure below
tokio::spawn(async move {
@@ -510,6 +511,7 @@ impl ObjectIO for SetDisks {
&disks,
set_index,
pool_index,
skip_verify,
)
.await
{
@@ -1654,6 +1656,7 @@ impl ObjectOperations for SetDisks {
let cloned_fi = fi.clone();
let set_index = self.set_index;
let pool_index = self.pool_index;
let skip_verify = opts.skip_verify_bitrot;
tokio::spawn(async move {
if let Err(e) = Self::get_object_with_fileinfo(
&cloned_bucket,
@@ -1666,6 +1669,7 @@ impl ObjectOperations for SetDisks {
&online_disks,
set_index,
pool_index,
skip_verify,
)
.await
{

View File

@@ -375,6 +375,7 @@ impl SetDisks {
till_offset,
erasure.shard_size(),
checksum_algo.clone(),
false,
)
.await
{

View File

@@ -568,6 +568,7 @@ impl SetDisks {
disks: &[Option<DiskStore>],
set_index: usize,
pool_index: usize,
skip_verify_bitrot: bool,
) -> Result<()>
where
W: AsyncWrite + Send + Sync + Unpin + 'static,
@@ -659,6 +660,7 @@ impl SetDisks {
till_offset,
erasure.shard_size(),
HashAlgorithm::HighwayHash256,
skip_verify_bitrot,
)
.await
{

View File

@@ -70,6 +70,7 @@ pub struct ObjectOptions {
pub eval_metadata: Option<HashMap<String, String>>,
pub want_checksum: Option<Checksum>,
pub skip_verify_bitrot: bool,
}
impl ObjectOptions {

View File

@@ -164,6 +164,13 @@ pub async fn get_opts(
opts.version_suspended = version_suspended;
opts.versioned = versioned;
// Optionally skip per-shard bitrot hash verification on reads to save CPU.
// Background scanner still performs full integrity checks asynchronously.
opts.skip_verify_bitrot = rustfs_utils::get_env_bool(
rustfs_config::ENV_OBJECT_GET_SKIP_BITROT_VERIFY,
rustfs_config::DEFAULT_OBJECT_GET_SKIP_BITROT_VERIFY,
);
fill_conditional_writes_opts_from_header(headers, &mut opts)?;
Ok(opts)