mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-08 15:41:15 +02:00
feat(scrub): EC LOCAL needle walk (split from FULL) (#10144)
* feat(ec): extract locate_ec_shard_needle_interval Mirrors Go's EcVolume.LocateEcShardNeedleInterval; reused by locate_needle and the upcoming local scrub walk. Claude-Session: https://claude.ai/code/session_015EE9Sc9EvNp8BCVva4RKdo * feat(ec): add EcVolumeShard::to_ec_shard_info Mirrors Go's ToEcShardInfo. Claude-Session: https://claude.ai/code/session_015EE9Sc9EvNp8BCVva4RKdo * feat(ec): add EcVolume::scrub_local Walk the .ecx and verify each needle against the locally-held shards, reading interval-by-interval (reusing one chunk buffer); CRC-check only fully-local needles, report short/unreadable local shards, and abort the scan on a structural size mismatch. Mirrors Go's EcVolume.ScrubLocal. Claude-Session: https://claude.ai/code/session_015EE9Sc9EvNp8BCVva4RKdo * feat(scrub): dispatch EC LOCAL (mode 3) to scrub_local Splits the mode 2|3 arm: FULL (2) keeps the Reed-Solomon parity check; LOCAL (3) now runs the per-needle local-shard walk. Claude-Session: https://claude.ai/code/session_015EE9Sc9EvNp8BCVva4RKdo
This commit is contained in:
@@ -3957,8 +3957,9 @@ impl VolumeServer for VolumeGrpcService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
2 | 3 => {
|
2 => {
|
||||||
// FULL (2) / LOCAL (3): verify EC shard data
|
// FULL: Reed-Solomon parity verification over the local shards.
|
||||||
|
// (Cross-server needle verification arrives in a follow-up.)
|
||||||
let files = ecv.walk_ecx_stats().map(|(f, _, _)| f).unwrap_or(0);
|
let files = ecv.walk_ecx_stats().map(|(f, _, _)| f).unwrap_or(0);
|
||||||
|
|
||||||
// After cross-disk reconciliation, an EcVolume can
|
// After cross-disk reconciliation, an EcVolume can
|
||||||
@@ -4013,6 +4014,19 @@ impl VolumeServer for VolumeGrpcService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
3 => {
|
||||||
|
// LOCAL: verify each needle against the locally-held shards.
|
||||||
|
total_volumes += 1;
|
||||||
|
let (files, shard_infos, errs) = ecv.scrub_local();
|
||||||
|
total_files += files;
|
||||||
|
if !errs.is_empty() || !shard_infos.is_empty() {
|
||||||
|
broken_volume_ids.push(vid.0);
|
||||||
|
broken_shard_infos.extend(shard_infos);
|
||||||
|
for msg in errs {
|
||||||
|
details.push(format!("ecvol {}: {}", vid.0, msg));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => unreachable!(), // validated above
|
_ => unreachable!(), // validated above
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,6 +109,17 @@ impl EcVolumeShard {
|
|||||||
self.ecd_file_size
|
self.ecd_file_size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Protobuf descriptor for this shard. Mirrors Go's ToEcShardInfo.
|
||||||
|
pub fn to_ec_shard_info(&self) -> crate::pb::volume_server_pb::EcShardInfo {
|
||||||
|
crate::pb::volume_server_pb::EcShardInfo {
|
||||||
|
shard_id: self.shard_id as u32,
|
||||||
|
size: self.file_size(),
|
||||||
|
collection: self.collection.clone(),
|
||||||
|
volume_id: self.volume_id.0,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Close the shard file.
|
/// Close the shard file.
|
||||||
pub fn close(&mut self) {
|
pub fn close(&mut self) {
|
||||||
if let Some(ref file) = self.ecd_file {
|
if let Some(ref file) = self.ecd_file {
|
||||||
|
|||||||
@@ -546,6 +546,26 @@ impl EcVolume {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Locate the EC shard intervals needed to read a needle.
|
/// Locate the EC shard intervals needed to read a needle.
|
||||||
|
/// Locate the EC shard intervals covering a needle at `actual_offset` whose
|
||||||
|
/// index size is `size`. Mirrors Go's EcVolume.LocateEcShardNeedleInterval.
|
||||||
|
pub fn locate_ec_shard_needle_interval(
|
||||||
|
&self,
|
||||||
|
actual_offset: i64,
|
||||||
|
size: Size,
|
||||||
|
) -> Vec<ec_locate::Interval> {
|
||||||
|
// shardSize = datFileSize / DataShards when known, else ecdFileSize - 1
|
||||||
|
// (shards are padded to the small block size; the -1 avoids an
|
||||||
|
// off-by-one in the large-block row count).
|
||||||
|
let shard_size = if self.dat_file_size > 0 {
|
||||||
|
self.dat_file_size / self.data_shards as i64
|
||||||
|
} else {
|
||||||
|
self.shard_file_size() - 1
|
||||||
|
};
|
||||||
|
// locate_data wants the on-disk size (header+body+checksum+timestamp+padding).
|
||||||
|
let actual = get_actual_size(size, self.version);
|
||||||
|
ec_locate::locate_data(actual_offset, Size(actual as i32), shard_size, self.data_shards)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn locate_needle(
|
pub fn locate_needle(
|
||||||
&self,
|
&self,
|
||||||
needle_id: NeedleId,
|
needle_id: NeedleId,
|
||||||
@@ -559,25 +579,7 @@ impl EcVolume {
|
|||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Match Go's LocateEcShardNeedleInterval: shardSize = shard.ecdFileSize - 1
|
let intervals = self.locate_ec_shard_needle_interval(offset.to_actual_offset(), size);
|
||||||
// Shards are usually padded to ErasureCodingSmallBlockSize, so subtract 1
|
|
||||||
// to avoid off-by-one in large block row count calculation.
|
|
||||||
// If datFileSize is known, use datFileSize / DataShards instead.
|
|
||||||
let shard_size = if self.dat_file_size > 0 {
|
|
||||||
self.dat_file_size / self.data_shards as i64
|
|
||||||
} else {
|
|
||||||
self.shard_file_size() - 1
|
|
||||||
};
|
|
||||||
// Pass the actual on-disk size (header+body+checksum+timestamp+padding)
|
|
||||||
// to locate_data, matching Go: types.Size(needle.GetActualSize(size, version))
|
|
||||||
let actual = get_actual_size(size, self.version);
|
|
||||||
let intervals = ec_locate::locate_data(
|
|
||||||
offset.to_actual_offset(),
|
|
||||||
Size(actual as i32),
|
|
||||||
shard_size,
|
|
||||||
self.data_shards,
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(Some((offset, size, intervals)))
|
Ok(Some((offset, size, intervals)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -734,6 +736,154 @@ impl EcVolume {
|
|||||||
crate::storage::idx::check_index_file(&mut ecx_file, self.ecx_file_size, self.version)
|
crate::storage::idx::check_index_file(&mut ecx_file, self.ecx_file_size, self.version)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// ScrubLocal verifies each needle against the LOCAL shards only; it cannot
|
||||||
|
/// CRC-check a needle whose intervals span shards held on other servers.
|
||||||
|
/// Mirrors Go's EcVolume.ScrubLocal. Returns (rows walked, broken shards, errors).
|
||||||
|
pub fn scrub_local(
|
||||||
|
&self,
|
||||||
|
) -> (u64, Vec<crate::pb::volume_server_pb::EcShardInfo>, Vec<String>) {
|
||||||
|
// Local scan also verifies the index.
|
||||||
|
let (_, mut errs) = self.scrub_index();
|
||||||
|
|
||||||
|
let mut broken_shards: HashSet<ShardId> = HashSet::new();
|
||||||
|
let mut count: u64 = 0;
|
||||||
|
|
||||||
|
let ecx_path = self.ecx_file_name();
|
||||||
|
let mut ecx_file = match File::open(&ecx_path) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => {
|
||||||
|
errs.push(format!("open ECX file {}: {}", ecx_path, e));
|
||||||
|
return (count, Vec::new(), errs);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reused across every needle/chunk to avoid a per-chunk allocation.
|
||||||
|
let mut chunk_buf: Vec<u8> = Vec::new();
|
||||||
|
let walk = crate::storage::idx::walk_index_file(&mut ecx_file, 0, |id, offset, size| {
|
||||||
|
count += 1;
|
||||||
|
if size.is_tombstone() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let locations = self.locate_ec_shard_needle_interval(offset.to_actual_offset(), size);
|
||||||
|
// A needle is verifiable locally only if every shard it spans is local;
|
||||||
|
// when any is remote, skip the reassembly buffer entirely.
|
||||||
|
let has_remote_chunks = locations.iter().any(|iv| {
|
||||||
|
let (sid, _) = iv.to_shard_id_and_offset(self.data_shards);
|
||||||
|
self.shards.get(sid as usize).and_then(|s| s.as_ref()).is_none()
|
||||||
|
});
|
||||||
|
let mut read: i64 = 0;
|
||||||
|
let mut data: Vec<u8> = if has_remote_chunks {
|
||||||
|
Vec::new()
|
||||||
|
} else {
|
||||||
|
Vec::with_capacity(get_actual_size(size, self.version) as usize)
|
||||||
|
};
|
||||||
|
let mut local_shard_ids: Vec<ShardId> = Vec::new();
|
||||||
|
|
||||||
|
for (i, iv) in locations.iter().enumerate() {
|
||||||
|
let (sid, soffset) = iv.to_shard_id_and_offset(self.data_shards);
|
||||||
|
let ssize = iv.size;
|
||||||
|
let shard = match self.shards.get(sid as usize).and_then(|s| s.as_ref()) {
|
||||||
|
Some(s) => s,
|
||||||
|
None => {
|
||||||
|
// Shard is not local; we can't verify it without decoding.
|
||||||
|
read += ssize;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
local_shard_ids.push(sid);
|
||||||
|
|
||||||
|
if soffset + ssize > shard.file_size() {
|
||||||
|
broken_shards.insert(sid);
|
||||||
|
errs.push(format!(
|
||||||
|
"local shard {} for needle {} is too short ({}), cannot read chunk {}/{}",
|
||||||
|
sid,
|
||||||
|
id.0,
|
||||||
|
shard.file_size(),
|
||||||
|
i + 1,
|
||||||
|
locations.len()
|
||||||
|
));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
chunk_buf.resize(ssize as usize, 0);
|
||||||
|
match shard.read_at(&mut chunk_buf, soffset as u64) {
|
||||||
|
Err(e) => {
|
||||||
|
broken_shards.insert(sid);
|
||||||
|
errs.push(format!(
|
||||||
|
"failed to read chunk {}/{} for needle {} from local shard {} at offset {}: {}",
|
||||||
|
i + 1,
|
||||||
|
locations.len(),
|
||||||
|
id.0,
|
||||||
|
sid,
|
||||||
|
soffset,
|
||||||
|
e
|
||||||
|
));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Ok(got) if got as i64 != ssize => {
|
||||||
|
broken_shards.insert(sid);
|
||||||
|
errs.push(format!(
|
||||||
|
"expected {} bytes for chunk {}/{} for needle {} from local shard {}, got {}",
|
||||||
|
ssize,
|
||||||
|
i + 1,
|
||||||
|
locations.len(),
|
||||||
|
id.0,
|
||||||
|
sid,
|
||||||
|
got
|
||||||
|
));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Ok(_) => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !has_remote_chunks {
|
||||||
|
data.extend_from_slice(&chunk_buf);
|
||||||
|
}
|
||||||
|
read += ssize;
|
||||||
|
}
|
||||||
|
|
||||||
|
local_shard_ids.sort_unstable();
|
||||||
|
|
||||||
|
let want = get_actual_size(size, self.version);
|
||||||
|
if read != want {
|
||||||
|
// Like Go, returning from the walk callback aborts the scan.
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::Other,
|
||||||
|
format!(
|
||||||
|
"expected {} bytes for needle {} on volume {}, got {}",
|
||||||
|
want, id.0, self.volume_id.0, read
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only a fully-local needle can be reassembled and CRC-checked.
|
||||||
|
if !has_remote_chunks {
|
||||||
|
let mut n = Needle::default();
|
||||||
|
if let Err(e) = n.read_bytes(&data, 0, size, self.version) {
|
||||||
|
errs.push(format!(
|
||||||
|
"needle {} on volume {}, shards {:?}: {}",
|
||||||
|
id.0, self.volume_id.0, local_shard_ids, e
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
if let Err(e) = walk {
|
||||||
|
// Go appends the walk/callback error verbatim.
|
||||||
|
errs.push(e.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut broken: Vec<crate::pb::volume_server_pb::EcShardInfo> = broken_shards
|
||||||
|
.iter()
|
||||||
|
.filter_map(|sid| self.shards.get(*sid as usize).and_then(|s| s.as_ref()))
|
||||||
|
.map(|s| s.to_ec_shard_info())
|
||||||
|
.collect();
|
||||||
|
broken.sort_by(|a, b| a.shard_id.cmp(&b.shard_id));
|
||||||
|
|
||||||
|
(count, broken, errs)
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Deletion ----
|
// ---- Deletion ----
|
||||||
|
|
||||||
/// Write `TOMBSTONE_FILE_SIZE` over the Size field of an existing .ecx
|
/// Write `TOMBSTONE_FILE_SIZE` over the Size field of an existing .ecx
|
||||||
@@ -1272,4 +1422,45 @@ mod tests {
|
|||||||
assert_eq!(vol.data_shards, 16);
|
assert_eq!(vol.data_shards, 16);
|
||||||
assert_eq!(vol.parity_shards, 4);
|
assert_eq!(vol.parity_shards, 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_scrub_local_skips_tombstones() {
|
||||||
|
let tmp = TempDir::new().unwrap();
|
||||||
|
let dir = tmp.path().to_str().unwrap();
|
||||||
|
let entries = vec![(NeedleId(1), Offset::from_actual_offset(0), Size(-1))];
|
||||||
|
write_ecx_file(dir, "", VolumeId(1), &entries);
|
||||||
|
|
||||||
|
let vol = EcVolume::new(dir, dir, "", VolumeId(1)).unwrap();
|
||||||
|
let (count, broken, errs) = vol.scrub_local();
|
||||||
|
assert_eq!(count, 1);
|
||||||
|
assert!(broken.is_empty(), "{:?}", broken);
|
||||||
|
assert!(errs.is_empty(), "{:?}", errs);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_scrub_local_clean_when_no_local_shards() {
|
||||||
|
let tmp = TempDir::new().unwrap();
|
||||||
|
let dir = tmp.path().to_str().unwrap();
|
||||||
|
let entries = vec![(NeedleId(1), Offset::from_actual_offset(0), Size(100))];
|
||||||
|
write_ecx_file(dir, "", VolumeId(1), &entries);
|
||||||
|
|
||||||
|
// dat_file_size makes the shard-size math well-defined.
|
||||||
|
let vif = crate::storage::volume::VifVolumeInfo {
|
||||||
|
dat_file_size: 14000,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let base = crate::storage::volume::volume_file_name(dir, "", VolumeId(1));
|
||||||
|
std::fs::write(
|
||||||
|
format!("{}.vif", base),
|
||||||
|
serde_json::to_string_pretty(&vif).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// No shard files present: nothing to verify locally, so no errors.
|
||||||
|
let vol = EcVolume::new(dir, dir, "", VolumeId(1)).unwrap();
|
||||||
|
let (count, broken, errs) = vol.scrub_local();
|
||||||
|
assert_eq!(count, 1);
|
||||||
|
assert!(broken.is_empty(), "{:?}", broken);
|
||||||
|
assert!(errs.is_empty(), "{:?}", errs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user