mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-08 15:41:15 +02:00
* Introduce a new `READS` scrub mode. `READS` performs a full volume scrub but, unlike `FULL`, it will attempt to reconstruct data for missing/damaged shard intervals from other shards in the cluster when necessary. The goal of this check is to ensure that EC volume contents _are readable by Seaweed_ even on a degraded storage state, by exercising parity data which is not read in `FULL` mode. This is useful not only to validate data is user-readable, but also to detect potential parity shard issues which may be difficult to pinpoint otherwise - particularly for older volumes lacking sidecar data, and hence unaffected by `CHECKSUM` scrubs. For regular volumes, this operation is equivalent to `FULL`. Example: ``` > ec.shard.unmount --volumeId=1 --shardId=0,3,11 --delete --apply Live shard topology for volume ID 1 (14 shards): 0@10.200.18.89:9001 1@10.200.18.89:9002 2@10.200.18.89:9003 3@10.200.18.89:9004 4@10.200.18.89:9005 5@10.200.18.89:9006 6@10.200.18.89:9007 7@10.200.18.89:9008 8@10.200.18.89:9009 9@10.200.18.89:9013 10@10.200.18.89:9010 11@10.200.18.89:9011 12@10.200.18.89:9012 13@10.200.18.89:9020 Will unmount + delete 3 shard(s): 0@10.200.18.89:9001 3@10.200.18.89:9004 11@10.200.18.89:9011 Unmounting shard 0@10.200.18.89:9001 for volume ID 1... Deleting shard 0@10.200.18.89:9001 for volume ID 1... Unmounting shard 3@10.200.18.89:9004 for volume ID 1... Deleting shard 3@10.200.18.89:9004 for volume ID 1... Unmounting shard 11@10.200.18.89:9011 for volume ID 1... Deleting shard 11@10.200.18.89:9011 for volume ID 1... All done! > ec.scrub --volumeId=1 --node=10.200.18.89:9002 --mode=full using FULL mode Scrubbing 10.200.18.89:9002 (1/1)... Scrubbed 6 EC files and 1 volumes on 1 nodes Got scrub failures on 1 EC volumes and 1 EC shards :( Affected volumes: 10.200.18.89:9002:1 Affected shards: 10.200.18.89:9002:1:0 > ec.scrub --volumeId=1 --node=10.200.18.89:9002 --mode=reads using READS mode Scrubbing 10.200.18.89:9002 (1/1)... Scrubbed 6 EC files and 1 volumes on 1 nodes ``` * ec: report the shards a READS scrub had to rebuild A READS scrub that recovers an interval was recording nothing, so a volume missing three shards came back clean and nobody repaired it. The unreadable shard is now recorded before the rebuild is attempted: READS reports the same broken shards as FULL and differs only in whether the needles themselves failed, which is the signal worth having - shards are gone, data is still there. forceDeletedNeedlesCheck now applies to READS as well, in the shell and in the RPC guard: it runs the same needle walk as FULL. Regenerated the proto instead of hand-editing it, so the pancis typo (which protoc-gen-go-grpc emits into eight other files here) and the header whitespace stay as generated. Mirrors into the Rust volume server, which also now honors force_deleted_needles_check rather than hardcoding it off. Claude-Session: https://claude.ai/code/session_014yMNebkUjSbx9sfUCWJJtq * ec: answer a deleted needle from a READS rebuild as deleted #11020 gave the Rust recovery a deleted flag alongside its bytes, and it answers a deleted needle with no bytes at all. The READS scrub appended that empty answer, which does not compile against the new signature and, once it did, would leave the needle short and report the size mismatch as damage. Zero-fill the interval instead, the way the direct read beside it already does: the assembled needle then reaches read_bytes as the delete-state mismatch the walk already tolerates. Go takes the same branch off the flag its recovery returns, rather than discarding it. Claude-Session: https://claude.ai/code/session_014yMNebkUjSbx9sfUCWJJtq --------- Co-authored-by: Lisandro Pin <lisandro.pin@proton.ch>
172 lines
5.5 KiB
Go
172 lines
5.5 KiB
Go
package weed_server
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/stats"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
|
|
)
|
|
|
|
func (vs *VolumeServer) ScrubVolume(ctx context.Context, req *volume_server_pb.ScrubVolumeRequest) (*volume_server_pb.ScrubVolumeResponse, error) {
|
|
if err := vs.checkGrpcAdminAuth(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
vids := []needle.VolumeId{}
|
|
if len(req.GetVolumeIds()) == 0 {
|
|
for _, l := range vs.store.Locations {
|
|
vids = append(vids, l.VolumeIds()...)
|
|
}
|
|
} else {
|
|
for _, vid := range req.GetVolumeIds() {
|
|
vids = append(vids, needle.VolumeId(vid))
|
|
}
|
|
}
|
|
|
|
var details []string
|
|
var totalVolumes, totalFiles uint64
|
|
var brokenVolumes []*storage.Volume
|
|
var brokenVolumeIds []uint32
|
|
for _, vid := range vids {
|
|
v := vs.store.GetVolume(vid)
|
|
if v == nil {
|
|
return nil, fmt.Errorf("volume id %d not found", vid)
|
|
}
|
|
|
|
var files int64
|
|
var serrs []error
|
|
switch m := req.GetMode(); m {
|
|
case volume_server_pb.VolumeScrubMode_INDEX:
|
|
files, serrs = v.ScrubIndex()
|
|
case volume_server_pb.VolumeScrubMode_LOCAL, volume_server_pb.VolumeScrubMode_READS:
|
|
// both are equivalent to FULL for regular volumes: there are no shards to
|
|
// stay local to, and nothing to reconstruct from
|
|
fallthrough
|
|
case volume_server_pb.VolumeScrubMode_FULL:
|
|
files, serrs = v.Scrub()
|
|
default:
|
|
return nil, fmt.Errorf("unsupported volume scrub mode %d", m)
|
|
}
|
|
|
|
totalVolumes += 1
|
|
totalFiles += uint64(files)
|
|
if len(serrs) != 0 {
|
|
brokenVolumes = append(brokenVolumes, v)
|
|
brokenVolumeIds = append(brokenVolumeIds, uint32(v.Id))
|
|
for _, err := range serrs {
|
|
details = append(details, err.Error())
|
|
}
|
|
}
|
|
}
|
|
|
|
errs := []error{}
|
|
if req.GetMarkBrokenVolumesReadonly() {
|
|
for _, v := range brokenVolumes {
|
|
if err := vs.makeVolumeReadonly(ctx, v, false, true); err != nil {
|
|
errs = append(errs, err)
|
|
details = append(details, err.Error())
|
|
} else {
|
|
details = append(details, fmt.Sprintf("volume %d is now read-only", v.Id))
|
|
}
|
|
}
|
|
}
|
|
|
|
scrubLabels := prometheus.Labels{"mode": req.GetMode().String()}
|
|
stats.VolumeServerScrubLastTimeSeconds.With(scrubLabels).Set(float64(time.Now().Unix()))
|
|
stats.VolumeServerScrubVolumeFailures.With(scrubLabels).Add(float64(len(brokenVolumes)))
|
|
|
|
if len(errs) != 0 {
|
|
return nil, errors.Join(errs...)
|
|
}
|
|
|
|
res := &volume_server_pb.ScrubVolumeResponse{
|
|
TotalVolumes: totalVolumes,
|
|
TotalFiles: totalFiles,
|
|
BrokenVolumeIds: brokenVolumeIds,
|
|
Details: details,
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
func (vs *VolumeServer) ScrubEcVolume(ctx context.Context, req *volume_server_pb.ScrubEcVolumeRequest) (*volume_server_pb.ScrubEcVolumeResponse, error) {
|
|
if err := vs.checkGrpcAdminAuth(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
if m := req.GetMode(); req.GetForceDeletedNeedlesCheck() &&
|
|
m != volume_server_pb.VolumeScrubMode_FULL && m != volume_server_pb.VolumeScrubMode_READS {
|
|
return nil, fmt.Errorf("deleted needle checks are only supported for FULL and READS scrubs")
|
|
}
|
|
|
|
vids := []needle.VolumeId{}
|
|
if len(req.GetVolumeIds()) == 0 {
|
|
for _, l := range vs.store.Locations {
|
|
vids = append(vids, l.EcVolumeIds()...)
|
|
}
|
|
} else {
|
|
for _, vid := range req.GetVolumeIds() {
|
|
vids = append(vids, needle.VolumeId(vid))
|
|
}
|
|
}
|
|
|
|
var details []string
|
|
var totalVolumes, totalFiles uint64
|
|
var brokenVolumeIds []uint32
|
|
var brokenShardInfos []*volume_server_pb.EcShardInfo
|
|
for _, vid := range vids {
|
|
v, found := vs.store.FindEcVolume(vid)
|
|
if !found {
|
|
return nil, fmt.Errorf("EC volume id %d not found", vid)
|
|
}
|
|
|
|
var files int64
|
|
var shardInfos []*volume_server_pb.EcShardInfo
|
|
var serrs []error
|
|
switch m := req.GetMode(); m {
|
|
case volume_server_pb.VolumeScrubMode_INDEX:
|
|
// index scrubs do not verify individual EC shards
|
|
files, serrs = v.ScrubIndex()
|
|
case volume_server_pb.VolumeScrubMode_LOCAL:
|
|
files, shardInfos, serrs = v.ScrubLocal()
|
|
case volume_server_pb.VolumeScrubMode_FULL, volume_server_pb.VolumeScrubMode_READS:
|
|
files, shardInfos, serrs = vs.store.ScrubEcVolume(v.VolumeId, m, req.GetForceDeletedNeedlesCheck())
|
|
case volume_server_pb.VolumeScrubMode_CHECKSUM:
|
|
// Verify each local shard's raw bytes against the bitrot sidecar,
|
|
// exercising cold parity shards. Read-only. ChecksumScrub's first
|
|
// return is blocks scanned, not files — discard it so TotalFiles
|
|
// (a needle/file count) isn't inflated by the block count.
|
|
_, shardInfos, serrs = v.ChecksumScrub()
|
|
default:
|
|
return nil, fmt.Errorf("unsupported EC volume scrub mode %d", m)
|
|
}
|
|
|
|
totalVolumes += 1
|
|
totalFiles += uint64(files)
|
|
if len(serrs) != 0 || len(shardInfos) != 0 {
|
|
brokenVolumeIds = append(brokenVolumeIds, uint32(v.VolumeId))
|
|
brokenShardInfos = append(brokenShardInfos, shardInfos...)
|
|
for _, err := range serrs {
|
|
details = append(details, err.Error())
|
|
}
|
|
}
|
|
}
|
|
|
|
scrubLabels := prometheus.Labels{"mode": req.GetMode().String()}
|
|
stats.VolumeServerScrubLastTimeSeconds.With(scrubLabels).Set(float64(time.Now().Unix()))
|
|
stats.VolumeServerScrubVolumeFailures.With(scrubLabels).Add(float64(len(brokenVolumeIds)))
|
|
stats.VolumeServerScrubShardFailures.With(scrubLabels).Add(float64(len(brokenShardInfos)))
|
|
|
|
res := &volume_server_pb.ScrubEcVolumeResponse{
|
|
TotalVolumes: totalVolumes,
|
|
TotalFiles: totalFiles,
|
|
BrokenVolumeIds: brokenVolumeIds,
|
|
BrokenShardInfos: brokenShardInfos,
|
|
Details: details,
|
|
}
|
|
return res, nil
|
|
}
|