mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-13 23:39:33 +01:00
ServerAddress.String() drops a custom gRPC port, so the printed -node would dial the HTTP port plus 10000. Print the full address. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
142 lines
5.0 KiB
Go
142 lines
5.0 KiB
Go
package shell
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"io"
|
|
"log"
|
|
"maps"
|
|
"slices"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/super_block"
|
|
"github.com/seaweedfs/seaweedfs/weed/util/wildcard"
|
|
)
|
|
|
|
func init() {
|
|
Commands = append(Commands, &commandVolumeDeleteEmpty{})
|
|
}
|
|
|
|
type commandVolumeDeleteEmpty struct {
|
|
}
|
|
|
|
func (c *commandVolumeDeleteEmpty) Name() string {
|
|
return "volume.deleteEmpty"
|
|
}
|
|
|
|
func (c *commandVolumeDeleteEmpty) Help() string {
|
|
return `delete empty volumes from all volume servers
|
|
|
|
volume.deleteEmpty -quietFor=24h -apply
|
|
volume.deleteEmpty -collectionPattern=important* -quietFor=24h -apply
|
|
|
|
This command deletes all empty volumes from all volume servers.
|
|
A volume with no live needles left is empty too, even when its
|
|
.dat file is still large: compacting it first would only rewrite
|
|
bytes that are all deleted already.
|
|
A replicated volume is deleted only when all of its copies are
|
|
listed and every copy is empty.
|
|
|
|
`
|
|
}
|
|
|
|
func (c *commandVolumeDeleteEmpty) HasTag(CommandTag) bool {
|
|
return false
|
|
}
|
|
|
|
func (c *commandVolumeDeleteEmpty) Do(args []string, commandEnv *CommandEnv, writer io.Writer) (err error) {
|
|
|
|
volDeleteCommand := flag.NewFlagSet(c.Name(), flag.ContinueOnError)
|
|
quietPeriod := volDeleteCommand.Duration("quietFor", 24*time.Hour, "select empty volumes with no recent writes, avoid newly created ones")
|
|
collectionPattern := volDeleteCommand.String("collectionPattern", "", "comma-separated collection names, with '*' and '?' wildcards; empty matches all")
|
|
applyBalancing := volDeleteCommand.Bool("apply", false, "apply to delete empty volumes")
|
|
// TODO: remove this alias
|
|
applyBalancingAlias := volDeleteCommand.Bool("force", false, "apply to delete empty volumes (alias for -apply)")
|
|
if err = volDeleteCommand.Parse(args); err != nil {
|
|
return nil
|
|
}
|
|
|
|
handleDeprecatedForceFlag(writer, volDeleteCommand, applyBalancingAlias, applyBalancing)
|
|
infoAboutSimulationMode(writer, *applyBalancing, "-apply")
|
|
|
|
if err = commandEnv.confirmIsLocked(args); err != nil {
|
|
return
|
|
}
|
|
|
|
// collect topology information
|
|
topologyInfo, _, err := collectTopologyInfo(commandEnv, 0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
collectionMatcher, err := wildcard.CompileCollectionMatcher(*collectionPattern)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
quietSeconds := int64(*quietPeriod / time.Second)
|
|
nowUnixSeconds := time.Now().Unix()
|
|
|
|
volumeReplicas, _ := collectVolumeReplicaLocations(topologyInfo)
|
|
for _, vid := range slices.Sorted(maps.Keys(volumeReplicas)) {
|
|
replicas := volumeReplicas[vid]
|
|
if !isEmptyVolumeCopiesDeleteCandidate(replicas, quietSeconds, nowUnixSeconds, collectionMatcher) {
|
|
continue
|
|
}
|
|
var kept []pb.ServerAddress
|
|
for _, r := range replicas {
|
|
dn := r.location.dataNode
|
|
if !*applyBalancing {
|
|
log.Printf("empty volume %d from %s", vid, dn.Id)
|
|
continue
|
|
}
|
|
log.Printf("deleting empty volume %d from %s", vid, dn.Id)
|
|
onlyGarbage := r.info.FileCount > 0 && r.info.FileCount <= r.info.DeleteCount
|
|
// onlyEmpty stays set so a pre-upgrade server checks
|
|
// emptiness and refuses instead of deleting unseen.
|
|
server := pb.NewServerAddressFromDataNode(dn)
|
|
if deleteErr := deleteVolume(context.Background(), commandEnv.option.GrpcDialOption, needle.VolumeId(vid),
|
|
server, true, onlyGarbage, false); deleteErr != nil {
|
|
err = deleteErr
|
|
kept = append(kept, server)
|
|
}
|
|
}
|
|
// a rerun skips the survivors as under-replicated
|
|
if len(kept) < len(replicas) {
|
|
for _, server := range kept {
|
|
log.Printf("volume %d stays on %s after its other copies were deleted; once it is empty, remove it with volume.delete -node %s -volumeId %d", vid, server, string(server), vid)
|
|
}
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// isEmptyVolumeCopiesDeleteCandidate requires every copy to be listed and
|
|
// empty: deleting only the empty ones would leave a copy that holds data, or
|
|
// one that is offline, as the volume's only copy.
|
|
func isEmptyVolumeCopiesDeleteCandidate(replicas []*VolumeReplica, quietSeconds, nowUnixSeconds int64, collectionMatcher *wildcard.CollectionMatcher) bool {
|
|
for _, r := range replicas {
|
|
rp, err := super_block.NewReplicaPlacementFromByte(byte(r.info.ReplicaPlacement))
|
|
if err != nil || len(replicas) < rp.GetCopyCount() ||
|
|
!isEmptyVolumeDeleteCandidate(r.info, quietSeconds, nowUnixSeconds, collectionMatcher) {
|
|
return false
|
|
}
|
|
}
|
|
return len(replicas) > 0
|
|
}
|
|
|
|
func isEmptyVolumeDeleteCandidate(v *master_pb.VolumeInformationMessage, quietSeconds, nowUnixSeconds int64, collectionMatcher *wildcard.CollectionMatcher) bool {
|
|
return collectionMatcher.Matches(v.Collection) &&
|
|
// A remote-backed replica shares its cloud object with the others, so
|
|
// deleting one cannot drop it without hurting the survivors.
|
|
v.RemoteStorageName == "" &&
|
|
(!v.ReadOnly || v.ReadOnlyCanDelete) &&
|
|
(v.Size <= super_block.SuperBlockSize || v.FileCount > 0 && v.FileCount <= v.DeleteCount) &&
|
|
v.ModifiedAtSecond > 0 &&
|
|
v.ModifiedAtSecond+quietSeconds < nowUnixSeconds
|
|
}
|