package erasure_coding import ( "fmt" "io" "os" "path/filepath" "github.com/klauspost/reedsolomon" "github.com/seaweedfs/seaweedfs/weed/glog" "github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb" "github.com/seaweedfs/seaweedfs/weed/storage/idx" "github.com/seaweedfs/seaweedfs/weed/storage/needle_map" "github.com/seaweedfs/seaweedfs/weed/storage/types" "github.com/seaweedfs/seaweedfs/weed/storage/volume_info" "github.com/seaweedfs/seaweedfs/weed/util" ) const ( DataShardsCount = 10 ParityShardsCount = 4 TotalShardsCount = DataShardsCount + ParityShardsCount MaxShardCount = 32 // Maximum number of shards since ShardBits is uint32 (bits 0-31) MinTotalDisks = TotalShardsCount/ParityShardsCount + 1 ErasureCodingLargeBlockSize = 1024 * 1024 * 1024 // 1GB ErasureCodingSmallBlockSize = 1024 * 1024 // 1MB ) // WriteSortedFileFromIdx generates .ecx file from existing .idx file // all keys are sorted in ascending order func WriteSortedFileFromIdx(baseFileName string, ext string) (e error) { nm, err := readNeedleMap(baseFileName) if nm != nil { defer nm.Close() } if err != nil { return fmt.Errorf("readNeedleMap: %w", err) } ecxFile, err := os.OpenFile(baseFileName+ext, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { return fmt.Errorf("failed to open ecx file: %w", err) } defer ecxFile.Close() err = nm.AscendingVisit(func(value needle_map.NeedleValue) error { bytes := value.ToBytes() _, writeErr := ecxFile.Write(bytes) return writeErr }) if err != nil { return fmt.Errorf("failed to visit idx file: %w", err) } return nil } // WriteEcFiles generates .ec00 ~ .ec13 files from baseFileName.dat. Pass // BackgroundECContext for the default ratio, or an explicit ctx for a configured // (e.g. custom-ratio) layout. It returns the bitrot protection (per-shard block // CRC32C) computed during the single encode pass; the caller persists it as a // .ecsum sidecar. func WriteEcFiles(baseFileName string, ctx *ECContext) (*volume_server_pb.EcBitrotProtection, error) { if ctx == nil || ctx.Total() == 0 { ctx = NewDefaultECContext("", 0) } return generateEcFiles(baseFileName, 256*1024, ErasureCodingLargeBlockSize, ErasureCodingSmallBlockSize, ctx) } // RebuildEcFiles rebuilds missing EC shard files. Pass BackgroundECContext to // resolve the layout from the volume's .vif (falling back to the default ratio), // or an explicit ctx when the caller already knows the shard layout. // additionalDirs are extra directories to search for existing shard files, // which handles multi-disk servers where shards may be spread across disks. // When a bitrot checksum sidecar is present for the (generation-0) volume, // present input shards are verified against it and corrupt ones are excluded // from Reed-Solomon and regenerated; unsafeIgnoreSidecar bypasses that guard. func RebuildEcFiles(baseFileName string, ctx *ECContext, unsafeIgnoreSidecar bool, additionalDirs ...string) ([]uint32, error) { if ctx == nil || ctx.Total() == 0 { // Resolve the layout from the .vif to preserve the original configuration. volumeInfo, _, foundVif, vifErr := volume_info.MaybeLoadVolumeInfo(baseFileName + ".vif") if vifErr != nil { // The .vif exists but cannot be read or parsed. Fail closed rather // than silently falling back to the default ratio, which would // rebuild a custom-ratio volume with the wrong layout. Pass an // explicit ctx to override. return nil, fmt.Errorf("RebuildEcFiles %s: cannot load .vif: %w", baseFileName, vifErr) } if foundVif && volumeInfo.EcShardConfig != nil { ds := int(volumeInfo.EcShardConfig.DataShards) ps := int(volumeInfo.EcShardConfig.ParityShards) // Validate EC config before using it if ds > 0 && ps > 0 && ds+ps <= MaxShardCount { ctx = &ECContext{ DataShards: ds, ParityShards: ps, } glog.V(0).Infof("Rebuilding EC files for %s with config from .vif: %s", baseFileName, ctx.String()) } else { glog.Warningf("Invalid EC config in .vif for %s (data=%d, parity=%d), using default", baseFileName, ds, ps) ctx = NewDefaultECContext("", 0) } } else { glog.V(0).Infof("Rebuilding EC files for %s with default config", baseFileName) ctx = NewDefaultECContext("", 0) } } return generateMissingEcFiles(baseFileName, 256*1024, ErasureCodingLargeBlockSize, ErasureCodingSmallBlockSize, ctx, unsafeIgnoreSidecar, additionalDirs) } func ToExt(ecIndex int) string { return fmt.Sprintf(".ec%02d", ecIndex) } func generateEcFiles(baseFileName string, bufferSize int, largeBlockSize int64, smallBlockSize int64, ctx *ECContext) (*volume_server_pb.EcBitrotProtection, error) { file, err := os.OpenFile(baseFileName+".dat", os.O_RDONLY, 0) if err != nil { return nil, fmt.Errorf("failed to open dat file: %w", err) } defer file.Close() fi, err := file.Stat() if err != nil { return nil, fmt.Errorf("failed to stat dat file: %w", err) } // One rolling-CRC builder per shard; fed as each shard's bytes are written. builders := make([]*shardChecksumBuilder, ctx.Total()) for i := range builders { builders[i] = newShardChecksumBuilder(BitrotBlockSize) } glog.V(0).Infof("encodeDatFile %s.dat size:%d with EC context %s", baseFileName, fi.Size(), ctx.String()) err = encodeDatFile(fi.Size(), baseFileName, bufferSize, largeBlockSize, file, smallBlockSize, ctx, builders) if err != nil { return nil, fmt.Errorf("encodeDatFile: %w", err) } return buildProtectionFromBuilders(ctx, builders, BitrotBlockSize), nil } // findShardFile looks for a shard file at baseFileName+ext, then in additionalDirs. func findShardFile(baseFileName string, ext string, additionalDirs []string) string { primary := baseFileName + ext if util.FileExists(primary) { return primary } baseName := filepath.Base(baseFileName) for _, dir := range additionalDirs { candidate := filepath.Join(dir, baseName+ext) if util.FileExists(candidate) { return candidate } } return "" } func generateMissingEcFiles(baseFileName string, bufferSize int, largeBlockSize int64, smallBlockSize int64, ctx *ECContext, unsafeIgnoreSidecar bool, additionalDirs []string) (generatedShardIds []uint32, err error) { // Pass 1: discover which shards exist and which are missing, // opening input files but NOT creating output files yet. shardHasData := make([]bool, ctx.Total()) shardPaths := make([]string, ctx.Total()) // non-empty for present shards (also the in-place output for a reclassified-corrupt shard) inputFiles := make([]*os.File, ctx.Total()) presentCount := 0 for shardId := 0; shardId < ctx.Total(); shardId++ { ext := ctx.ToExt(shardId) shardPath := findShardFile(baseFileName, ext, additionalDirs) if shardPath != "" { shardHasData[shardId] = true shardPaths[shardId] = shardPath inputFiles[shardId], err = os.OpenFile(shardPath, os.O_RDONLY, 0) if err != nil { return nil, err } defer inputFiles[shardId].Close() presentCount++ } else { generatedShardIds = append(generatedShardIds, uint32(shardId)) } } // Bitrot verify-and-exclude: when a generation-0 checksum sidecar is present // and valid, verify each present input shard against it and reclassify // corrupt ones as missing so Reed-Solomon regenerates them instead of // silently consuming corrupt bytes. corruptOwned marks shards whose // (corrupt) original file must be replaced in place at its discovered path. corruptOwned := make([]bool, ctx.Total()) prot, status := loadRebuildSidecar(baseFileName, ctx, additionalDirs) switch status { case BitrotInvalid: if !unsafeIgnoreSidecar { return nil, fmt.Errorf("bitrot sidecar for %s is malformed/unverifiable; refusing to rebuild (pass unsafeIgnoreSidecar to override)", baseFileName) } glog.Warningf("bitrot sidecar for %s is malformed/unverifiable; proceeding because unsafeIgnoreSidecar is set", baseFileName) case BitrotOn: corrupt := make([]int, 0, ctx.Total()) for shardId := 0; shardId < ctx.Total(); shardId++ { if !shardHasData[shardId] { continue } entry := shardChecksums(prot, uint32(shardId)) if entry == nil { continue } mismatched, verr := verifyShardFileBlocks(shardPaths[shardId], entry, int64(prot.BlockSize)) if verr != nil { // A read error means we cannot trust this shard as a Reed-Solomon // input. Exclude it (treat as corrupt) rather than silently // feeding possibly-corrupt bytes into reconstruction. glog.Warningf("bitrot: failed to verify present shard %d for %s: %v; excluding it", shardId, baseFileName, verr) corrupt = append(corrupt, shardId) continue } if len(mismatched) > 0 { corrupt = append(corrupt, shardId) } } if len(corrupt) > 0 { // Wholesale-mismatch guard (RS-arbiter conservative form): localized // bitrot touches a few shards; a stale/wrong sidecar mismatches more // than parity_shards. In that case refuse rather than excluding good // shards en masse. if len(corrupt) > ctx.ParityShards && !unsafeIgnoreSidecar { return nil, fmt.Errorf("bitrot sidecar suspect for %s: %d/%d present shards mismatch (> parity %d); refusing to rebuild (pass unsafeIgnoreSidecar to override)", baseFileName, len(corrupt), presentCount, ctx.ParityShards) } if presentCount-len(corrupt) < ctx.DataShards && !unsafeIgnoreSidecar { return nil, fmt.Errorf("bitrot: only %d verified-good shards for %s, need %d data shards; sidecar may be stale (pass unsafeIgnoreSidecar to override)", presentCount-len(corrupt), baseFileName, ctx.DataShards) } if !unsafeIgnoreSidecar { for _, shardId := range corrupt { glog.Warningf("bitrot: present shard %d for %s fails checksum; excluding from rebuild inputs and regenerating", shardId, baseFileName) shardHasData[shardId] = false corruptOwned[shardId] = true generatedShardIds = append(generatedShardIds, uint32(shardId)) presentCount-- } } } } // Pre-check: bail out before creating any output files. if presentCount < ctx.DataShards { return nil, fmt.Errorf("not enough shards to rebuild %s: found %d shards, need at least %d (data shards), missing shards: %v", baseFileName, presentCount, ctx.DataShards, generatedShardIds) } glog.V(0).Infof("rebuilding %s: %d shards present, %d missing %v, config %s", baseFileName, presentCount, len(generatedShardIds), generatedShardIds, ctx.String()) // Pass 2: create output files for missing shards. A genuinely-absent shard // is written at baseFileName+ext; a reclassified-corrupt shard is written to // a temp file beside its discovered location and atomically renamed over the // corrupt original after the rebuild (and checksum) succeed, so we never // leave a duplicate shard id or a half-written file. outputFiles := make([]*os.File, ctx.Total()) writePaths := make([]string, ctx.Total()) finalPaths := make([]string, ctx.Total()) for shardId := 0; shardId < ctx.Total(); shardId++ { if shardHasData[shardId] { continue } finalPath := baseFileName + ctx.ToExt(shardId) writePath := finalPath if corruptOwned[shardId] && shardPaths[shardId] != "" { finalPath = shardPaths[shardId] writePath = shardPaths[shardId] + ".rebuilding" } outputFiles[shardId], err = os.OpenFile(writePath, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0644) if err != nil { return nil, err } defer outputFiles[shardId].Close() writePaths[shardId] = writePath finalPaths[shardId] = finalPath } if err = rebuildEcFiles(shardHasData, inputFiles, outputFiles, ctx); err != nil { return nil, fmt.Errorf("rebuildEcFiles: %w", err) } // Verify regenerated shards against the sidecar. Reed-Solomon is // deterministic, so a regenerated shard that does NOT match the sidecar // means the sidecar is wrong/stale (not the shard) — fail closed rather than // publishing bytes we cannot trust. On ANY verification failure (sync, read // error, or mismatch) remove every generated output so the rebuild publishes // nothing: a genuinely-missing shard returns to missing; a reclassified- // corrupt shard keeps its untouched original. if status == BitrotOn && !unsafeIgnoreSidecar { for shardId := 0; shardId < ctx.Total(); shardId++ { if writePaths[shardId] == "" { continue } entry := shardChecksums(prot, uint32(shardId)) if entry == nil { continue } if err = outputFiles[shardId].Sync(); err != nil { cleanupRebuildOutputs(outputFiles, writePaths) return nil, fmt.Errorf("sync regenerated shard %d: %w", shardId, err) } mismatched, verr := verifyShardFileBlocks(writePaths[shardId], entry, int64(prot.BlockSize)) if verr != nil { cleanupRebuildOutputs(outputFiles, writePaths) return nil, fmt.Errorf("bitrot: verify regenerated shard %d for %s: %w", shardId, baseFileName, verr) } if len(mismatched) > 0 { cleanupRebuildOutputs(outputFiles, writePaths) return nil, fmt.Errorf("bitrot: regenerated shard %d for %s does not match sidecar (%d blocks differ); sidecar likely stale — aborting (pass unsafeIgnoreSidecar to override)", shardId, baseFileName, len(mismatched)) } } } // Atomically move reclassified-corrupt rebuilds over their originals. for shardId := 0; shardId < ctx.Total(); shardId++ { if writePaths[shardId] != "" && writePaths[shardId] != finalPaths[shardId] { outputFiles[shardId].Close() if rerr := os.Rename(writePaths[shardId], finalPaths[shardId]); rerr != nil { return nil, fmt.Errorf("bitrot: replace corrupt shard %d (%s -> %s): %w", shardId, writePaths[shardId], finalPaths[shardId], rerr) } } } return } // cleanupRebuildOutputs removes every generated output on a failed fail-closed // rebuild: temp replacements AND genuinely-missing shards written directly at // their final path, so no unverified bytes are published. A reclassified-corrupt // shard's untouched original (at finalPath, distinct from its temp writePath) is // left in place; a genuinely-missing shard (writePath == finalPath) returns to // missing. func cleanupRebuildOutputs(outputFiles []*os.File, writePaths []string) { for i := range writePaths { if writePaths[i] == "" { continue } if outputFiles[i] != nil { outputFiles[i].Close() } os.Remove(writePaths[i]) } } // loadRebuildSidecar loads and validates the generation-0 checksum sidecar for a // rebuild. RebuildEcFiles operates on the un-suffixed (generation 0) shard // names, so only the legacy sidecar is relevant here. Returns BitrotOff when // absent or describing a different generation/config, BitrotInvalid on a // self-integrity/manifest failure, BitrotOn when usable. func loadRebuildSidecar(baseFileName string, ctx *ECContext, additionalDirs []string) (*volume_server_pb.EcBitrotProtection, BitrotStatus) { path := findBitrotSidecar(0, baseFileName, baseFileName, additionalDirs...) if path == "" { return nil, BitrotOff } prot, err := LoadBitrotSidecar(path) if err != nil { glog.Warningf("bitrot: sidecar %s self-integrity failed: %v", path, err) return nil, BitrotInvalid } if prot.Generation != 0 { return nil, BitrotOff } if prot.EcShardConfig == nil || int(prot.EcShardConfig.DataShards) != ctx.DataShards || int(prot.EcShardConfig.ParityShards) != ctx.ParityShards { return nil, BitrotOff } if err := ValidateBitrotManifest(prot, ctx.DataShards, ctx.ParityShards); err != nil { glog.Warningf("bitrot: sidecar %s manifest invalid: %v", path, err) return nil, BitrotInvalid } return prot, BitrotOn } func encodeData(file *os.File, enc reedsolomon.Encoder, startOffset, blockSize int64, buffers [][]byte, outputs []*os.File, ctx *ECContext, builders []*shardChecksumBuilder) error { bufferSize := int64(len(buffers[0])) if bufferSize == 0 { glog.Fatal("unexpected zero buffer size") } batchCount := blockSize / bufferSize if blockSize%bufferSize != 0 { glog.Fatalf("unexpected block size %d buffer size %d", blockSize, bufferSize) } for b := int64(0); b < batchCount; b++ { err := encodeDataOneBatch(file, enc, startOffset+b*bufferSize, blockSize, buffers, outputs, ctx, builders) if err != nil { return err } } return nil } func openEcFiles(baseFileName string, forRead bool, ctx *ECContext) (files []*os.File, err error) { for i := 0; i < ctx.Total(); i++ { fname := baseFileName + ctx.ToExt(i) openOption := os.O_TRUNC | os.O_CREATE | os.O_WRONLY if forRead { openOption = os.O_RDONLY } f, err := os.OpenFile(fname, openOption, 0644) if err != nil { return files, fmt.Errorf("failed to open file %s: %v", fname, err) } files = append(files, f) } return } func closeEcFiles(files []*os.File) { for _, f := range files { if f != nil { f.Close() } } } func encodeDataOneBatch(file *os.File, enc reedsolomon.Encoder, startOffset, blockSize int64, buffers [][]byte, outputs []*os.File, ctx *ECContext, builders []*shardChecksumBuilder) error { // read data into buffers for i := 0; i < ctx.DataShards; i++ { n, err := file.ReadAt(buffers[i], startOffset+blockSize*int64(i)) if err != nil { if err != io.EOF { return err } } if n < len(buffers[i]) { for t := len(buffers[i]) - 1; t >= n; t-- { buffers[i][t] = 0 } } } err := enc.Encode(buffers) if err != nil { return err } for i := 0; i < ctx.Total(); i++ { _, err := outputs[i].Write(buffers[i]) if err != nil { return err } // Accumulate this shard's block CRC over exactly the bytes written. if builders != nil && builders[i] != nil { builders[i].write(buffers[i]) } } return nil } func encodeDatFile(remainingSize int64, baseFileName string, bufferSize int, largeBlockSize int64, file *os.File, smallBlockSize int64, ctx *ECContext, builders []*shardChecksumBuilder) error { var processedSize int64 enc, err := ctx.CreateEncoder() if err != nil { return fmt.Errorf("failed to create encoder: %w", err) } buffers := make([][]byte, ctx.Total()) for i := range buffers { buffers[i] = make([]byte, bufferSize) } outputs, err := openEcFiles(baseFileName, false, ctx) defer closeEcFiles(outputs) if err != nil { return fmt.Errorf("failed to open ec files %s: %v", baseFileName, err) } // Pre-calculate row sizes to avoid redundant calculations in loops largeRowSize := largeBlockSize * int64(ctx.DataShards) smallRowSize := smallBlockSize * int64(ctx.DataShards) for remainingSize >= largeRowSize { err = encodeData(file, enc, processedSize, largeBlockSize, buffers, outputs, ctx, builders) if err != nil { return fmt.Errorf("failed to encode large chunk data: %w", err) } remainingSize -= largeRowSize processedSize += largeRowSize } for remainingSize > 0 { err = encodeData(file, enc, processedSize, smallBlockSize, buffers, outputs, ctx, builders) if err != nil { return fmt.Errorf("failed to encode small chunk data: %w", err) } remainingSize -= smallRowSize processedSize += smallRowSize } return nil } func rebuildEcFiles(shardHasData []bool, inputFiles []*os.File, outputFiles []*os.File, ctx *ECContext) error { enc, err := ctx.CreateEncoder() if err != nil { return fmt.Errorf("failed to create encoder: %w", err) } // The output shard size equals the present input shards' size (all EC // shards are equal length). Deriving it up front turns a short read on a // truncated/corrupt input into an error instead of a silent early return // that would publish truncated shards as restored redundancy. var expectedShardSize int64 = -1 for i := 0; i < ctx.Total(); i++ { if !shardHasData[i] { continue } fi, statErr := inputFiles[i].Stat() if statErr != nil { return fmt.Errorf("stat input shard %d: %w", i, statErr) } if expectedShardSize < 0 { expectedShardSize = fi.Size() } else if fi.Size() != expectedShardSize { return fmt.Errorf("ec rebuild: input shard %d size %d != %d (truncated input?)", i, fi.Size(), expectedShardSize) } } if expectedShardSize <= 0 { return fmt.Errorf("ec rebuild: no input shard data (expected shard size %d)", expectedShardSize) } buffers := make([][]byte, ctx.Total()) for i := range buffers { if shardHasData[i] { buffers[i] = make([]byte, ErasureCodingSmallBlockSize) } } for startOffset := int64(0); startOffset < expectedShardSize; { thisBlock := int64(ErasureCodingSmallBlockSize) if remaining := expectedShardSize - startOffset; remaining < thisBlock { thisBlock = remaining } // read the input data; a short read means a truncated input shard. shards := make([][]byte, ctx.Total()) for i := 0; i < ctx.Total(); i++ { if !shardHasData[i] { continue // nil: reconstructed below } b := buffers[i][:thisBlock] n, readErr := inputFiles[i].ReadAt(b, startOffset) if readErr != nil && readErr != io.EOF { return fmt.Errorf("ec rebuild read shard %d at %d: %w", i, startOffset, readErr) } if int64(n) != thisBlock { return fmt.Errorf("ec rebuild short read shard %d at %d: got %d want %d", i, startOffset, n, thisBlock) } shards[i] = b } if err = enc.Reconstruct(shards); err != nil { return fmt.Errorf("reconstruct: %w", err) } for i := 0; i < ctx.Total(); i++ { if shardHasData[i] { continue } n, writeErr := outputFiles[i].WriteAt(shards[i][:thisBlock], startOffset) if writeErr != nil { return fmt.Errorf("ec rebuild write shard %d at %d: %w", i, startOffset, writeErr) } if int64(n) != thisBlock { return fmt.Errorf("ec rebuild short write shard %d at %d: got %d want %d", i, startOffset, n, thisBlock) } } startOffset += thisBlock } // Flush every regenerated shard before it is mounted/renamed and published // as restored redundancy, so a crash cannot leave a peer trusting a shard // whose bytes never reached disk. for i := 0; i < ctx.Total(); i++ { if shardHasData[i] { continue } if err = outputFiles[i].Sync(); err != nil { return fmt.Errorf("ec rebuild sync shard %d: %w", i, err) } } return nil } func readNeedleMap(baseFileName string) (*needle_map.MemDb, error) { indexFile, err := os.OpenFile(baseFileName+".idx", os.O_RDONLY, 0644) if err != nil { return nil, fmt.Errorf("cannot read Volume Index %s.idx: %v", baseFileName, err) } defer indexFile.Close() cm := needle_map.NewMemDb() err = idx.WalkIndexFile(indexFile, 0, func(key types.NeedleId, offset types.Offset, size types.Size) error { if !offset.IsZero() && !size.IsDeleted() { cm.Set(key, offset, size) } else { cm.Delete(key) } return nil }) return cm, err }