mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-08-21 14:46:58 +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.
396 lines
11 KiB
Go
396 lines
11 KiB
Go
package dash
|
|
|
|
import (
|
|
"context"
|
|
"sort"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
|
|
)
|
|
|
|
// GetClusterCollections retrieves cluster collections data
|
|
func (s *AdminServer) GetClusterCollections() (*ClusterCollectionsData, error) {
|
|
var collections []CollectionInfo
|
|
var totalVolumes int
|
|
var totalEcVolumes int
|
|
var totalChunks int64
|
|
var totalSize int64
|
|
collectionMap := make(map[string]*CollectionInfo)
|
|
|
|
// Get actual collection information from volume data
|
|
err := s.WithMasterClient(func(client master_pb.SeaweedClient) error {
|
|
resp, err := pb.CollectVolumeList(context.Background(), client, &master_pb.VolumeListRequest{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if resp.TopologyInfo != nil {
|
|
for _, dc := range resp.TopologyInfo.DataCenterInfos {
|
|
for _, rack := range dc.RackInfos {
|
|
for _, node := range rack.DataNodeInfos {
|
|
for _, diskInfo := range node.DiskInfos {
|
|
// Process regular volumes
|
|
for _, volInfo := range diskInfo.VolumeInfos {
|
|
// Extract collection name from volume info
|
|
collectionName := volInfo.Collection
|
|
if collectionName == "" {
|
|
collectionName = "default" // Default collection for volumes without explicit collection
|
|
}
|
|
|
|
// Get disk type from volume info, default to hdd if empty
|
|
diskType := volInfo.DiskType
|
|
if diskType == "" {
|
|
diskType = "hdd"
|
|
}
|
|
|
|
// Get or create collection info
|
|
if collection, exists := collectionMap[collectionName]; exists {
|
|
collection.VolumeCount++
|
|
collection.TotalSize += int64(volInfo.Size)
|
|
|
|
// Update data center if this collection spans multiple DCs
|
|
if collection.DataCenter != dc.Id && collection.DataCenter != "multi" {
|
|
collection.DataCenter = "multi"
|
|
}
|
|
|
|
// Add disk type if not already present
|
|
diskTypeExists := false
|
|
for _, existingDiskType := range collection.DiskTypes {
|
|
if existingDiskType == diskType {
|
|
diskTypeExists = true
|
|
break
|
|
}
|
|
}
|
|
if !diskTypeExists {
|
|
collection.DiskTypes = append(collection.DiskTypes, diskType)
|
|
}
|
|
|
|
totalVolumes++
|
|
totalSize += int64(volInfo.Size)
|
|
} else {
|
|
newCollection := CollectionInfo{
|
|
Name: collectionName,
|
|
DataCenter: dc.Id,
|
|
VolumeCount: 1,
|
|
EcVolumeCount: 0,
|
|
TotalSize: int64(volInfo.Size),
|
|
DiskTypes: []string{diskType},
|
|
}
|
|
collectionMap[collectionName] = &newCollection
|
|
totalVolumes++
|
|
totalSize += int64(volInfo.Size)
|
|
}
|
|
}
|
|
|
|
// Process EC volumes
|
|
ecVolumeMap := make(map[uint32]bool) // Track unique EC volumes to avoid double counting
|
|
for _, ecShardInfo := range diskInfo.EcShardInfos {
|
|
// Extract collection name from EC shard info
|
|
collectionName := ecShardInfo.Collection
|
|
if collectionName == "" {
|
|
collectionName = "default" // Default collection for EC volumes without explicit collection
|
|
}
|
|
|
|
// Only count each EC volume once (not per shard)
|
|
if !ecVolumeMap[ecShardInfo.Id] {
|
|
ecVolumeMap[ecShardInfo.Id] = true
|
|
|
|
// Get disk type from disk info, default to hdd if empty
|
|
diskType := diskInfo.Type
|
|
if diskType == "" {
|
|
diskType = "hdd"
|
|
}
|
|
|
|
// Get or create collection info
|
|
if collection, exists := collectionMap[collectionName]; exists {
|
|
collection.EcVolumeCount++
|
|
|
|
// Update data center if this collection spans multiple DCs
|
|
if collection.DataCenter != dc.Id && collection.DataCenter != "multi" {
|
|
collection.DataCenter = "multi"
|
|
}
|
|
|
|
// Add disk type if not already present
|
|
diskTypeExists := false
|
|
for _, existingDiskType := range collection.DiskTypes {
|
|
if existingDiskType == diskType {
|
|
diskTypeExists = true
|
|
break
|
|
}
|
|
}
|
|
if !diskTypeExists {
|
|
collection.DiskTypes = append(collection.DiskTypes, diskType)
|
|
}
|
|
|
|
totalEcVolumes++
|
|
} else {
|
|
newCollection := CollectionInfo{
|
|
Name: collectionName,
|
|
DataCenter: dc.Id,
|
|
VolumeCount: 0,
|
|
EcVolumeCount: 1,
|
|
TotalSize: 0,
|
|
DiskTypes: []string{diskType},
|
|
}
|
|
collectionMap[collectionName] = &newCollection
|
|
totalEcVolumes++
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Chunk counts come from the shared collection aggregation, which
|
|
// nets out tombstones and counts a chunk once no matter how many
|
|
// volume replicas or EC shard holders report it.
|
|
for collectionName, stats := range collectCollectionStats(resp.TopologyInfo) {
|
|
if collection, exists := collectionMap[collectionName]; exists {
|
|
collection.ChunkCount = stats.FileCount
|
|
totalChunks += stats.FileCount
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Convert map to slice
|
|
for _, collection := range collectionMap {
|
|
collections = append(collections, *collection)
|
|
}
|
|
|
|
// Sort collections alphabetically by name
|
|
sort.Slice(collections, func(i, j int) bool {
|
|
return collections[i].Name < collections[j].Name
|
|
})
|
|
|
|
// If no collections found, show a message indicating no collections exist
|
|
if len(collections) == 0 {
|
|
// Return empty collections data instead of creating fake ones
|
|
return &ClusterCollectionsData{
|
|
Collections: []CollectionInfo{},
|
|
TotalCollections: 0,
|
|
TotalVolumes: 0,
|
|
TotalEcVolumes: 0,
|
|
TotalChunks: 0,
|
|
TotalSize: 0,
|
|
LastUpdated: time.Now(),
|
|
}, nil
|
|
}
|
|
|
|
return &ClusterCollectionsData{
|
|
Collections: collections,
|
|
TotalCollections: len(collections),
|
|
TotalVolumes: totalVolumes,
|
|
TotalEcVolumes: totalEcVolumes,
|
|
TotalChunks: totalChunks,
|
|
TotalSize: totalSize,
|
|
LastUpdated: time.Now(),
|
|
}, nil
|
|
}
|
|
|
|
// GetCollectionDetails retrieves detailed information for a specific collection including volumes and EC volumes
|
|
func (s *AdminServer) GetCollectionDetails(collectionName string, page int, pageSize int, sortBy string, sortOrder string) (*CollectionDetailsData, error) {
|
|
// Set defaults
|
|
if page < 1 {
|
|
page = 1
|
|
}
|
|
if pageSize < 1 || pageSize > 1000 {
|
|
pageSize = 25
|
|
}
|
|
if sortBy == "" {
|
|
sortBy = "volume_id"
|
|
}
|
|
if sortOrder == "" {
|
|
sortOrder = "asc"
|
|
}
|
|
|
|
var regularVolumes []VolumeWithTopology
|
|
var ecVolumes []EcVolumeWithShards
|
|
var totalSize int64
|
|
dataCenters := make(map[string]bool)
|
|
diskTypes := make(map[string]bool)
|
|
|
|
// Get regular volumes for this collection
|
|
regularVolumeData, err := s.GetClusterVolumes(1, 10000, "volume_id", "asc", collectionName) // Get all volumes
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
regularVolumes = regularVolumeData.Volumes
|
|
totalSize = regularVolumeData.TotalSize
|
|
|
|
// Chunk counts come from the shared collection aggregation, which nets out
|
|
// tombstones and counts a chunk once no matter how many volume replicas or
|
|
// EC shard holders report it. Fail rather than render a zero that reads
|
|
// like an empty collection.
|
|
stats, err := s.getCollectionStats()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
totalChunks := stats[collectionName].FileCount
|
|
|
|
// Collect data centers and disk types from regular volumes
|
|
for _, vol := range regularVolumes {
|
|
dataCenters[vol.DataCenter] = true
|
|
diskTypes[vol.DiskType] = true
|
|
}
|
|
|
|
// Get EC volumes for this collection
|
|
ecVolumeData, err := s.GetClusterEcVolumes(1, 10000, "volume_id", "asc", collectionName) // Get all EC volumes
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ecVolumes = ecVolumeData.EcVolumes
|
|
|
|
// Collect data centers from EC volumes
|
|
for _, ecVol := range ecVolumes {
|
|
for _, dc := range ecVol.DataCenters {
|
|
dataCenters[dc] = true
|
|
}
|
|
}
|
|
|
|
// Combine all volumes for sorting and pagination
|
|
type VolumeForSorting struct {
|
|
Type string // "regular" or "ec"
|
|
RegularVolume *VolumeWithTopology
|
|
EcVolume *EcVolumeWithShards
|
|
}
|
|
|
|
var allVolumes []VolumeForSorting
|
|
for i := range regularVolumes {
|
|
allVolumes = append(allVolumes, VolumeForSorting{
|
|
Type: "regular",
|
|
RegularVolume: ®ularVolumes[i],
|
|
})
|
|
}
|
|
for i := range ecVolumes {
|
|
allVolumes = append(allVolumes, VolumeForSorting{
|
|
Type: "ec",
|
|
EcVolume: &ecVolumes[i],
|
|
})
|
|
}
|
|
|
|
// Sort all volumes
|
|
sort.Slice(allVolumes, func(i, j int) bool {
|
|
var less bool
|
|
switch sortBy {
|
|
case "volume_id":
|
|
var idI, idJ uint32
|
|
if allVolumes[i].Type == "regular" {
|
|
idI = allVolumes[i].RegularVolume.Id
|
|
} else {
|
|
idI = allVolumes[i].EcVolume.VolumeID
|
|
}
|
|
if allVolumes[j].Type == "regular" {
|
|
idJ = allVolumes[j].RegularVolume.Id
|
|
} else {
|
|
idJ = allVolumes[j].EcVolume.VolumeID
|
|
}
|
|
less = idI < idJ
|
|
case "type":
|
|
// Sort by type first (regular before ec), then by volume ID
|
|
if allVolumes[i].Type == allVolumes[j].Type {
|
|
var idI, idJ uint32
|
|
if allVolumes[i].Type == "regular" {
|
|
idI = allVolumes[i].RegularVolume.Id
|
|
} else {
|
|
idI = allVolumes[i].EcVolume.VolumeID
|
|
}
|
|
if allVolumes[j].Type == "regular" {
|
|
idJ = allVolumes[j].RegularVolume.Id
|
|
} else {
|
|
idJ = allVolumes[j].EcVolume.VolumeID
|
|
}
|
|
less = idI < idJ
|
|
} else {
|
|
less = allVolumes[i].Type < allVolumes[j].Type // "ec" < "regular"
|
|
}
|
|
default:
|
|
// Default to volume ID sort
|
|
var idI, idJ uint32
|
|
if allVolumes[i].Type == "regular" {
|
|
idI = allVolumes[i].RegularVolume.Id
|
|
} else {
|
|
idI = allVolumes[i].EcVolume.VolumeID
|
|
}
|
|
if allVolumes[j].Type == "regular" {
|
|
idJ = allVolumes[j].RegularVolume.Id
|
|
} else {
|
|
idJ = allVolumes[j].EcVolume.VolumeID
|
|
}
|
|
less = idI < idJ
|
|
}
|
|
|
|
if sortOrder == "desc" {
|
|
return !less
|
|
}
|
|
return less
|
|
})
|
|
|
|
// Apply pagination
|
|
totalVolumesAndEc := len(allVolumes)
|
|
totalPages := (totalVolumesAndEc + pageSize - 1) / pageSize
|
|
startIndex := (page - 1) * pageSize
|
|
endIndex := startIndex + pageSize
|
|
if endIndex > totalVolumesAndEc {
|
|
endIndex = totalVolumesAndEc
|
|
}
|
|
|
|
if startIndex >= totalVolumesAndEc {
|
|
startIndex = 0
|
|
endIndex = 0
|
|
}
|
|
|
|
// Extract paginated results
|
|
var paginatedRegularVolumes []VolumeWithTopology
|
|
var paginatedEcVolumes []EcVolumeWithShards
|
|
|
|
for i := startIndex; i < endIndex; i++ {
|
|
if allVolumes[i].Type == "regular" {
|
|
paginatedRegularVolumes = append(paginatedRegularVolumes, *allVolumes[i].RegularVolume)
|
|
} else {
|
|
paginatedEcVolumes = append(paginatedEcVolumes, *allVolumes[i].EcVolume)
|
|
}
|
|
}
|
|
|
|
// Convert maps to slices
|
|
var dcList []string
|
|
for dc := range dataCenters {
|
|
dcList = append(dcList, dc)
|
|
}
|
|
sort.Strings(dcList)
|
|
|
|
var diskTypeList []string
|
|
for diskType := range diskTypes {
|
|
diskTypeList = append(diskTypeList, diskType)
|
|
}
|
|
sort.Strings(diskTypeList)
|
|
|
|
return &CollectionDetailsData{
|
|
CollectionName: collectionName,
|
|
RegularVolumes: paginatedRegularVolumes,
|
|
EcVolumes: paginatedEcVolumes,
|
|
TotalVolumes: len(regularVolumes),
|
|
TotalEcVolumes: len(ecVolumes),
|
|
TotalChunks: totalChunks,
|
|
TotalSize: totalSize,
|
|
DataCenters: dcList,
|
|
DiskTypes: diskTypeList,
|
|
LastUpdated: time.Now(),
|
|
Page: page,
|
|
PageSize: pageSize,
|
|
TotalPages: totalPages,
|
|
SortBy: sortBy,
|
|
SortOrder: sortOrder,
|
|
}, nil
|
|
}
|