mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-20 13:30:46 +02:00
* admin: count each chunk once in the dashboard total The dashboard summed file_count from every node's volume list, so a chunk was counted once per replica and deleted chunks were never subtracted. Reuse the collection aggregation, which dedupes replicas and EC shard holders and nets out tombstones. * admin: the dashboard card counts chunks, so name it that Volumes store chunks, and a file is split into one or more of them, so the 'Total Files' card always read far higher than the number of files in the filer. Rename it to 'Total Chunks' and say so in the tooltip. * admin: collections pages count chunks once and say so The collections list and detail pages summed file_count straight off the topology, so replicas multiplied the count, tombstones stayed in it, and the detail page ignored EC volumes entirely. Take the numbers from the shared collection aggregation and label them chunks. * admin: dedupe replica chunk counts per volume instead of dividing Dividing each replica's live count by the copy count truncated a chunk per odd-sized volume, and reported half the count while a volume's second replica had not checked in yet. Replicas mirror each other's needles and deletes, so keep the fullest report per volume id. * admin: fix the collections CSV export column mapping The exporter read chunks from the EC-volume cell and shifted size and disk types with it. Read every column the table actually has.
395 lines
11 KiB
Go
395 lines
11 KiB
Go
package dash
|
|
|
|
import (
|
|
"context"
|
|
"sort"
|
|
"time"
|
|
|
|
"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 := client.VolumeList(context.Background(), &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
|
|
}
|