mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-07 04:27:49 +01:00
admin: show disk usage on the dashboard instead of the data size (#12466)
* admin: show disk usage on the dashboard instead of the data size The dashboard's Total Size card summed the volume and EC shard sizes the volume servers report. That leaves out deleted needles waiting for vacuum, EC generations kept after a vacuum, and anything else on the disks, so on a cluster with a lot of EC vacuum activity it showed far less than the disks held. The card now shows the bytes in use on the volume servers' disks, the same statfs numbers the volume server list uses, with the disks' capacity, and its trend line follows that number. Servers that report no statfs numbers count their data size, as on the volume server list. Claude-Session: https://claude.ai/code/session_011UpvafcfUuC2mLxNAXLQJi * admin: compute the disk usage percentage in float64 current*100 overflows int64 past about 92 PB of used space, so the dashboard card showed a negative percentage. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * admin: size the volume server disk bar with calculatePercent64 int(host.DiskUsage) truncates byte counts on 32-bit builds, so the bar width was wrong for any disk over 2 GB there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * admin: test the cluster disk totals across statfs and fallback servers Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fd81a50c57
commit
7beac0e8ea
11 files changed
+559
-425
No files matched your search
@@ -25,6 +25,8 @@ type AdminData struct {
|
||||
TotalVolumes int `json:"total_volumes"`
|
||||
TotalChunks int64 `json:"total_chunks"`
|
||||
TotalSize int64 `json:"total_size"`
|
||||
DiskUsage int64 `json:"disk_usage"`
|
||||
DiskCapacity int64 `json:"disk_capacity"`
|
||||
VolumeSizeLimitMB uint64 `json:"volume_size_limit_mb"`
|
||||
MasterNodes []MasterNode `json:"master_nodes"`
|
||||
VolumeServers []VolumeServer `json:"volume_servers"`
|
||||
@@ -240,6 +242,8 @@ func (s *AdminServer) GetAdminData(username string) (AdminData, error) {
|
||||
TotalVolumes: topology.TotalVolumes,
|
||||
TotalChunks: topology.TotalChunks,
|
||||
TotalSize: topology.TotalSize,
|
||||
DiskUsage: topology.DiskUsage,
|
||||
DiskCapacity: topology.DiskCapacity,
|
||||
VolumeSizeLimitMB: volumeSizeLimitMB,
|
||||
MasterNodes: masterNodes,
|
||||
VolumeServers: volumeServersData.VolumeServers,
|
||||
|
||||
@@ -235,6 +235,8 @@ func (s *AdminServer) getTopologyViaGRPC(topology *ClusterTopology) error {
|
||||
// TotalSize is the logical data size, wherever the
|
||||
// bytes live, so remote-tiered volumes still count.
|
||||
topology.TotalSize += totalSize + remoteSize
|
||||
topology.DiskUsage += diskUsage
|
||||
topology.DiskCapacity += diskCapacity
|
||||
}
|
||||
|
||||
dataCenter.Racks = append(dataCenter.Racks, rackObj)
|
||||
|
||||
@@ -60,7 +60,7 @@ func (s *AdminServer) recordDashboardSample() {
|
||||
t: time.Now(),
|
||||
volumes: float64(topology.TotalVolumes),
|
||||
ecShards: float64(ecShards),
|
||||
diskUsed: float64(topology.TotalSize),
|
||||
diskUsed: float64(topology.DiskUsage),
|
||||
chunks: float64(topology.TotalChunks),
|
||||
}
|
||||
if s.maintenanceManager != nil {
|
||||
|
||||
@@ -18,9 +18,14 @@ type ClusterTopology struct {
|
||||
TotalVolumes int `json:"total_volumes"`
|
||||
// TotalChunks counts chunks stored in volumes, not filer entries: a file
|
||||
// is split into one or more chunks.
|
||||
TotalChunks int64 `json:"total_chunks"`
|
||||
TotalSize int64 `json:"total_size"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
TotalChunks int64 `json:"total_chunks"`
|
||||
TotalSize int64 `json:"total_size"`
|
||||
// DiskUsage and DiskCapacity sum the volume servers' DiskUsage and
|
||||
// DiskCapacity: the bytes in use on their disks, parity, deleted needles
|
||||
// and retained EC generations included, and the disks' size.
|
||||
DiskUsage int64 `json:"disk_usage"`
|
||||
DiskCapacity int64 `json:"disk_capacity"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
// TierStats breaks volumes and EC shards down by storage tier: local
|
||||
// disk types plus one entry per remote storage holding tiered volumes.
|
||||
TierStats []TierStats `json:"tier_stats"`
|
||||
|
||||
@@ -282,3 +282,35 @@ func TestGetClusterVolumeServersDiskAccounting(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// The dashboard totals sum each server's disk usage and capacity, statfs or
|
||||
// fallback, while TotalSize stays the logical data size.
|
||||
func TestClusterTopologyDiskTotals(t *testing.T) {
|
||||
admin := newVolumeServersTestAdmin(t, volumeAccessTestTopology(
|
||||
&master_pb.DataNodeInfo{
|
||||
Id: "node-a",
|
||||
DiskInfos: map[string]*master_pb.DiskInfo{"hdd": {
|
||||
MaxVolumeCount: 100,
|
||||
DiskTotalBytes: 1_000_000_000_000,
|
||||
DiskFreeBytes: 400_000_000_000,
|
||||
VolumeInfos: []*master_pb.VolumeInformationMessage{{Id: 1, Size: 590_000_000_000}},
|
||||
}},
|
||||
},
|
||||
&master_pb.DataNodeInfo{
|
||||
Id: "node-b",
|
||||
DiskInfos: map[string]*master_pb.DiskInfo{"hdd": {
|
||||
MaxVolumeCount: 10,
|
||||
VolumeInfos: []*master_pb.VolumeInformationMessage{
|
||||
{Id: 2, Size: 30_000_000_000},
|
||||
{Id: 3, Size: 5_000_000_000, RemoteStorageName: "s3"},
|
||||
},
|
||||
}},
|
||||
},
|
||||
))
|
||||
|
||||
topology, err := admin.GetClusterTopology()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(600_000_000_000+30_000_000_000), topology.DiskUsage)
|
||||
require.Equal(t, int64(1_000_000_000_000+10*30000*1024*1024), topology.DiskCapacity)
|
||||
require.Equal(t, int64(590_000_000_000+30_000_000_000+5_000_000_000), topology.TotalSize)
|
||||
}
|
||||
@@ -89,11 +89,16 @@ templ Admin(data dash.AdminData) {
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col mr-2">
|
||||
<div class="text-xs font-weight-bold text-warning text-uppercase mb-1">
|
||||
Total Size
|
||||
Disk Usage
|
||||
</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800">
|
||||
{formatBytes(data.TotalSize)}
|
||||
{formatBytes(data.DiskUsage)}
|
||||
</div>
|
||||
if data.DiskCapacity > 0 {
|
||||
<div class="text-xs text-muted">
|
||||
of {formatBytes(data.DiskCapacity)} ({fmt.Sprintf("%d%%", calculatePercent64(data.DiskUsage, data.DiskCapacity))})
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<i class="bi bi-device-hdd icon-2x text-gray-300"></i>
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/admin/dash"
|
||||
)
|
||||
|
||||
// The size card reports the bytes in use on the volume servers' disks, which
|
||||
// is what fills them, rather than the size of the data they hold.
|
||||
func TestAdminDashboardShowsDiskUsage(t *testing.T) {
|
||||
const tib = int64(1) << 40
|
||||
data := dash.AdminData{
|
||||
TotalSize: 276 * tib,
|
||||
DiskUsage: 348 * tib,
|
||||
DiskCapacity: 1000 * tib,
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := Admin(data).Render(context.Background(), &buf); err != nil {
|
||||
t.Fatalf("render: %v", err)
|
||||
}
|
||||
got := buf.String()
|
||||
for _, want := range []string{"Disk Usage", "348.0 TB", "of 1000.0 TB (34%)"} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Errorf("dashboard is missing %q", want)
|
||||
}
|
||||
}
|
||||
if strings.Contains(got, "276.0 TB") {
|
||||
t.Error("dashboard still shows the logical data size")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculatePercent64PetabyteScale(t *testing.T) {
|
||||
const pib = int64(1) << 50
|
||||
if got := calculatePercent64(100*pib, 200*pib); got != 50 {
|
||||
t.Errorf("100 of 200 PiB = %d%%, want 50%%", got)
|
||||
}
|
||||
}
|
||||
+452
-416
File diff suppressed because it is too large.
Load diff
@@ -169,7 +169,7 @@ templ ClusterVolumeServers(data dash.ClusterVolumeServersData) {
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="progress me-2" style="width: 60px; height: 16px;">
|
||||
<div class="progress-bar" role="progressbar"
|
||||
style={fmt.Sprintf("width: %d%%", calculatePercent(int(host.DiskUsage), int(host.DiskCapacity)))}>
|
||||
style={fmt.Sprintf("width: %d%%", calculatePercent64(host.DiskUsage, host.DiskCapacity))}>
|
||||
</div>
|
||||
</div>
|
||||
<small>{formatBytes(host.DiskUsage)}</small>
|
||||
|
||||
@@ -303,9 +303,9 @@ func ClusterVolumeServers(data dash.ClusterVolumeServersData) templ.Component {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var19 string
|
||||
templ_7745c5c3_Var19, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("width: %d%%", calculatePercent(int(host.DiskUsage), int(host.DiskCapacity))))
|
||||
templ_7745c5c3_Var19, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("width: %d%%", calculatePercent64(host.DiskUsage, host.DiskCapacity)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volume_servers.templ`, Line: 172, Col: 153}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volume_servers.templ`, Line: 172, Col: 145}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -46,6 +46,15 @@ func formatNumber(num int64) string {
|
||||
return result
|
||||
}
|
||||
|
||||
// calculatePercent64 is calculatePercent for byte counts, which overflow
|
||||
// integer arithmetic once multiplied by 100.
|
||||
func calculatePercent64(current, max int64) int {
|
||||
if max <= 0 {
|
||||
return 0
|
||||
}
|
||||
return int(min(float64(current)/float64(max)*100, 100))
|
||||
}
|
||||
|
||||
// calculatePercent calculates percentage for progress bars
|
||||
func calculatePercent(current, max int) int {
|
||||
if max == 0 {
|
||||
|
||||
Reference in new issue
Block a user