Files
upstream-seaweedfs/weed/storage/erasure_coding/ec_volume_scrub.go
upstream-mirror 89f5be005d
Some checks failed
go: build dev binaries / cleanup (push) Successful in 4s
Codespell / Check for spelling errors (push) Failing after 3s
docker: build dev containers / build-rust-binaries (amd64, x86_64-unknown-linux-musl) (push) Failing after 3s
docker: build dev containers / build-dev-containers (push) Has been skipped
docker: build dev containers / build-rust-binaries (arm64, true, aarch64-unknown-linux-musl) (push) Failing after 3s
End to End / FUSE Mount (push) Failing after 2s
EC Integration Tests / EC Integration Tests (push) Failing after 30s
FUSE DLM Integration Tests / FUSE DLM Integration Tests (push) Failing after 3s
FUSE Volume Server Failover Tests / FUSE Volume Server Failover (push) Failing after 3s
FUSE Integration Tests / FUSE Integration Testing (push) Failing after 2s
FUSE P2P Peer Chunk Sharing Integration Tests / FUSE P2P Peer Chunk Sharing (push) Failing after 3s
go: build binary / Go Vet (push) Failing after 2s
go: build binary / Go Vet 32-bit (push) Failing after 3s
go: build binary / Build (push) Failing after 4s
go: build binary / Build cross-platform (freebsd/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (darwin/arm64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/arm64) (push) Failing after 3s
go: build binary / Test (push) Failing after 2s
go: build binary / Test 32-bit (push) Failing after 3s
helm: lint and test charts / lint-test (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (11) (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (17) (push) Failing after 3s
go: build dev binaries / build_dev_linux_windows (amd64, linux) (push) Failing after 2s
go: build dev binaries / build_dev_linux_windows (amd64, windows) (push) Failing after 3s
go: build dev binaries / build_dev_darwin (amd64, darwin) (push) Has been skipped
go: build dev binaries / build_dev_darwin (arm64, darwin) (push) Has been skipped
Java Client Unit Tests / Java Unit Tests (11) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (17) (push) Failing after 2s
Java Client Unit Tests / Java Unit Tests (21) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (8) (push) Failing after 3s
Kafka Quick Test (Load Test with Schema Registry) / Kafka Client Load Test (Quick) (push) Failing after 3s
Kafka Gateway Tests / Kafka Unit Tests (unit-tests-1) (push) Failing after 11s
Kafka Gateway Tests / Kafka Integration Tests (Critical) (integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka End-to-End Tests (with SMQ) (e2e-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Consumer Group Tests (Highly Isolated) (consumer-group-1) (push) Failing after 2s
Kafka Gateway Tests / Kafka Client Compatibility (with SMQ) (client-compat-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka SMQ Integration Tests (Full Stack) (smq-integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Protocol Tests (Isolated) (protocol-1) (push) Failing after 3s
KMS Tests / KMS Provider Integration Tests (push) Failing after 2s
KMS Tests / S3 KMS End-to-End Tests (push) Failing after 3s
Master Cold Start Tests / Master Cold Start Tests (push) Failing after 4s
Metadata Subscribe Integration Tests / Metadata Subscribe Integration Tests (push) Failing after 3s
Multi-Master Tests / Multi-Master Failover Tests (push) Failing after 3s
Performance / CPU and Heap Profile (push) Failing after 2s
Performance / S3 Read/Write Benchmark (rust volume) (push) Failing after 3s
pjdfstest POSIX Compliance / pjdfstest (push) Failing after 2s
Plugin Worker Integration Tests / Plugin Worker: erasure_coding (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: vacuum (push) Failing after 3s
Rust Volume Server Tests / Rust Integration Tests (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 1) (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 1) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 3) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Distributed Tests (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Performance Tests (push) Failing after 3s
Performance / Throughput Benchmark (go volume) (push) Failing after 3s
Performance / Throughput Benchmark (rust volume) (push) Failing after 2s
Performance / S3 Read/Write Benchmark (go volume) (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: volume_balance (push) Failing after 3s
PostgreSQL Gateway Tests / PostgreSQL Basic Tests (push) Failing after 3s
Rust Volume Server Tests / Rust Unit Tests (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 3) (push) Failing after 3s
Rust Plugin Worker Tests / Rust Plugin Worker Build and Unit Tests (push) Failing after 3s
rust: build dev volume server binaries / cleanup (push) Successful in 3s
S3 ETag and ACL Tests / S3 ETag + ACL Integration Tests (push) Failing after 3s
S3 IAM Integration Tests / IAM Unit Tests (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (advanced) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (basic) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (group) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (policy-enforcement) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (sts) (push) Failing after 4s
S3 Keycloak Integration Tests / S3 Keycloak Integration Tests (push) Failing after 3s
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.11) (push) Failing after 3s
S3 PyArrow Parquet Tests / Go Unit Tests (Implicit Directory) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.12) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-amd64, x86_64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-arm64, aarch64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-linux (linux-amd64, x86_64-unknown-linux-gnu) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.9) (push) Has been cancelled
S3 Proxy Signature Tests / S3 Proxy Signature Verification Tests (push) Has been cancelled
S3 SDK V2 Route Disambiguation Tests / S3 SDK V2 Routing Tests (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (quick) (push) Has been cancelled
S3 SSE Tests / S3 SSE Compatibility Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Metadata Persistence Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Copy Operations Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Multipart Upload Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Performance Test (push) Has been cancelled
S3 SSE Tests / S3 Volume Encryption Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (comprehensive) (push) Has been cancelled
Spark Integration Tests / Spark Integration Tests (push) Failing after 2s
Telemetry Integration Tests / telemetry-integration-test (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 1) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 1) (push) Failing after 3s
Ceph S3 tests / Basic S3 tests (KV store) (push) Has been cancelled
Ceph S3 tests / S3 Versioning & Object Lock tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Unit Tests (push) Failing after 3s
Ceph S3 tests / S3 CORS tests (push) Has been cancelled
Ceph S3 tests / SeaweedFS Custom S3 Copy tests (push) Has been cancelled
Ceph S3 tests / Basic S3 tests (SQL store) (push) Has been cancelled
test s3 over https using aws-cli / awscli-tests (push) Has been cancelled
TLS Rotation Integration Tests / TLS Rotation Integration Tests (push) Has been cancelled
Vacuum Integration Tests / Vacuum Integration Tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Variables Integration Tests (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (advanced-policy) (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (basic-policy) (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (contextual-policy) (push) Failing after 4s
S3 Policy Integration Tests / Trusted Proxy Detection Tests (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Shell Integration Tests (push) Failing after 3s
Samba on FUSE Integration / samba-integration (push) Failing after 2s
SFTP Integration Tests / SFTP Integration Testing (push) Failing after 3s
terraform: validate and test modules / fmt, validate, plan-level tests (push) Failing after 3s
terraform: validate and test modules / local cluster smoke test (real weed) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 3) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 2) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 2) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 3) (push) Failing after 5s
mount: windows conformance / WinFsp conformance (push) Has been cancelled
mount: windows / Mount on Windows (push) Has been cancelled
snapshot of https://github.com/seaweedfs/seaweedfs.git (master, 2026-09-16; blobs > 131072B omitted)
2026-09-16 03:23:42 +05:00

316 lines
11 KiB
Go

package erasure_coding
import (
"bytes"
"errors"
"fmt"
"slices"
"github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb"
"github.com/seaweedfs/seaweedfs/weed/storage/idx"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
"github.com/seaweedfs/seaweedfs/weed/storage/types"
)
// ScrubIndex verifies index integrity of an EC volume.
func (ev *EcVolume) ScrubIndex() (int64, []error) {
if ev.ecxFile == nil {
return 0, []error{fmt.Errorf("no ECX file associated with EC volume %v", ev.VolumeId)}
}
if ev.ecxFileSize == 0 {
return 0, []error{fmt.Errorf("zero-size ECX file for EC volume %v", ev.VolumeId)}
}
return idx.CheckIndexFile(ev.ecxFile, ev.ecxFileSize, ev.Version)
}
// ChecksumScrub verifies every locally-held EC shard's raw bytes against the
// active-generation bitrot checksum sidecar. It is read-only: it detects and
// reports corruption but never mutates or quarantines anything (the apply path
// drives quarantine + rebuild). It is the only path that exercises cold parity
// shards, which are never read during normal serving.
//
// Returns the number of blocks scanned, the broken shards (sidecar mismatch or
// length drift), and errors. A wholesale mismatch (more than parity_shards
// shards differing) is reported as a suspect/stale sidecar — an integrity error
// — rather than flagging the shards as corrupt, so a bad sidecar can never be
// mistaken for mass bitrot.
func (ecv *EcVolume) ChecksumScrub() (blocksScanned int64, brokenShards []*volume_server_pb.EcShardInfo, errs []error) {
prot, status := ecv.BitrotProtection()
switch status {
case BitrotOff:
// Unprotected generation: nothing to verify. Not an error.
return 0, nil, nil
case BitrotInvalid:
return 0, nil, []error{fmt.Errorf("ec volume %d: bitrot sidecar is malformed/unverifiable (sidecar integrity)", ecv.VolumeId)}
}
blockSize := int64(prot.BlockSize)
var broken []*EcVolumeShard
for _, shard := range ecv.Shards {
entry := shardChecksums(prot, uint32(shard.ShardId))
if entry == nil {
errs = append(errs, fmt.Errorf("ec volume %d: no checksum entry for local shard %d", ecv.VolumeId, shard.ShardId))
continue
}
if shard.Size() != entry.CoveredSize {
errs = append(errs, fmt.Errorf("ec volume %d shard %d: size %d != covered_size %d (truncated or extended)",
ecv.VolumeId, shard.ShardId, shard.Size(), entry.CoveredSize))
broken = append(broken, shard)
continue
}
want := unpackUint32LE(entry.BlockCrc32C)
buf := make([]byte, blockSize)
var offset int64
shardBad := false
for i := 0; i < len(want); i++ {
toRead := blockSize
if rem := entry.CoveredSize - offset; rem < toRead {
toRead = rem
}
n, rerr := shard.ReadAt(buf[:toRead], offset)
if rerr != nil || int64(n) != toRead {
errs = append(errs, fmt.Errorf("ec volume %d shard %d: read block %d at %d: %v (got %d/%d)",
ecv.VolumeId, shard.ShardId, i, offset, rerr, n, toRead))
shardBad = true
break
}
if uint32(needle.NewCRC(buf[:n])) != want[i] {
errs = append(errs, fmt.Errorf("ec volume %d shard %d: checksum mismatch at block %d (offset %d)",
ecv.VolumeId, shard.ShardId, i, offset))
shardBad = true
}
offset += int64(n)
blocksScanned++
}
if shardBad {
broken = append(broken, shard)
}
}
// Wholesale-mismatch guard: localized bitrot touches a few shards; a
// stale/wrong sidecar mismatches more than parity_shards. Classify the
// latter as sidecar integrity, never as mass shard corruption.
if ecv.ECContext != nil && len(broken) > ecv.ECContext.ParityShards {
return blocksScanned, nil, []error{fmt.Errorf("ec volume %d: %d/%d local shards mismatch (> parity %d): suspect stale sidecar, not flagging shard corruption",
ecv.VolumeId, len(broken), len(ecv.Shards), ecv.ECContext.ParityShards)}
}
// Reed-Solomon arbitration: a sidecar mismatch could mean the shard rotted
// OR the sidecar block is stale. When enough OTHER shards are local to
// reconstruct, reconstruct each flagged shard from the verified-clean shards
// and compare to its on-disk bytes. Only shards RS *also* disagrees with are
// truly corrupt; the rest are stale-sidecar false positives we must not
// quarantine. This is the arbiter that makes auto-repair safe.
if ecv.ECContext != nil && len(broken) > 0 {
brokenSet := make(map[ShardId]bool, len(broken))
for _, s := range broken {
brokenSet[s.ShardId] = true
}
cleanCount := 0
for _, s := range ecv.Shards {
if !brokenSet[s.ShardId] {
cleanCount++
}
}
if cleanCount >= ecv.ECContext.DataShards {
confirmed := make([]*EcVolumeShard, 0, len(broken))
for _, s := range broken {
corrupt, aerr := ecv.rsConfirmsShardCorrupt(s.ShardId, brokenSet, blockSize)
if aerr != nil {
// Could not arbitrate this shard; stay conservative and keep it.
errs = append(errs, fmt.Errorf("ec volume %d shard %d: RS arbitration failed: %v", ecv.VolumeId, s.ShardId, aerr))
confirmed = append(confirmed, s)
continue
}
if corrupt {
confirmed = append(confirmed, s)
} else {
errs = append(errs, fmt.Errorf("ec volume %d shard %d: sidecar mismatch but Reed-Solomon confirms the bytes are correct: stale sidecar, not flagging shard",
ecv.VolumeId, s.ShardId))
}
}
broken = confirmed
}
}
for _, s := range broken {
brokenShards = append(brokenShards, s.ToEcShardInfo())
}
slices.SortFunc(brokenShards, func(a, b *volume_server_pb.EcShardInfo) int {
return int(a.ShardId) - int(b.ShardId)
})
return blocksScanned, brokenShards, errs
}
// rsConfirmsShardCorrupt reconstructs targetId from the verified-clean local
// shards (all local shards except those in brokenSet) and compares the result
// to targetId's on-disk bytes block-by-block. It returns true only if
// Reed-Solomon disagrees with the disk (the shard is genuinely corrupt); a full
// match means the shard is fine and its sidecar block is stale. The caller
// guarantees at least DataShards clean shards are local before invoking this.
func (ecv *EcVolume) rsConfirmsShardCorrupt(targetId ShardId, brokenSet map[ShardId]bool, blockSize int64) (bool, error) {
enc, err := ecv.ECContext.CreateEncoder()
if err != nil {
return false, err
}
total := ecv.ECContext.Total()
local := make(map[ShardId]*EcVolumeShard, len(ecv.Shards))
for _, s := range ecv.Shards {
local[s.ShardId] = s
}
target := local[targetId]
if target == nil {
return false, fmt.Errorf("target shard not local")
}
size := target.Size()
buffers := make([][]byte, total)
diskBuf := make([]byte, blockSize)
var offset int64
for offset < size {
n := blockSize
if rem := size - offset; rem < n {
n = rem
}
// Fill clean local shards; nil for the target and any broken/absent shard
// so Reconstruct regenerates the target from the trusted inputs.
for id := 0; id < total; id++ {
sid := ShardId(id)
if sid == targetId || brokenSet[sid] {
buffers[id] = nil
continue
}
s := local[sid]
if s == nil {
buffers[id] = nil
continue
}
buf := make([]byte, n)
got, rerr := s.ReadAt(buf, offset)
if rerr != nil || int64(got) != n {
return false, fmt.Errorf("read clean shard %d at %d: %v (got %d/%d)", sid, offset, rerr, got, n)
}
buffers[id] = buf
}
if err := enc.Reconstruct(buffers); err != nil {
return false, fmt.Errorf("reconstruct: %w", err)
}
got, rerr := target.ReadAt(diskBuf[:n], offset)
if rerr != nil || int64(got) != n {
return false, fmt.Errorf("read target shard %d at %d: %v", targetId, offset, rerr)
}
if !bytes.Equal(buffers[targetId][:n], diskBuf[:n]) {
return true, nil // Reed-Solomon disagrees with disk: genuinely corrupt.
}
offset += n
}
return false, nil // every block matches: shard is fine, sidecar block is stale.
}
// ScrubLocal checks the integrity of local shards for a EC volume. Notably, it cannot verify CRC on needles.
// Returns a count of processed file entries, slice of found broken shards, and slice of found errors.
func (ecv *EcVolume) ScrubLocal() (int64, []*volume_server_pb.EcShardInfo, []error) {
// local scan means verifying indexes as well
_, errs := ecv.ScrubIndex()
brokenShardsMap := map[ShardId]*EcVolumeShard{}
var count int64
flagShardBroken := func(ecs *EcVolumeShard, errFmt string, a ...any) {
// reads for EC chunks can hit the same shard multiple times, so dedupe upon read errors
brokenShardsMap[ecs.ShardId] = ecs
errs = append(errs, fmt.Errorf(errFmt, a...))
}
err := idx.WalkIndexFile(ecv.ecxFile, 0, func(id types.NeedleId, offset types.Offset, size types.Size) error {
count++
if size.IsTombstone() {
// nothing to do for tombstones...
return nil
}
var read int64
var hasRemoteChunks bool
var data []byte
locations := ecv.LocateEcShardNeedleInterval(ecv.Version, offset.ToActualOffset(), size)
localShardIds := []ShardId{}
for i, iv := range locations {
sid, soffset := ecv.IntervalToShardIdAndOffset(iv)
ssize := int64(iv.Size.Raw())
shard, found := ecv.FindEcVolumeShard(sid)
if !found {
// shard is not local :( skip it
hasRemoteChunks = true
read += ssize
continue
}
localShardIds = append(localShardIds, sid)
if soffset+int64(ssize) > shard.Size() {
flagShardBroken(shard, "local shard %d for needle %d is too short (%d), cannot read chunk %d/%d", sid, id, shard.Size(), i+1, len(locations))
continue
}
chunk := make([]byte, ssize)
got, err := shard.ReadAt(chunk, soffset)
if err != nil {
flagShardBroken(shard, "failed to read chunk %d/%d for needle %d from local shard %d at offset %d: %v", i+1, len(locations), id, sid, soffset, err)
continue
}
if int64(got) != ssize {
flagShardBroken(shard, "expected %d bytes for chunk %d/%d for needle %d from local shard %d, got %d", ssize, i+1, len(locations), id, sid, got)
continue
}
if !hasRemoteChunks {
data = append(data, chunk...)
}
read += int64(got)
}
slices.Sort(localShardIds)
if got, want := read, needle.GetActualSize(size, ecv.Version); got != want {
return fmt.Errorf("expected %d bytes for needle %d on volume %d, got %d", want, id, ecv.VolumeId, got)
}
if !hasRemoteChunks {
// needle was fully recovered from local shards \o/ let's check it
n := needle.Needle{}
if err := n.ReadBytes(data, 0, size, ecv.Version); err != nil {
// A delete-state disagreement between the .ecx index and the reassembled
// on-disk header (live index vs zero header size) is not corruption.
deleteStateDisagrees := errors.Is(err, needle.ErrorSizeMismatch) && size.IsDeleted() != (n.Size == 0)
if !deleteStateDisagrees {
errs = append(errs, fmt.Errorf("needle %d on volume %d, shards %v: %v", id, ecv.VolumeId, localShardIds, err))
}
}
}
return nil
})
if err != nil {
errs = append(errs, err)
}
// collect broken shard infos for reporting
brokenShards := make([]*volume_server_pb.EcShardInfo, 0, len(brokenShardsMap))
for _, s := range brokenShardsMap {
brokenShards = append(brokenShards, s.ToEcShardInfo())
}
slices.SortFunc(brokenShards, func(a, b *volume_server_pb.EcShardInfo) int {
if a.ShardId < b.ShardId {
return -1
}
if a.ShardId > b.ShardId {
return 1
}
return 0
})
return count, brokenShards, errs
}