mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-19 04:24:28 +02:00
* ec: let the encode's balance see a migrating volume's shards across disk-type buckets Shard generation writes beside the source .dat, so a cross-tier encode (source on hdd, -diskType=ssd) leaves the fresh shards in the source disk-type bucket. The encode's internal balance ingested only the target bucket, saw no shards, and planned no moves; the spread guard then correctly aborted the encode (and before that guard existed, the shards silently stayed clumped on the generation host in the wrong tier). EcBalance now takes the encode batch as migratingVolumeIds and ingests those volumes' shards from every bucket, while everything else keeps the bucket filter so a plain ec.balance never drags deliberately tiered shards onto another disk type. The in-memory model delete also becomes bucket-agnostic: a node holds a given shard in exactly one bucket, and a bucket-scoped delete missed cross-bucket moves in the dry-run model. * volume: decode reads shard 0 from its resolved path, not the EC volume's base dir On a multi-disk server a volume's shards can sit on several disks; the store registers each shard with its own path and CollectEcShards resolves them, but FindDatFileSize derived the .ec00 path from the EcVolume's base directory. When shard 0 lived on a sibling disk, VolumeEcShardsToVolume failed with 'open ...ec00: no such file or directory' and ec.decode aborted. * ec: decode re-copies shards the topology claims but the target does not hold An interrupted earlier decode or balance can leave the master believing the decode target holds a shard whose file never landed: the mount registered but the partial copy was cleaned, or the file was swept. The collect step took the topology's word for it, excluded the shard from the copy set, and the decode failed with 'missing shard'. Probe the target's live inventory (VolumeEcShardsInfo) and treat anything it cannot serve as still-to-copy. * ec: decode discovers shards across disk-type buckets Shards sit wherever encode generation and balance left them: a cross-tier encode leaves them in the source disk-type bucket, a partial migration straddles buckets. ec.decode scoped its shard discovery to the -diskType bucket and reported a decodable volume as having no shards at all. Union across buckets, the way the encode's shard verification already does. * test: EC chaos lifecycle harness Randomized, seeded sequences of the EC lifecycle against a live cluster in the production-shaped layout: multiple data disks per server, a separate -dir.idx directory so .ecx/.ecj sidecars are shared across disks, and a tagged ssd tier. Operations cover encode (hdd and ssd targets), balance, shard damage plus rebuild, decode, re-encode, deletes, scrub, tier moves, crash-restarts, sidecar fault injections (a data-dir .vif pushed into the shared idx dir; a stale-generation shard planted beside a newer encode), and interruptions: a real weed shell subprocess killed mid-encode, mid-decode, and mid-balance, with the recovery re-run required to converge. One invariant holds after every step: every stored byte reads back identical and every deleted needle stays deleted. EC_CHAOS_SEED and EC_CHAOS_STEPS make runs reproducible and scalable. A known gap is tolerated and logged rather than fixed here: a shard mounted on two disks of one node (orphan adoption after an interrupted copy) is invisible to ec.balance's dedup and unaddressable by ec.shard.unmount's shard@address form, so no cleanup path exists yet. * test: fail payload-corruption checks on the test goroutine t.Fatalf inside require.Eventually's condition runs on the poller's goroutine, where Goexit kills only that goroutine and the corruption message can be lost behind a generic timeout. Record the mismatch, end the polling, and fail on the test goroutine. Also assert the full shard count in the cross-bucket decode-discovery test.
399 lines
15 KiB
Go
399 lines
15 KiB
Go
package ec
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/operation"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/types"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
)
|
|
|
|
func DoEcDecode(env *Env, topoInfo *master_pb.TopologyInfo, collection string, vid needle.VolumeId, diskType types.DiskType, checkMinFreeSpace bool, diskUsageState *DecodeDiskUsageState) (err error) {
|
|
|
|
if !env.isLocked() {
|
|
return fmt.Errorf("lock is lost")
|
|
}
|
|
|
|
// find volume location
|
|
nodeToEcShardsInfo, dataShards := collectEcNodeShardsInfo(topoInfo, vid)
|
|
|
|
fmt.Printf("ec volume %d shard locations: %+v\n", vid, nodeToEcShardsInfo)
|
|
|
|
if len(nodeToEcShardsInfo) == 0 {
|
|
return fmt.Errorf("no EC shards found for volume %d", vid)
|
|
}
|
|
|
|
var originalShardCounts map[pb.ServerAddress]int
|
|
if diskUsageState != nil {
|
|
originalShardCounts = make(map[pb.ServerAddress]int, len(nodeToEcShardsInfo))
|
|
for location, si := range nodeToEcShardsInfo {
|
|
originalShardCounts[location] = si.Count()
|
|
}
|
|
}
|
|
|
|
var eligibleTargets map[pb.ServerAddress]struct{}
|
|
if checkMinFreeSpace {
|
|
if diskUsageState == nil {
|
|
return fmt.Errorf("min free space checking requires disk usage state")
|
|
}
|
|
eligibleTargets = make(map[pb.ServerAddress]struct{})
|
|
for location := range nodeToEcShardsInfo {
|
|
if freeCount, found := diskUsageState.freeVolumeCount(location); found && freeCount > 0 {
|
|
eligibleTargets[location] = struct{}{}
|
|
}
|
|
}
|
|
if len(eligibleTargets) == 0 {
|
|
return fmt.Errorf("no eligible target datanodes with free volume slots for volume %d (diskType %s); use -checkMinFreeSpace=false to override", vid, diskType.ReadableString())
|
|
}
|
|
}
|
|
|
|
// collect ec shards to the server with most space
|
|
targetNodeLocation, err := collectEcShards(env, nodeToEcShardsInfo, collection, vid, eligibleTargets, dataShards)
|
|
if err != nil {
|
|
return fmt.Errorf("collectEcShards for volume %d: %v", vid, err)
|
|
}
|
|
|
|
// generate a normal volume
|
|
err = generateNormalVolume(env.GrpcDialOption, vid, collection, targetNodeLocation)
|
|
if err != nil {
|
|
// Special case: if the EC index has no live entries, decoding is a no-op.
|
|
// Just purge EC shards and return success without generating/mounting an empty volume.
|
|
if isEcDecodeEmptyVolumeErr(err) {
|
|
if err := unmountAndDeleteEcShards(env.GrpcDialOption, collection, nodeToEcShardsInfo, vid); err != nil {
|
|
return err
|
|
}
|
|
if diskUsageState != nil {
|
|
diskUsageState.applyDecode(targetNodeLocation, originalShardCounts, false)
|
|
}
|
|
return nil
|
|
}
|
|
return fmt.Errorf("generate normal volume %d on %s: %v", vid, targetNodeLocation, err)
|
|
}
|
|
|
|
// mount the decoded volume after server-side offline compaction succeeded
|
|
err = mountDecodedVolume(env.GrpcDialOption, targetNodeLocation, vid)
|
|
if err != nil {
|
|
return fmt.Errorf("mount decoded volume %d on %s: %v", vid, targetNodeLocation, err)
|
|
}
|
|
|
|
// Confirm the regenerated .dat is present and non-empty before destroying
|
|
// the shards. Without this gate, a silent failure in generate/mount could
|
|
// leave the cluster with neither shards nor volume.
|
|
if err := verifyDecodedVolumeBeforeDelete(env.GrpcDialOption, targetNodeLocation, vid); err != nil {
|
|
return fmt.Errorf("verify decoded volume %d on %s before deleting shards: %w", vid, targetNodeLocation, err)
|
|
}
|
|
|
|
// delete the previous ec shards
|
|
err = unmountAndDeleteEcShardsWithPrefix("deleteDecodedEcShards", env.GrpcDialOption, collection, nodeToEcShardsInfo, vid)
|
|
if err != nil {
|
|
return fmt.Errorf("delete ec shards for volume %d: %v", vid, err)
|
|
}
|
|
if diskUsageState != nil {
|
|
diskUsageState.applyDecode(targetNodeLocation, originalShardCounts, true)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func isEcDecodeEmptyVolumeErr(err error) bool {
|
|
st, ok := status.FromError(err)
|
|
if !ok {
|
|
return false
|
|
}
|
|
if st.Code() != codes.FailedPrecondition {
|
|
return false
|
|
}
|
|
// Keep this robust against wording tweaks while still being specific.
|
|
return strings.Contains(st.Message(), erasure_coding.EcNoLiveEntriesSubstring)
|
|
}
|
|
|
|
func unmountAndDeleteEcShards(grpcDialOption grpc.DialOption, collection string, nodeToShardsInfo map[pb.ServerAddress]*erasure_coding.ShardsInfo, vid needle.VolumeId) error {
|
|
return unmountAndDeleteEcShardsWithPrefix("unmountAndDeleteEcShards", grpcDialOption, collection, nodeToShardsInfo, vid)
|
|
}
|
|
|
|
func unmountAndDeleteEcShardsWithPrefix(prefix string, grpcDialOption grpc.DialOption, collection string, nodeToShardsInfo map[pb.ServerAddress]*erasure_coding.ShardsInfo, vid needle.VolumeId) error {
|
|
ewg := util.NewErrorWaitGroup(len(nodeToShardsInfo))
|
|
|
|
// unmount and delete ec shards in parallel (one goroutine per location)
|
|
for location, si := range nodeToShardsInfo {
|
|
location, si := location, si // capture loop variables for goroutine
|
|
ewg.Add(func() error {
|
|
fmt.Printf("unmount ec volume %d on %s has shards: %+v\n", vid, location, si.Ids())
|
|
if err := UnmountEcShards(grpcDialOption, vid, location, si.Ids()); err != nil {
|
|
return fmt.Errorf("%s unmount ec volume %d on %s: %w", prefix, vid, location, err)
|
|
}
|
|
|
|
fmt.Printf("delete ec volume %d on %s has shards: %+v\n", vid, location, si.Ids())
|
|
if err := SourceServerDeleteEcShards(grpcDialOption, collection, vid, location, si.Ids()); err != nil {
|
|
return fmt.Errorf("%s delete ec volume %d on %s: %w", prefix, vid, location, err)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
return ewg.Wait()
|
|
}
|
|
|
|
func verifyDecodedVolumeBeforeDelete(grpcDialOption grpc.DialOption, target pb.ServerAddress, vid needle.VolumeId) error {
|
|
var resp *volume_server_pb.ReadVolumeFileStatusResponse
|
|
if err := operation.WithVolumeServerClient(false, target, grpcDialOption, func(client volume_server_pb.VolumeServerClient) error {
|
|
r, e := client.ReadVolumeFileStatus(context.Background(), &volume_server_pb.ReadVolumeFileStatusRequest{
|
|
VolumeId: uint32(vid),
|
|
})
|
|
if e != nil {
|
|
return e
|
|
}
|
|
resp = r
|
|
return nil
|
|
}); err != nil {
|
|
return fmt.Errorf("read volume file status: %w", err)
|
|
}
|
|
if resp.DatFileSize == 0 {
|
|
return fmt.Errorf("decoded .dat is 0 bytes")
|
|
}
|
|
if resp.IdxFileSize == 0 {
|
|
return fmt.Errorf("decoded .idx is 0 bytes")
|
|
}
|
|
glog.V(0).Infof("ec decode verification ok for volume %d on %s: dat=%d idx=%d", vid, target, resp.DatFileSize, resp.IdxFileSize)
|
|
return nil
|
|
}
|
|
|
|
func mountDecodedVolume(grpcDialOption grpc.DialOption, targetNodeLocation pb.ServerAddress, vid needle.VolumeId) error {
|
|
return operation.WithVolumeServerClient(false, targetNodeLocation, grpcDialOption, func(volumeServerClient volume_server_pb.VolumeServerClient) error {
|
|
_, mountErr := volumeServerClient.VolumeMount(context.Background(), &volume_server_pb.VolumeMountRequest{
|
|
VolumeId: uint32(vid),
|
|
})
|
|
return mountErr
|
|
})
|
|
}
|
|
|
|
func generateNormalVolume(grpcDialOption grpc.DialOption, vid needle.VolumeId, collection string, sourceVolumeServer pb.ServerAddress) error {
|
|
fmt.Printf("generateNormalVolume from ec volume %d on %s\n", vid, sourceVolumeServer)
|
|
|
|
err := operation.WithVolumeServerClient(false, sourceVolumeServer, grpcDialOption, func(volumeServerClient volume_server_pb.VolumeServerClient) error {
|
|
_, genErr := volumeServerClient.VolumeEcShardsToVolume(context.Background(), &volume_server_pb.VolumeEcShardsToVolumeRequest{
|
|
VolumeId: uint32(vid),
|
|
Collection: collection,
|
|
})
|
|
return genErr
|
|
})
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
func collectEcShards(env *Env, nodeToShardsInfo map[pb.ServerAddress]*erasure_coding.ShardsInfo, collection string, vid needle.VolumeId, eligibleTargets map[pb.ServerAddress]struct{}, dataShards int) (targetNodeLocation pb.ServerAddress, err error) {
|
|
|
|
maxShardCount := -1
|
|
existingShardsInfo := erasure_coding.NewShardsInfo()
|
|
for loc, si := range nodeToShardsInfo {
|
|
if eligibleTargets != nil {
|
|
if _, ok := eligibleTargets[loc]; !ok {
|
|
continue
|
|
}
|
|
}
|
|
toBeCopiedShardCount := si.MinusParityShards(dataShards).Count()
|
|
if toBeCopiedShardCount > maxShardCount {
|
|
maxShardCount = toBeCopiedShardCount
|
|
targetNodeLocation = loc
|
|
existingShardsInfo = si
|
|
}
|
|
}
|
|
if targetNodeLocation == "" {
|
|
return "", fmt.Errorf("no eligible target datanodes available to decode volume %d", vid)
|
|
}
|
|
|
|
// Trust only shards the target can actually serve: an interrupted earlier
|
|
// decode or balance can leave the master believing the target holds a
|
|
// shard whose file never landed. A phantom entry here would exclude the
|
|
// shard from the copy set and the decode would then fail with "missing
|
|
// shard"; probing the target's live inventory makes the re-run re-copy it.
|
|
if present, probeErr := erasure_coding.CollectShardsOnServer(context.Background(), collection, uint32(vid), string(targetNodeLocation), env.GrpcDialOption); probeErr == nil {
|
|
confirmed := erasure_coding.NewShardsInfo()
|
|
for _, sid := range existingShardsInfo.Ids() {
|
|
if present.Has(sid) {
|
|
confirmed.Set(erasure_coding.NewShardInfo(sid, 0))
|
|
}
|
|
}
|
|
existingShardsInfo = confirmed
|
|
} else {
|
|
fmt.Printf("collectEcShards: probe %s inventory for volume %d: %v (keeping the topology's view)\n", targetNodeLocation, vid, probeErr)
|
|
}
|
|
|
|
fmt.Printf("collectEcShards: ec volume %d collect shards to %s from: %+v\n", vid, targetNodeLocation, nodeToShardsInfo)
|
|
|
|
copiedShardsInfo := erasure_coding.NewShardsInfo()
|
|
for loc, si := range nodeToShardsInfo {
|
|
if loc == targetNodeLocation {
|
|
continue
|
|
}
|
|
|
|
needToCopyShardsInfo := si.Minus(existingShardsInfo).MinusParityShards(dataShards)
|
|
|
|
err = operation.WithVolumeServerClient(false, targetNodeLocation, env.GrpcDialOption, func(volumeServerClient volume_server_pb.VolumeServerClient) error {
|
|
|
|
// Always collect .ecj from every shard location. Each server's .ecj
|
|
// only contains deletions for needles whose data resides in shards
|
|
// held by that server. Without merging all .ecj files, deletions
|
|
// recorded on other servers would be lost during decode.
|
|
if needToCopyShardsInfo.Count() > 0 {
|
|
fmt.Printf("copy %d.%v %s => %s\n", vid, needToCopyShardsInfo.Ids(), loc, targetNodeLocation)
|
|
} else {
|
|
fmt.Printf("collect ecj %d %s => %s\n", vid, loc, targetNodeLocation)
|
|
}
|
|
|
|
_, copyErr := volumeServerClient.VolumeEcShardsCopy(context.Background(), &volume_server_pb.VolumeEcShardsCopyRequest{
|
|
VolumeId: uint32(vid),
|
|
Collection: collection,
|
|
ShardIds: needToCopyShardsInfo.IdsUint32(),
|
|
CopyEcxFile: false,
|
|
CopyEcjFile: true,
|
|
CopyVifFile: needToCopyShardsInfo.Count() > 0,
|
|
SourceDataNode: string(loc),
|
|
})
|
|
if copyErr != nil {
|
|
return fmt.Errorf("copy %d.%v %s => %s: %v", vid, needToCopyShardsInfo.Ids(), loc, targetNodeLocation, copyErr)
|
|
}
|
|
|
|
if needToCopyShardsInfo.Count() > 0 {
|
|
fmt.Printf("mount %d.%v on %s\n", vid, needToCopyShardsInfo.Ids(), targetNodeLocation)
|
|
_, mountErr := volumeServerClient.VolumeEcShardsMount(context.Background(), &volume_server_pb.VolumeEcShardsMountRequest{
|
|
VolumeId: uint32(vid),
|
|
Collection: collection,
|
|
ShardIds: needToCopyShardsInfo.IdsUint32(),
|
|
})
|
|
if mountErr != nil {
|
|
return fmt.Errorf("mount %d.%v on %s: %v", vid, needToCopyShardsInfo.Ids(), targetNodeLocation, mountErr)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
break
|
|
}
|
|
|
|
copiedShardsInfo.Add(needToCopyShardsInfo)
|
|
}
|
|
|
|
nodeToShardsInfo[targetNodeLocation] = existingShardsInfo.Plus(copiedShardsInfo)
|
|
|
|
return targetNodeLocation, err
|
|
}
|
|
func CollectEcShardIds(topoInfo *master_pb.TopologyInfo, collectionRegex *regexp.Regexp, diskType types.DiskType) (vids []needle.VolumeId) {
|
|
vidMap := make(map[uint32]bool)
|
|
EachDataNode(topoInfo, func(dc DataCenterId, rack RackId, dn *master_pb.DataNodeInfo) {
|
|
if diskInfo, found := dn.DiskInfos[string(diskType)]; found {
|
|
for _, v := range diskInfo.EcShardInfos {
|
|
if collectionRegex.MatchString(v.Collection) {
|
|
vidMap[v.Id] = true
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
for vid := range vidMap {
|
|
vids = append(vids, needle.VolumeId(vid))
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func collectEcNodeShardsInfo(topoInfo *master_pb.TopologyInfo, vid needle.VolumeId) (map[pb.ServerAddress]*erasure_coding.ShardsInfo, int) {
|
|
res := make(map[pb.ServerAddress]*erasure_coding.ShardsInfo)
|
|
EachDataNode(topoInfo, func(dc DataCenterId, rack RackId, dn *master_pb.DataNodeInfo) {
|
|
// Union across ALL disk-type buckets and, within a node, across its
|
|
// physical disks. Shards sit wherever encode generation and balance
|
|
// left them — a cross-tier encode leaves them in the source bucket, a
|
|
// partial migration straddles buckets — and a decode that only looks
|
|
// at one bucket reports a decodable volume as having no shards at all.
|
|
// (Same rationale as the encode's shard verification.)
|
|
for _, diskInfo := range dn.DiskInfos {
|
|
if diskInfo == nil {
|
|
continue
|
|
}
|
|
for _, v := range diskInfo.EcShardInfos {
|
|
if v.Id == uint32(vid) {
|
|
addr := pb.NewServerAddressFromDataNode(dn)
|
|
si := erasure_coding.ShardsInfoFromVolumeEcShardInformationMessage(v)
|
|
if existing, ok := res[addr]; ok {
|
|
existing.Add(si)
|
|
} else {
|
|
res[addr] = si
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
// OSS is always 10+4; the per-volume ratio override lives in the enterprise build.
|
|
return res, erasure_coding.DataShardsCount
|
|
}
|
|
|
|
type DecodeDiskUsageState struct {
|
|
byNode map[pb.ServerAddress]*decodeDiskUsageCounts
|
|
}
|
|
|
|
type decodeDiskUsageCounts struct {
|
|
maxVolumeCount int64
|
|
volumeCount int64
|
|
remoteVolumeCount int64
|
|
ecShardCount int64
|
|
}
|
|
|
|
func NewDecodeDiskUsageState(topoInfo *master_pb.TopologyInfo, diskType types.DiskType) *DecodeDiskUsageState {
|
|
state := &DecodeDiskUsageState{byNode: make(map[pb.ServerAddress]*decodeDiskUsageCounts)}
|
|
EachDataNode(topoInfo, func(dc DataCenterId, rack RackId, dn *master_pb.DataNodeInfo) {
|
|
if diskInfo, found := dn.DiskInfos[string(diskType)]; found {
|
|
state.byNode[pb.NewServerAddressFromDataNode(dn)] = &decodeDiskUsageCounts{
|
|
maxVolumeCount: diskInfo.MaxVolumeCount,
|
|
volumeCount: diskInfo.VolumeCount,
|
|
remoteVolumeCount: diskInfo.RemoteVolumeCount,
|
|
ecShardCount: int64(CountShards(diskInfo.EcShardInfos)),
|
|
}
|
|
}
|
|
})
|
|
return state
|
|
}
|
|
|
|
func (state *DecodeDiskUsageState) freeVolumeCount(location pb.ServerAddress) (int64, bool) {
|
|
if state == nil {
|
|
return 0, false
|
|
}
|
|
usage, found := state.byNode[location]
|
|
if !found {
|
|
return 0, false
|
|
}
|
|
free := usage.maxVolumeCount - (usage.volumeCount - usage.remoteVolumeCount)
|
|
free -= (usage.ecShardCount + int64(erasure_coding.DataShardsCount) - 1) / int64(erasure_coding.DataShardsCount)
|
|
return free, true
|
|
}
|
|
|
|
func (state *DecodeDiskUsageState) applyDecode(targetNodeLocation pb.ServerAddress, shardCounts map[pb.ServerAddress]int, createdVolume bool) {
|
|
if state == nil {
|
|
return
|
|
}
|
|
for location, shardCount := range shardCounts {
|
|
if usage, found := state.byNode[location]; found {
|
|
usage.ecShardCount -= int64(shardCount)
|
|
}
|
|
}
|
|
if createdVolume {
|
|
if usage, found := state.byNode[targetNodeLocation]; found {
|
|
usage.volumeCount++
|
|
}
|
|
}
|
|
}
|