mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-08-16 12:16:36 +00:00
* master: stream volume listings A listing of 800k volumes is 36MB on the wire but 305MB as messages, and the master built all of it, then held it while grpc encoded it. Two of those at once is most of a small master's heap, and the maintenance scanner asks every 30 minutes. The topology goes out first, listing nothing, then its volumes in batches, so the master holds a batch rather than a cluster: 341MB of live heap for one listing becomes 4.4MB. It allocates much the same either way -- what changes is how much of it has to be live at once, which is what sets the heap ceiling. Batches are built under their disk's lock and sent outside it, so a slow reader stalls the stream rather than the topology. They therefore do not share one instant, which a single listing did not either: it takes each disk's lock in turn, so a volume moving during either can be seen twice or not at all. The client helper hides which kind of master answered: one too old for the stream is asked the old way and its reply cut into the same batches. Either way the topology handed over lists no volumes, so a caller cannot come to depend on finding them there. * admin: stream the listing the maintenance scan reads It asks for every volume in the cluster every 30 minutes. Reassembling it client-side keeps the scan identical -- ActiveTopology splits disks by the disk ids on the volumes, so it needs them in the topology -- while the master no longer builds the whole reply to send it. * topology: report a disk id that does not depend on map order A topology disk that fronts several physical disks took its reported id from whichever volume the map yielded first, so two listings of an unchanged disk could disagree. Take the smallest instead. * topology: test that a streamed listing rebuilds to the whole one The callers that stream now rebuild the listing from a topology sent without volumes plus the batches after it, so that has to come out the same as being sent it whole, at every batch size and under a filter. * clients: stream the volume listings that ask for everything The dashboard's list and export pages, the collection and ec shard pages, the topology view, the worker metrics and two shell commands each asked the master to build all 800k volumes into one reply. They read the same listing as before, rebuilt on their side, so the master no longer holds it. The three that already ask for one volume or one collection stay as they are: their replies are small, and streaming one costs a round trip to say so.
250 lines
8.3 KiB
Go
250 lines
8.3 KiB
Go
package pluginworker
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/admin/topology"
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
|
|
workertypes "github.com/seaweedfs/seaweedfs/weed/worker/types"
|
|
"google.golang.org/grpc"
|
|
)
|
|
|
|
// CollectVolumeMetricsFromMasters dials the provided master addresses in order
|
|
// until one returns a usable volume list, then converts that into per-volume
|
|
// health metrics, an active-topology view, and a replica-location map.
|
|
func CollectVolumeMetricsFromMasters(
|
|
ctx context.Context,
|
|
masterAddresses []string,
|
|
collectionFilter string,
|
|
grpcDialOption grpc.DialOption,
|
|
) ([]*workertypes.VolumeHealthMetrics, *topology.ActiveTopology, map[uint32][]workertypes.ReplicaLocation, error) {
|
|
if grpcDialOption == nil {
|
|
return nil, nil, nil, fmt.Errorf("grpc dial option is not configured")
|
|
}
|
|
if len(masterAddresses) == 0 {
|
|
return nil, nil, nil, fmt.Errorf("no master addresses provided in cluster context")
|
|
}
|
|
|
|
for _, masterAddress := range masterAddresses {
|
|
response, err := FetchVolumeList(ctx, masterAddress, grpcDialOption)
|
|
if err != nil {
|
|
glog.Warningf("Plugin worker failed master volume list at %s: %v", masterAddress, err)
|
|
continue
|
|
}
|
|
|
|
metrics, activeTopology, replicaMap, buildErr := buildVolumeMetrics(response, collectionFilter)
|
|
if buildErr != nil {
|
|
// Configuration errors (e.g. invalid regex) will fail on every master,
|
|
// so return immediately instead of masking them with retries.
|
|
if isConfigError(buildErr) {
|
|
return nil, nil, nil, buildErr
|
|
}
|
|
glog.Warningf("Plugin worker failed to build metrics from master %s: %v", masterAddress, buildErr)
|
|
continue
|
|
}
|
|
return metrics, activeTopology, replicaMap, nil
|
|
}
|
|
|
|
return nil, nil, nil, fmt.Errorf("failed to load topology from all provided masters")
|
|
}
|
|
|
|
// FetchVolumeList dials the given master address (trying both the address as
|
|
// given and the gRPC port variant) and returns the master's volume list. Used
|
|
// by detection helpers that already know which master address to talk to.
|
|
// FetchDefaultReplicaPlacement returns the master's configured default replication
|
|
// (GetMasterConfiguration), used by detectors as the replica-placement fallback so
|
|
// the plugin path matches the shell. Returns "" if it cannot be fetched, so callers
|
|
// fall back to even spread rather than failing detection.
|
|
func FetchDefaultReplicaPlacement(ctx context.Context, masterAddresses []string, grpcDialOption grpc.DialOption) string {
|
|
if grpcDialOption == nil {
|
|
return ""
|
|
}
|
|
for _, address := range masterAddresses {
|
|
for _, candidate := range MasterAddressCandidates(address) {
|
|
if ctx.Err() != nil {
|
|
return ""
|
|
}
|
|
dialCtx, cancelDial := context.WithTimeout(ctx, 5*time.Second)
|
|
conn, err := pb.GrpcDial(dialCtx, candidate, false, grpcDialOption)
|
|
cancelDial()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
client := master_pb.NewSeaweedClient(conn)
|
|
callCtx, cancelCall := context.WithTimeout(ctx, 10*time.Second)
|
|
resp, callErr := client.GetMasterConfiguration(callCtx, &master_pb.GetMasterConfigurationRequest{})
|
|
cancelCall()
|
|
_ = conn.Close()
|
|
if callErr == nil {
|
|
return resp.DefaultReplication
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func FetchVolumeList(ctx context.Context, address string, grpcDialOption grpc.DialOption) (*master_pb.VolumeListResponse, error) {
|
|
var lastErr error
|
|
for _, candidate := range MasterAddressCandidates(address) {
|
|
if ctx.Err() != nil {
|
|
return nil, ctx.Err()
|
|
}
|
|
|
|
dialCtx, cancelDial := context.WithTimeout(ctx, 5*time.Second)
|
|
conn, err := pb.GrpcDial(dialCtx, candidate, false, grpcDialOption)
|
|
cancelDial()
|
|
if err != nil {
|
|
lastErr = err
|
|
continue
|
|
}
|
|
|
|
client := master_pb.NewSeaweedClient(conn)
|
|
callCtx, cancelCall := context.WithTimeout(ctx, 10*time.Second)
|
|
response, callErr := pb.CollectVolumeList(callCtx, client, &master_pb.VolumeListRequest{})
|
|
cancelCall()
|
|
_ = conn.Close()
|
|
|
|
if callErr == nil {
|
|
return response, nil
|
|
}
|
|
lastErr = callErr
|
|
}
|
|
|
|
if lastErr == nil {
|
|
lastErr = fmt.Errorf("no valid master address candidate")
|
|
}
|
|
return nil, lastErr
|
|
}
|
|
|
|
func buildVolumeMetrics(
|
|
response *master_pb.VolumeListResponse,
|
|
collectionFilter string,
|
|
) ([]*workertypes.VolumeHealthMetrics, *topology.ActiveTopology, map[uint32][]workertypes.ReplicaLocation, error) {
|
|
if response == nil || response.TopologyInfo == nil {
|
|
return nil, nil, nil, fmt.Errorf("volume list response has no topology info")
|
|
}
|
|
|
|
activeTopology := topology.NewActiveTopology(10)
|
|
if err := activeTopology.UpdateTopology(response.TopologyInfo); err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
var collectionRegex *regexp.Regexp
|
|
trimmedFilter := strings.TrimSpace(collectionFilter)
|
|
filterMode := CollectionFilterMode(trimmedFilter)
|
|
if trimmedFilter != "" && filterMode != CollectionFilterAll && filterMode != CollectionFilterEach && trimmedFilter != "*" {
|
|
var err error
|
|
collectionRegex, err = regexp.Compile(trimmedFilter)
|
|
if err != nil {
|
|
return nil, nil, nil, &configError{err: fmt.Errorf("invalid collection_filter regex %q: %w", trimmedFilter, err)}
|
|
}
|
|
}
|
|
|
|
volumeSizeLimitBytes := uint64(response.VolumeSizeLimitMb) * 1024 * 1024
|
|
now := time.Now()
|
|
metrics := make([]*workertypes.VolumeHealthMetrics, 0, 256)
|
|
replicaMap := make(map[uint32][]workertypes.ReplicaLocation)
|
|
|
|
for _, dc := range response.TopologyInfo.DataCenterInfos {
|
|
for _, rack := range dc.RackInfos {
|
|
for _, node := range rack.DataNodeInfos {
|
|
for diskType, diskInfo := range node.DiskInfos {
|
|
for _, volume := range diskInfo.VolumeInfos {
|
|
// Build replica map from ALL volumes BEFORE collection filtering,
|
|
// since replicas may span filtered/unfiltered nodes.
|
|
replicaMap[volume.Id] = append(replicaMap[volume.Id], workertypes.ReplicaLocation{
|
|
DataCenter: dc.Id,
|
|
Rack: rack.Id,
|
|
NodeID: node.Id,
|
|
Host: pb.NewServerAddressFromDataNode(node).ToHost(),
|
|
})
|
|
|
|
if collectionRegex != nil && !collectionRegex.MatchString(volume.Collection) {
|
|
continue
|
|
}
|
|
|
|
metric := &workertypes.VolumeHealthMetrics{
|
|
VolumeID: volume.Id,
|
|
Server: node.Id,
|
|
ServerAddress: string(pb.NewServerAddressFromDataNode(node)),
|
|
DiskType: diskType,
|
|
DiskId: volume.DiskId,
|
|
DataCenter: dc.Id,
|
|
Rack: rack.Id,
|
|
Collection: volume.Collection,
|
|
Size: volume.Size,
|
|
DeletedBytes: volume.DeletedByteCount,
|
|
LastModified: time.Unix(volume.ModifiedAtSecond, 0),
|
|
ReplicaCount: 1,
|
|
ExpectedReplicas: int(volume.ReplicaPlacement),
|
|
IsReadOnly: volume.ReadOnly,
|
|
HasRemoteCopy: volume.RemoteStorageName != "",
|
|
}
|
|
if metric.Size > 0 {
|
|
metric.GarbageRatio = float64(metric.DeletedBytes) / float64(metric.Size)
|
|
}
|
|
if volumeSizeLimitBytes > 0 {
|
|
metric.FullnessRatio = float64(metric.Size) / float64(volumeSizeLimitBytes)
|
|
}
|
|
metric.Age = now.Sub(metric.LastModified)
|
|
metrics = append(metrics, metric)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
replicaCounts := make(map[uint32]int)
|
|
for _, metric := range metrics {
|
|
replicaCounts[metric.VolumeID]++
|
|
}
|
|
for _, metric := range metrics {
|
|
metric.ReplicaCount = replicaCounts[metric.VolumeID]
|
|
}
|
|
|
|
return metrics, activeTopology, replicaMap, nil
|
|
}
|
|
|
|
// configError wraps configuration errors that should not be retried across masters.
|
|
type configError struct {
|
|
err error
|
|
}
|
|
|
|
func (e *configError) Error() string { return e.err.Error() }
|
|
func (e *configError) Unwrap() error { return e.err }
|
|
|
|
func isConfigError(err error) bool {
|
|
var ce *configError
|
|
return errors.As(err, &ce)
|
|
}
|
|
|
|
// MasterAddressCandidates returns address forms to try when dialing a master:
|
|
// the address as given plus the gRPC variant (port + 10000). Both are tried
|
|
// because callers may pass either an HTTP-port or gRPC-port address.
|
|
func MasterAddressCandidates(address string) []string {
|
|
trimmed := strings.TrimSpace(address)
|
|
if trimmed == "" {
|
|
return nil
|
|
}
|
|
candidateSet := map[string]struct{}{
|
|
trimmed: {},
|
|
}
|
|
converted := pb.ServerToGrpcAddress(trimmed)
|
|
candidateSet[converted] = struct{}{}
|
|
|
|
candidates := make([]string, 0, len(candidateSet))
|
|
for candidate := range candidateSet {
|
|
candidates = append(candidates, candidate)
|
|
}
|
|
sort.Strings(candidates)
|
|
return candidates
|
|
}
|