mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-08 15:41:15 +02:00
* fix(volume_server): load orphan EC shards across disks on startup (#9212) When ec.balance / ec.rebuild copies an EC shard onto a destination node without also pinning subsequent shards to the disk that holds .ecx, the shard ends up on a different physical disk than its index files. The per-disk loadAllEcShards has no visibility into other DiskLocations on the same store, so those orphan shards were silently left out of ecVolumes and never reported to master — volume.list showed partial counts, and ec.rebuild reported the volume as unrepairable even though all shards were physically present. After every DiskLocation finishes its initial pass, sweep the store for shard files that are on disk but not yet in any EcVolume, look up the .ecx-owning sibling disk, and load each shard against its physical disk with dirIdx pointing at the sibling. Each shard is still registered on its own disk's ecVolumes map so heartbeat reporting carries the right DiskId per shard (master fix #9219 already aggregates per-disk messages correctly). Also fall back to dirIdx for .vif lookup when dir != dirIdx, so the reconciliation path doesn't write a stub .vif on the shard disk and lose the real EC config and datFileSize. * fix(volume_server): track actual .ecx dir in cross-disk reconcile indexEcxOwners scans both IdxDirectory and Directory to find each volume's .ecx — the second scan covers the legacy case where index files were written into the data dir before -dir.idx was configured (removeEcVolumeFiles already accounts for this in disk_location_ec.go). But the returned map dropped which directory matched, and reconcile unconditionally passed owner.IdxDirectory to loadEcShardsWithIdxDir. When the owner's .ecx is in Directory and IdxDirectory != Directory (server later re-configured with -dir.idx pointing at a fresh path), NewEcVolume opens IdxDirectory/.ecx → ENOENT, retries the same-disk fallback at dataBaseFileName+.ecx — but dataBaseFileName uses the *orphan* disk's data dir, not the owner's, so it ENOENTs again and the orphan shards stay unloaded. Track which scan dir matched in indexEcxOwners and pass it through. Adds TestLoadEcShardsWhenOwnerEcxIsInDataDir as the regression. Reported in PR #9244 review by @gemini-code-assist and @coderabbitai. * refactor(storage): thread dataShardCount as a parameter into calculateExpectedShardSize The helper used erasure_coding.DataShardsCount directly, but tests in store_ec_orphan_shard_test.go save .vif with a local dataShards=10 constant. If the package default ever diverged from 10 (e.g. an enterprise build), the test would write a .vif for one layout while sizing shard files for another and silently break. Take dataShardCount as a parameter. Existing callers (validateEcVolume + size-validation tests + real-world tests) pass erasure_coding.DataShardsCount unchanged. The orphan-shard tests pass the same dataShards local they save into .vif, so the persisted shape and the on-disk shape stay consistent. Reported in PR #9244 review by @coderabbitai.
171 lines
5.9 KiB
Go
171 lines
5.9 KiB
Go
package storage
|
|
|
|
import (
|
|
"os"
|
|
"path"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
|
|
)
|
|
|
|
// ecKeyForReconcile keys orphan-shard reconciliation by collection + volume
|
|
// id. Per-collection grouping matters because two collections can re-use the
|
|
// same volume id, and we must only pair shards with their own .ecx file.
|
|
type ecKeyForReconcile struct {
|
|
collection string
|
|
vid needle.VolumeId
|
|
}
|
|
|
|
// ecxOwnerInfo records both the disk that owns the .ecx and the actual
|
|
// directory it lives in (IdxDirectory or Directory). The directory matters
|
|
// because indexEcxOwners scans both — when .ecx lives in Directory (the
|
|
// legacy "written before -dir.idx was set" layout that removeEcVolumeFiles
|
|
// in disk_location_ec.go also keeps cleaning up), passing the owner's
|
|
// IdxDirectory to NewEcVolume would ENOENT both the primary and the
|
|
// same-disk fallback path, which uses the orphan disk's data dir, not the
|
|
// owner's. Tracking the actual scan dir lets reconcile point loaders at
|
|
// the directory the .ecx is really in.
|
|
type ecxOwnerInfo struct {
|
|
location *DiskLocation
|
|
idxDir string
|
|
}
|
|
|
|
// reconcileEcShardsAcrossDisks loads EC shards that the per-disk scan in
|
|
// loadAllEcShards skipped because the disk holding the .ec?? files does not
|
|
// also hold the matching .ecx / .ecj / .vif index files. The index files
|
|
// are located on a different disk of the same volume server (issue #9212).
|
|
//
|
|
// Per-disk loadAllEcShards correctly leaves these orphan shards on disk —
|
|
// it does not have visibility into other DiskLocations on the same store —
|
|
// so the cross-disk fan-out must happen here, after every disk's initial
|
|
// pass has completed. We register each shard against its physical disk's
|
|
// ecVolumes map (so heartbeat reporting carries the right DiskId per
|
|
// shard), but point the EcVolume at the sibling disk's index files so it
|
|
// can serve reads and route deletes through a real .ecx / .ecj.
|
|
func (s *Store) reconcileEcShardsAcrossDisks() {
|
|
if len(s.Locations) < 2 {
|
|
return
|
|
}
|
|
|
|
ecxOwners := s.indexEcxOwners()
|
|
if len(ecxOwners) == 0 {
|
|
return
|
|
}
|
|
|
|
for _, loc := range s.Locations {
|
|
orphans := loc.collectOrphanEcShards()
|
|
if len(orphans) == 0 {
|
|
continue
|
|
}
|
|
for key, shards := range orphans {
|
|
owner, ok := ecxOwners[key]
|
|
if !ok {
|
|
glog.Warningf("ec volume %d (collection=%q) has shards on %s without a matching .ecx anywhere on this volume server; shards %v will stay unloaded until the missing .ecx is restored",
|
|
key.vid, key.collection, loc.Directory, shards)
|
|
continue
|
|
}
|
|
if owner.location == loc {
|
|
// .ecx is on this same disk, but loadAllEcShards still
|
|
// did not load these shards — handleFoundEcxFile already
|
|
// logged the underlying failure. Don't try again here.
|
|
continue
|
|
}
|
|
glog.V(0).Infof("ec volume %d (collection=%q): loading orphan shards %v on %s using index files from %s (issue #9212)",
|
|
key.vid, key.collection, shards, loc.Directory, owner.idxDir)
|
|
if err := loc.loadEcShardsWithIdxDir(shards, key.collection, key.vid, owner.idxDir, loc.ecShardNotifyHandler); err != nil {
|
|
glog.Errorf("ec volume %d on %s: cross-disk shard load failed: %v", key.vid, loc.Directory, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// indexEcxOwners returns the disk and the actual directory that owns the
|
|
// .ecx file for each (collection, vid) on this store. .ecx normally lives
|
|
// in IdxDirectory but may have been written into the data directory before
|
|
// -dir.idx was set, so we check both — and we record which one matched so
|
|
// downstream loaders point NewEcVolume at the directory that really has
|
|
// the file. The first owner found wins; duplicates across disks are
|
|
// unusual but tolerated.
|
|
func (s *Store) indexEcxOwners() map[ecKeyForReconcile]ecxOwnerInfo {
|
|
owners := make(map[ecKeyForReconcile]ecxOwnerInfo)
|
|
for _, loc := range s.Locations {
|
|
seen := make(map[string]bool, 2)
|
|
for _, scan := range []string{loc.IdxDirectory, loc.Directory} {
|
|
if scan == "" || seen[scan] {
|
|
continue
|
|
}
|
|
seen[scan] = true
|
|
entries, err := os.ReadDir(scan)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for _, entry := range entries {
|
|
if entry.IsDir() {
|
|
continue
|
|
}
|
|
name := entry.Name()
|
|
if !strings.HasSuffix(name, ".ecx") {
|
|
continue
|
|
}
|
|
base := name[:len(name)-len(".ecx")]
|
|
collection, vid, err := parseCollectionVolumeId(base)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
key := ecKeyForReconcile{collection: collection, vid: vid}
|
|
if _, exists := owners[key]; !exists {
|
|
owners[key] = ecxOwnerInfo{location: loc, idxDir: scan}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return owners
|
|
}
|
|
|
|
// collectOrphanEcShards walks the disk's data directory and returns the
|
|
// .ec?? shard files that are present on disk but not yet registered to an
|
|
// EcVolume in memory. The map is keyed by (collection, vid) so callers can
|
|
// match each group against the .ecx-owning disk in one lookup.
|
|
//
|
|
// Zero-byte shard files are ignored — loadAllEcShards already treats them
|
|
// as cleanup-worthy noise and we want the same shape here.
|
|
func (l *DiskLocation) collectOrphanEcShards() map[ecKeyForReconcile][]string {
|
|
entries, err := os.ReadDir(l.Directory)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
orphans := make(map[ecKeyForReconcile][]string)
|
|
for _, entry := range entries {
|
|
if entry.IsDir() {
|
|
continue
|
|
}
|
|
name := entry.Name()
|
|
ext := path.Ext(name)
|
|
if !re.MatchString(ext) {
|
|
continue
|
|
}
|
|
info, err := entry.Info()
|
|
if err != nil || info.Size() == 0 {
|
|
continue
|
|
}
|
|
shardId, err := strconv.ParseInt(ext[3:], 10, 64)
|
|
if err != nil || shardId < 0 || shardId > 255 {
|
|
continue
|
|
}
|
|
base := name[:len(name)-len(ext)]
|
|
collection, vid, err := parseCollectionVolumeId(base)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if _, loaded := l.FindEcShard(vid, erasure_coding.ShardId(shardId)); loaded {
|
|
continue
|
|
}
|
|
key := ecKeyForReconcile{collection: collection, vid: vid}
|
|
orphans[key] = append(orphans[key], name)
|
|
}
|
|
return orphans
|
|
}
|