Files
Chris LuandGitHub 97a155d14d admin: show capacity per storage tier and stop counting remote-tiered bytes as local disk usage (#10766)
* admin: show capacity per storage tier and stop counting remote-tiered bytes as local disk usage

A remote-tiered volume reports its cloud object's size, so summing volume
sizes inflated the dashboard's used-vs-capacity numbers (the local .dat is
gone after volume.tier.move). Split the accounting: DiskUsage now only
counts bytes on local disks, with the cloud bytes surfaced separately per
server and per remote storage name.

The dashboard gains a Storage Tiers table breaking volumes and EC shards
down by tier (each local disk type plus each remote storage), using the
per-disk-type statfs numbers already in the VolumeList response. The
volumes page badges remote-tiered volumes with their storage name, and
the EC shards page fills in real per-shard sizes instead of hardcoding 0.

* admin: review fixes for the tier capacity display

- A disk that predates disk_total_bytes now contributes its logical
  bytes to the tier's DiskUsed, so a tier mixing old and new volume
  servers doesn't underreport usage; the usage bar always reflects the
  displayed Disk Used value (the DataSize fallback in UsagePercent is
  gone, and the percent math is overflow-safe).
- getTopologyViaGRPC defaults a zero VolumeSizeLimitMb to 30000 MB like
  GetClusterVolumeServers, keeping slot-based capacities consistent.
- The dashboard volume-servers column reads Usage / Capacity to match
  its cell content, and the hdd disk-type default is shared between the
  volumes-page badge and countUniqueDiskTypes.
2026-08-15 12:35:16 -07:00

135 lines
4.1 KiB
Go

package dash
import (
"sort"
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
)
// TierStats aggregates the volumes and EC shards that live on one storage
// tier: a local disk type ("hdd", "ssd", or a custom tag), or the remote
// storage a tiered volume was uploaded to. A remote-tiered volume reports
// the size of its cloud object, so its bytes belong to the remote tier,
// not to the local disk that holds only its index.
type TierStats struct {
Name string `json:"name"`
IsRemote bool `json:"is_remote"`
VolumeCount int `json:"volume_count"`
EcShardCount int `json:"ec_shard_count"`
DataSize int64 `json:"data_size"`
DiskUsed int64 `json:"disk_used"`
DiskCapacity int64 `json:"disk_capacity"`
MaxVolumes int64 `json:"max_volumes"`
}
// UsagePercent is the tier's local disk usage in percent, clamped to
// [0, 100]. Remote tiers have no capacity and return 0.
func (t TierStats) UsagePercent() int {
if t.IsRemote || t.DiskCapacity <= 0 {
return 0
}
percent := int(float64(t.DiskUsed) / float64(t.DiskCapacity) * 100)
if percent < 0 {
return 0
}
if percent > 100 {
return 100
}
return percent
}
// tierDiskType maps the empty disk type to its display name.
func tierDiskType(diskType string) string {
if diskType == "" {
return "hdd"
}
return diskType
}
// CollectTierStats walks the topology and groups capacity and usage by
// tier. DiskUsed/DiskCapacity come from the statfs numbers the volume
// servers report per disk type; a disk that predates disk_total_bytes
// falls back to the slot-based capacity estimate and to the logical
// bytes it holds, so mixed-version tiers don't underreport usage.
// Remote tiers have no local disk, so only VolumeCount and DataSize are
// meaningful there.
func CollectTierStats(topo *master_pb.TopologyInfo, volumeSizeLimitMb uint64) []TierStats {
if topo == nil {
return nil
}
tiers := make(map[string]*TierStats)
tier := func(name string, isRemote bool) *TierStats {
key := name
if isRemote {
key = "remote\x00" + name
}
t := tiers[key]
if t == nil {
t = &TierStats{Name: name, IsRemote: isRemote}
tiers[key] = t
}
return t
}
for _, dc := range topo.DataCenterInfos {
for _, rack := range dc.RackInfos {
for _, node := range rack.DataNodeInfos {
for _, diskInfo := range node.DiskInfos {
local := tier(tierDiskType(diskInfo.Type), false)
local.MaxVolumes += diskInfo.MaxVolumeCount
hasStatfs := diskInfo.DiskTotalBytes > 0
if hasStatfs {
local.DiskCapacity += int64(diskInfo.DiskTotalBytes)
if diskInfo.DiskTotalBytes > diskInfo.DiskFreeBytes {
local.DiskUsed += int64(diskInfo.DiskTotalBytes - diskInfo.DiskFreeBytes)
}
} else {
local.DiskCapacity += diskInfo.MaxVolumeCount * int64(volumeSizeLimitMb) * 1024 * 1024
}
var diskLocalBytes int64
for _, volInfo := range diskInfo.VolumeInfos {
if volInfo.RemoteStorageName != "" {
remote := tier(volInfo.RemoteStorageName, true)
remote.VolumeCount++
remote.DataSize += int64(volInfo.Size)
} else {
local.VolumeCount++
local.DataSize += int64(volInfo.Size)
diskLocalBytes += int64(volInfo.Size)
}
}
// ShardSizes is local to this node, so summing across
// nodes gives the tier's physical footprint.
for _, ecShardInfo := range diskInfo.EcShardInfos {
local.EcShardCount += erasure_coding.GetShardCount(ecShardInfo)
ecBytes := erasure_coding.EcShardsTotalSize(ecShardInfo)
local.DataSize += ecBytes
diskLocalBytes += ecBytes
}
// Without statfs numbers, approximate this disk's
// footprint with the logical bytes it holds.
if !hasStatfs {
local.DiskUsed += diskLocalBytes
}
}
}
}
}
result := make([]TierStats, 0, len(tiers))
for _, t := range tiers {
result = append(result, *t)
}
sort.Slice(result, func(i, j int) bool {
if result[i].IsRemote != result[j].IsRemote {
return !result[i].IsRemote
}
return result[i].Name < result[j].Name
})
return result
}