mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-24 15:00:43 +02:00
* master: cap parallel vacuums per rack via -maxParallelVacuumPerRack Hosts that run one volume server per disk under a single rack label could not bound host-level vacuum I/O: maxParallelVacuumPerServer limits each process, so every disk on the machine compacted at once. A second quota map keyed on rack id debits alongside the per-server one, so at most N volume copies compact in parallel within a rack. Defaults to 0 (no limit); the flag is wired on master, server, mini and master.follower. * master: resolve a data node's rack key once per vacuum sweep A volume server that unregisters mid-sweep loses its parent node, so the quota credit path calling dn.GetRack() panicked and crashed the master. Rack names are also local to their data center, so same-named racks in different centers shared one quota bucket. Each node's rack key is now captured when the limiters are built and carries the data center id, so checks, debits and credits never touch the live parent pointer. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * master: reserve vacuum quota per needed copy count, not per copy Each copy of a volume checked the shared server/rack quota independently, so several same-rack replicas all passed against one remaining slot and the debits drove the bucket negative. Needs are now counted per server and per rack first; a volume whose own copies exceed a cap reserves the whole bucket once it is free instead of pending forever. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * master.follower: register -maxParallelVacuumPerRack as a flag It was only a fixed default, so passing the flag on the command line failed flag parsing. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * master: key the rack vacuum quota by rack node identity Joining data center and rack names with "/" still let two different racks share one bucket when the names themselves contained slashes. Key the limiter by the rack node instead, which is unique per rack regardless of naming. * master: keep the tiered-replica vacuum test through the rebase * master: document 0 as no limit for -maxParallelVacuumPerServer The clamped per-server need makes a non-positive cap mean "no limit", matching the new rack flag; say so in the flag help. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * master: key the rack-key cache by data node identity Node ids are only unique within a rack, so two servers that share an id in different racks would have reused the first server's cached rack and escaped the per-rack vacuum cap. * master: read the per-rack vacuum cap from master.toml -maxParallelVacuumPerRack becomes the master.max_parallel_vacuum_per_rack config key, so the new master limit is set the same way as other master options instead of a flag wired into every command that embeds a master. * master: let master.max_parallel_vacuum_per_server override the flag The released -maxParallelVacuumPerServer flag keeps working, but a value in master.toml now wins when set, matching how master.mdir already overrides -mdir. * master: group the vacuum caps under master.vacuum in master.toml Rename master.max_parallel_vacuum_per_rack to master.vacuum.max_parallel_per_rack and master.max_parallel_vacuum_per_server to master.vacuum.max_parallel_per_server, so the keys sit in one subsystem table like master.volume_growth and master.maintenance. * master: deprecate -maxParallelVacuumPerServer for master.toml The flag still works - and is overridden by master.vacuum.max_parallel_per_server when set - but the help text now points operators at the config key, matching the codebase style of help-text-only deprecation. * topology: read node parent atomically UnlinkChildNode clears a data node's parent under the rack's lock while readers like the vacuum sweep's rack capture reach it with no shared lock, so a torn interface read could crash the master. Store the parent in an atomic.Pointer and nil-guard the detached walk in DataNode.GetTopology. --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
237 lines
8.4 KiB
Go
237 lines
8.4 KiB
Go
package weed_server
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math/rand/v2"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/util/version"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/backend/memory_map"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/super_block"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/types"
|
|
"github.com/seaweedfs/seaweedfs/weed/topology"
|
|
util_http "github.com/seaweedfs/seaweedfs/weed/util/http"
|
|
)
|
|
|
|
func (ms *MasterServer) collectionDeleteHandler(w http.ResponseWriter, r *http.Request) {
|
|
collectionName := r.FormValue("collection")
|
|
if _, ok := ms.Topo.FindCollection(collectionName); !ok {
|
|
writeJsonError(w, r, http.StatusBadRequest, fmt.Errorf("collection %s does not exist", collectionName))
|
|
return
|
|
}
|
|
|
|
// The same two passes the gRPC CollectionDelete runs, rather than a second
|
|
// copy of the volume-server walk: this handler deleted only the normal
|
|
// volumes and left the collection's EC shards behind.
|
|
if err := ms.deleteCollection(r.Context(), collectionName); err != nil {
|
|
writeJsonError(w, r, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
return
|
|
}
|
|
|
|
func (ms *MasterServer) dirStatusHandler(w http.ResponseWriter, r *http.Request) {
|
|
m := make(map[string]interface{})
|
|
m["Version"] = version.Version()
|
|
m["TopologyId"] = ms.Topo.GetTopologyId()
|
|
m["Topology"] = ms.Topo.ToInfo()
|
|
writeJsonQuiet(w, r, http.StatusOK, m)
|
|
}
|
|
|
|
func (ms *MasterServer) volumeVacuumHandler(w http.ResponseWriter, r *http.Request) {
|
|
gcString := r.FormValue("garbageThreshold")
|
|
gcThreshold := ms.option.GarbageThreshold
|
|
if gcString != "" {
|
|
var err error
|
|
gcThreshold, err = strconv.ParseFloat(gcString, 32)
|
|
if err != nil {
|
|
glog.V(0).Infof("garbageThreshold %s is not a valid float number: %v", gcString, err)
|
|
writeJsonError(w, r, http.StatusNotAcceptable, fmt.Errorf("garbageThreshold %s is not a valid float number", gcString))
|
|
return
|
|
}
|
|
}
|
|
// glog.Infoln("garbageThreshold =", gcThreshold)
|
|
ms.Topo.Vacuum(ms.grpcDialOption, gcThreshold, ms.option.MaxParallelVacuumPerServer, ms.option.MaxParallelVacuumPerRack, 0, "", ms.preallocateSize, false, 0)
|
|
ms.dirStatusHandler(w, r)
|
|
}
|
|
|
|
func (ms *MasterServer) volumeGrowHandler(w http.ResponseWriter, r *http.Request) {
|
|
count := uint64(0)
|
|
option, err := ms.getVolumeGrowOption(r)
|
|
if err != nil {
|
|
writeJsonError(w, r, http.StatusNotAcceptable, err)
|
|
return
|
|
}
|
|
glog.V(0).Infof("volumeGrowHandler received %v from %v", option.String(), r.RemoteAddr)
|
|
|
|
if count, err = strconv.ParseUint(r.FormValue("count"), 10, 32); err == nil {
|
|
replicaCount := int64(count * uint64(option.ReplicaPlacement.GetCopyCount()))
|
|
if ms.Topo.AvailableSpaceFor(option) < replicaCount {
|
|
err = fmt.Errorf("only %d volumes left, not enough for %d", ms.Topo.AvailableSpaceFor(option), replicaCount)
|
|
} else if !ms.Topo.DataCenterExists(option.DataCenter) {
|
|
err = fmt.Errorf("data center %v not found in topology", option.DataCenter)
|
|
} else {
|
|
var newVidLocations []*master_pb.VolumeLocation
|
|
newVidLocations, err = ms.vg.GrowByCountAndType(ms.grpcDialOption, uint32(count), option, ms.Topo)
|
|
if len(newVidLocations) > 0 {
|
|
ms.broadcastVolumeLocationsToClients(newVidLocations)
|
|
}
|
|
count = uint64(len(newVidLocations))
|
|
}
|
|
} else {
|
|
err = fmt.Errorf("can not parse parameter count %s", r.FormValue("count"))
|
|
}
|
|
|
|
if err != nil {
|
|
writeJsonError(w, r, http.StatusNotAcceptable, err)
|
|
} else {
|
|
writeJsonQuiet(w, r, http.StatusOK, map[string]interface{}{"count": count})
|
|
}
|
|
}
|
|
|
|
func (ms *MasterServer) volumeStatusHandler(w http.ResponseWriter, r *http.Request) {
|
|
m := make(map[string]interface{})
|
|
m["Version"] = version.Version()
|
|
m["Volumes"] = ms.Topo.ToVolumeMap()
|
|
writeJsonQuiet(w, r, http.StatusOK, m)
|
|
}
|
|
|
|
func (ms *MasterServer) redirectHandler(w http.ResponseWriter, r *http.Request) {
|
|
vid, _, _, _, _ := parseURLPath(r.URL.Path)
|
|
collection := r.FormValue("collection")
|
|
location := ms.findVolumeLocation(collection, vid)
|
|
if location.Error == "" {
|
|
loc := location.Locations[rand.IntN(len(location.Locations))]
|
|
url, _ := util_http.NormalizeUrl(loc.PublicUrl)
|
|
if r.URL.RawQuery != "" {
|
|
url = url + r.URL.Path + "?" + r.URL.RawQuery
|
|
} else {
|
|
url = url + r.URL.Path
|
|
}
|
|
http.Redirect(w, r, url, http.StatusPermanentRedirect)
|
|
} else {
|
|
writeJsonError(w, r, http.StatusNotFound, fmt.Errorf("volume id %s not found: %s", vid, location.Error))
|
|
}
|
|
}
|
|
|
|
func (ms *MasterServer) submitFromMasterServerHandler(w http.ResponseWriter, r *http.Request) {
|
|
fileSizeLimitBytes := int64(ms.option.FileSizeLimitMB) * 1024 * 1024
|
|
if ms.Topo.IsLeader() {
|
|
submitForClientHandler(w, r, func(ctx context.Context) pb.ServerAddress { return ms.option.Master }, ms.grpcDialOption, fileSizeLimitBytes)
|
|
} else {
|
|
masterUrl, err := ms.Topo.Leader()
|
|
if err != nil {
|
|
writeJsonError(w, r, http.StatusInternalServerError, err)
|
|
} else {
|
|
submitForClientHandler(w, r, func(ctx context.Context) pb.ServerAddress { return masterUrl }, ms.grpcDialOption, fileSizeLimitBytes)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (ms *MasterServer) getVolumeGrowOption(r *http.Request) (*topology.VolumeGrowOption, error) {
|
|
replicationString := r.FormValue("replication")
|
|
if replicationString == "" {
|
|
replicationString = ms.option.DefaultReplicaPlacement
|
|
}
|
|
replicaPlacement, err := super_block.NewReplicaPlacementFromString(replicationString)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ttl, err := needle.ReadTTL(r.FormValue("ttl"))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
memoryMapMaxSizeMb, err := memory_map.ReadMemoryMapMaxSizeMb(r.FormValue("memoryMapMaxSizeMb"))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
diskType := types.ToDiskType(r.FormValue("disk"))
|
|
|
|
preallocate := ms.preallocateSize
|
|
if r.FormValue("preallocate") != "" {
|
|
preallocate, err = strconv.ParseInt(r.FormValue("preallocate"), 10, 64)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Failed to parse int64 preallocate = %s: %v", r.FormValue("preallocate"), err)
|
|
}
|
|
}
|
|
ver := needle.GetCurrentVersion()
|
|
volumeGrowOption := &topology.VolumeGrowOption{
|
|
Collection: r.FormValue("collection"),
|
|
ReplicaPlacement: replicaPlacement,
|
|
Ttl: ttl,
|
|
DiskType: diskType,
|
|
Preallocate: preallocate,
|
|
DataCenter: r.FormValue("dataCenter"),
|
|
Rack: r.FormValue("rack"),
|
|
DataNode: r.FormValue("dataNode"),
|
|
MemoryMapMaxSizeMb: memoryMapMaxSizeMb,
|
|
Version: uint32(ver),
|
|
}
|
|
return volumeGrowOption, nil
|
|
}
|
|
|
|
func (ms *MasterServer) collectionInfoHandler(w http.ResponseWriter, r *http.Request) {
|
|
//get collection from request
|
|
collectionName := r.FormValue("collection")
|
|
if collectionName == "" {
|
|
writeJsonError(w, r, http.StatusBadRequest, fmt.Errorf("collection is required"))
|
|
return
|
|
}
|
|
//output details of the volumes?
|
|
detail := r.FormValue("detail") == "true"
|
|
//collect collection info
|
|
collection, ok := ms.Topo.FindCollection(collectionName)
|
|
if !ok {
|
|
writeJsonError(w, r, http.StatusBadRequest, fmt.Errorf("collection %s does not exist", collectionName))
|
|
return
|
|
}
|
|
|
|
volumeLayouts := collection.GetAllVolumeLayouts()
|
|
|
|
if detail {
|
|
//prepare the json response
|
|
all_stats := make([]map[string]interface{}, len(volumeLayouts))
|
|
for i, volumeLayout := range volumeLayouts {
|
|
volumeLayoutStats := volumeLayout.Stats()
|
|
m := make(map[string]interface{})
|
|
m["Version"] = version.Version()
|
|
m["Collection"] = collectionName
|
|
m["TotalSize"] = volumeLayoutStats.TotalSize
|
|
m["FileCount"] = volumeLayoutStats.FileCount
|
|
m["UsedSize"] = volumeLayoutStats.UsedSize
|
|
all_stats[i] = m
|
|
}
|
|
//write it
|
|
writeJsonQuiet(w, r, http.StatusOK, all_stats)
|
|
} else {
|
|
//prepare the json response
|
|
collectionStats := map[string]interface{}{
|
|
"Version": version.Version(),
|
|
"Collection": collectionName,
|
|
"TotalSize": uint64(0),
|
|
"FileCount": uint64(0),
|
|
"UsedSize": uint64(0),
|
|
"VolumeCount": uint64(0),
|
|
}
|
|
for _, volumeLayout := range volumeLayouts {
|
|
volumeLayoutStats := volumeLayout.Stats()
|
|
collectionStats["TotalSize"] = collectionStats["TotalSize"].(uint64) + volumeLayoutStats.TotalSize
|
|
collectionStats["FileCount"] = collectionStats["FileCount"].(uint64) + volumeLayoutStats.FileCount
|
|
collectionStats["UsedSize"] = collectionStats["UsedSize"].(uint64) + volumeLayoutStats.UsedSize
|
|
collectionStats["VolumeCount"] = collectionStats["VolumeCount"].(uint64) + 1
|
|
}
|
|
//write it
|
|
writeJsonQuiet(w, r, http.StatusOK, collectionStats)
|
|
}
|
|
}
|