mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-08-19 05:36:58 +00: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.
196 lines
6.4 KiB
Go
196 lines
6.4 KiB
Go
package erasure_coding
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/operation"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb"
|
|
"google.golang.org/grpc"
|
|
)
|
|
|
|
type ServerShardInventory struct {
|
|
Bits ShardBits
|
|
QueryError error
|
|
}
|
|
|
|
// Query errors are recorded per-server and treated as zero shards rather
|
|
// than aborting the scan, so the caller still sees partial coverage from
|
|
// healthy peers when one server is down. The caller gates destructive
|
|
// actions on RequireRecoverableShardSet against the returned union.
|
|
func VerifyShardsAcrossServers(ctx context.Context, volumeID uint32,
|
|
servers []string, dialOption grpc.DialOption) (
|
|
union ShardBits, perServer map[string]ServerShardInventory) {
|
|
|
|
perServer = make(map[string]ServerShardInventory, len(servers))
|
|
|
|
for _, server := range servers {
|
|
if server == "" {
|
|
continue
|
|
}
|
|
if _, seen := perServer[server]; seen {
|
|
continue
|
|
}
|
|
|
|
var inv ServerShardInventory
|
|
|
|
callErr := operation.WithVolumeServerClient(false, pb.ServerAddress(server), dialOption,
|
|
func(client volume_server_pb.VolumeServerClient) error {
|
|
resp, e := client.VolumeEcShardsInfo(ctx, &volume_server_pb.VolumeEcShardsInfoRequest{
|
|
VolumeId: volumeID,
|
|
})
|
|
if e != nil {
|
|
return e
|
|
}
|
|
for _, s := range resp.EcShardInfos {
|
|
if s.VolumeId != volumeID || s.ShardId >= MaxShardCount {
|
|
continue
|
|
}
|
|
inv.Bits = inv.Bits.Set(ShardId(s.ShardId))
|
|
}
|
|
return nil
|
|
})
|
|
if callErr != nil {
|
|
inv.QueryError = callErr
|
|
}
|
|
|
|
perServer[server] = inv
|
|
union = ShardBits(uint32(union) | uint32(inv.Bits))
|
|
}
|
|
|
|
return union, perServer
|
|
}
|
|
|
|
// RequireRecoverableShardSet gates source-volume deletion after EC encode:
|
|
// a non-empty .dat may only be deleted when enough distinct shards exist to
|
|
// reconstruct the volume (>= dataShards). A full set returns (false, nil); a
|
|
// degraded-but-recoverable set returns (true, nil) so the caller can warn and
|
|
// proceed -- the missing shards can be rebuilt from the survivors, while
|
|
// keeping the source next to live shards is the more dangerous mixed state.
|
|
// Below dataShards it returns an error and the source must be kept.
|
|
// dataShards/totalShards are passed as parameters (not derived from the
|
|
// package constants) so enterprise builds with custom EC ratios share this
|
|
// helper verbatim.
|
|
func RequireRecoverableShardSet(volumeID uint32, shardsPresent ShardBits, dataShards, totalShards int) (degraded bool, err error) {
|
|
if totalShards <= 0 || totalShards > MaxShardCount {
|
|
return false, fmt.Errorf("invalid totalShards %d for volume %d (must be in [1, %d])",
|
|
totalShards, volumeID, MaxShardCount)
|
|
}
|
|
if dataShards <= 0 || dataShards > totalShards {
|
|
return false, fmt.Errorf("invalid dataShards %d for volume %d (must be in [1, %d])",
|
|
dataShards, volumeID, totalShards)
|
|
}
|
|
var missing []int
|
|
for id := 0; id < totalShards; id++ {
|
|
if !shardsPresent.Has(ShardId(id)) {
|
|
missing = append(missing, id)
|
|
}
|
|
}
|
|
if len(missing) == 0 {
|
|
return false, nil
|
|
}
|
|
if totalShards-len(missing) >= dataShards {
|
|
return true, nil
|
|
}
|
|
sort.Ints(missing)
|
|
return false, fmt.Errorf("EC shard set unrecoverable for volume %d: %d/%d shards present, need %d to reconstruct, missing shard ids %v",
|
|
volumeID, totalShards-len(missing), totalShards, dataShards, missing)
|
|
}
|
|
|
|
func SummarizeShardInventory(perServer map[string]ServerShardInventory) string {
|
|
servers := make([]string, 0, len(perServer))
|
|
for s := range perServer {
|
|
servers = append(servers, s)
|
|
}
|
|
sort.Strings(servers)
|
|
|
|
var b []byte
|
|
for i, s := range servers {
|
|
if i > 0 {
|
|
b = append(b, ' ')
|
|
}
|
|
inv := perServer[s]
|
|
b = append(b, s...)
|
|
b = append(b, '=')
|
|
b = append(b, '[')
|
|
ids := make([]int, 0)
|
|
for id := 0; id < MaxShardCount; id++ {
|
|
if inv.Bits.Has(ShardId(id)) {
|
|
ids = append(ids, id)
|
|
}
|
|
}
|
|
for j, id := range ids {
|
|
if j > 0 {
|
|
b = append(b, ' ')
|
|
}
|
|
b = append(b, []byte(fmt.Sprintf("%d", id))...)
|
|
}
|
|
if inv.QueryError != nil {
|
|
if len(ids) > 0 {
|
|
b = append(b, ' ')
|
|
}
|
|
b = append(b, []byte("ERR:"+inv.QueryError.Error())...)
|
|
}
|
|
b = append(b, ']')
|
|
}
|
|
return string(b)
|
|
}
|
|
|
|
// VerifyShardsOnServer confirms one server really holds the named shards of a
|
|
// specific (collection, volume), for callers about to delete another copy.
|
|
//
|
|
// Collection is checked, unlike in VerifyShardsAcrossServers: the inventory RPC
|
|
// is keyed by volume id alone, so a server answering for volume N says nothing
|
|
// about which collection's volume N it means. Approving a delete on the
|
|
// strength of a different collection's shard would remove the last real copy —
|
|
// the exact outcome the caller is trying to prevent.
|
|
//
|
|
// A server that cannot be queried is unknown, not confirmed, and returns an
|
|
// error: treating an unreachable peer as proof of a surviving copy is how a
|
|
// network blip becomes data loss.
|
|
// CollectShardsOnServer asks one volume server which shards of the volume it
|
|
// actually serves right now — its live inventory, as opposed to what the
|
|
// master's possibly stale topology claims for it.
|
|
func CollectShardsOnServer(ctx context.Context, collection string, volumeID uint32,
|
|
server string, dialOption grpc.DialOption) (present ShardBits, err error) {
|
|
err = operation.WithVolumeServerClient(false, pb.ServerAddress(server), dialOption,
|
|
func(client volume_server_pb.VolumeServerClient) error {
|
|
resp, e := client.VolumeEcShardsInfo(ctx, &volume_server_pb.VolumeEcShardsInfoRequest{
|
|
VolumeId: volumeID,
|
|
})
|
|
if e != nil {
|
|
return e
|
|
}
|
|
for _, s := range resp.EcShardInfos {
|
|
if s.VolumeId != volumeID || s.Collection != collection || s.ShardId >= MaxShardCount {
|
|
continue
|
|
}
|
|
present = present.Set(ShardId(s.ShardId))
|
|
}
|
|
return nil
|
|
})
|
|
return present, err
|
|
}
|
|
|
|
func VerifyShardsOnServer(ctx context.Context, collection string, volumeID uint32,
|
|
server string, shardIDs []uint32, dialOption grpc.DialOption) error {
|
|
|
|
if server == "" {
|
|
return fmt.Errorf("no server given to verify volume %d shard(s) %v", volumeID, shardIDs)
|
|
}
|
|
|
|
present, callErr := CollectShardsOnServer(ctx, collection, volumeID, server, dialOption)
|
|
if callErr != nil {
|
|
return fmt.Errorf("verify volume %d shard(s) %v on %s: %w", volumeID, shardIDs, server, callErr)
|
|
}
|
|
|
|
for _, sid := range shardIDs {
|
|
if !present.Has(ShardId(sid)) {
|
|
return fmt.Errorf("%s does not hold ec shard %d.%d of collection %q", server, volumeID, sid, collection)
|
|
}
|
|
}
|
|
return nil
|
|
}
|