mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-08-16 20:26:45 +00:00
* ec: confirm a surviving copy before deleting a duplicate EC shard The dedup phase of EC balancing removes a shard it believes exists elsewhere. It copies nothing first, so the shard surviving on another node is the only thing that makes the delete safe -- and it took the plan's word for that. The plan is built from the master's topology, which can name a location that holds nothing: such a server answers "CopyFile not found ec volume id N" when something later tries to read the shard there. A shard listed on a phantom location and on a real one looks duplicated, so dedup deletes one of them. When it picks the real one the last copy is gone, and the job reports success -- the loss only surfaces later, as a rebuild that cannot assemble enough shards. The move phase already refuses to work on trust: it verifies the shard registered on the destination before removing the source. Dedup now holds to the same standard. The planner records which node it chose to keep, and both executors -- the worker task and the shell's ec.balance -- confirm that node really holds the shard before deleting. A keep node that cannot be queried is unknown rather than confirmed, and blocks the delete. Tests drive the destructive path against an in-process volume server that tracks what is actually on disk separately from what the plan claims, which is the distinction the bug turns on. Without the guard, two of them fail by deleting the only copy and returning success. * ec: check the collection and bound the wait when confirming a survivor Two gaps in the dedup survivor check. The inventory RPC is keyed by volume id alone, so a server holding the same number for a different collection answers "yes, I have that shard" to a question about this one. Accepting that deletes the last real copy on the strength of an unrelated volume. The response already carries the collection, so verify against it rather than widening the RPC. The shell path also queried on a background context, so a keep node that accepts the connection but never answers would hang the whole balance run instead of reporting that the survivor could not be confirmed. Bound it. The check moves into VerifyShardsOnServer next to the existing helper, shared by both executors, so the two paths cannot drift.
188 lines
5.9 KiB
Go
188 lines
5.9 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.
|
|
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)
|
|
}
|
|
|
|
var present ShardBits
|
|
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.Collection != collection || s.ShardId >= MaxShardCount {
|
|
continue
|
|
}
|
|
present = present.Set(ShardId(s.ShardId))
|
|
}
|
|
return nil
|
|
})
|
|
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
|
|
}
|