Use guest meminfo available for VM memory usage

This commit is contained in:
rcourtman 2025-10-12 11:03:56 +00:00
parent 7ba89da12c
commit 2163d6f5a8
4 changed files with 171 additions and 51 deletions

View file

@ -49,6 +49,10 @@ type VMMemoryRaw struct {
MemInfoUsed uint64 `json:"meminfoUsed,omitempty"` MemInfoUsed uint64 `json:"meminfoUsed,omitempty"`
MemInfoFree uint64 `json:"meminfoFree,omitempty"` MemInfoFree uint64 `json:"meminfoFree,omitempty"`
MemInfoTotal uint64 `json:"meminfoTotal,omitempty"` MemInfoTotal uint64 `json:"meminfoTotal,omitempty"`
MemInfoAvailable uint64 `json:"meminfoAvailable,omitempty"`
MemInfoBuffers uint64 `json:"meminfoBuffers,omitempty"`
MemInfoCached uint64 `json:"meminfoCached,omitempty"`
MemInfoShared uint64 `json:"meminfoShared,omitempty"`
Agent int `json:"agent,omitempty"` Agent int `json:"agent,omitempty"`
DerivedFromBall bool `json:"derivedFromBalloon,omitempty"` DerivedFromBall bool `json:"derivedFromBalloon,omitempty"`
} }

View file

@ -2527,10 +2527,28 @@ func (m *Monitor) pollVMsAndContainersEfficient(ctx context.Context, instanceNam
guestRaw.Balloon = detailedStatus.Balloon guestRaw.Balloon = detailedStatus.Balloon
guestRaw.BalloonMin = detailedStatus.BalloonMin guestRaw.BalloonMin = detailedStatus.BalloonMin
guestRaw.Agent = detailedStatus.Agent guestRaw.Agent = detailedStatus.Agent
memAvailable := uint64(0)
if detailedStatus.MemInfo != nil { if detailedStatus.MemInfo != nil {
guestRaw.MemInfoUsed = detailedStatus.MemInfo.Used guestRaw.MemInfoUsed = detailedStatus.MemInfo.Used
guestRaw.MemInfoFree = detailedStatus.MemInfo.Free guestRaw.MemInfoFree = detailedStatus.MemInfo.Free
guestRaw.MemInfoTotal = detailedStatus.MemInfo.Total guestRaw.MemInfoTotal = detailedStatus.MemInfo.Total
guestRaw.MemInfoAvailable = detailedStatus.MemInfo.Available
guestRaw.MemInfoBuffers = detailedStatus.MemInfo.Buffers
guestRaw.MemInfoCached = detailedStatus.MemInfo.Cached
guestRaw.MemInfoShared = detailedStatus.MemInfo.Shared
switch {
case detailedStatus.MemInfo.Available > 0:
memAvailable = detailedStatus.MemInfo.Available
memorySource = "meminfo-available"
case detailedStatus.MemInfo.Free > 0 ||
detailedStatus.MemInfo.Buffers > 0 ||
detailedStatus.MemInfo.Cached > 0:
memAvailable = detailedStatus.MemInfo.Free +
detailedStatus.MemInfo.Buffers +
detailedStatus.MemInfo.Cached
memorySource = "meminfo-derived"
}
} }
// Use actual disk I/O values from detailed status // Use actual disk I/O values from detailed status
@ -2547,10 +2565,16 @@ func (m *Monitor) pollVMsAndContainersEfficient(ctx context.Context, instanceNam
guestRaw.DerivedFromBall = false guestRaw.DerivedFromBall = false
} }
if detailedStatus.FreeMem > 0 && memTotal >= detailedStatus.FreeMem { switch {
case memAvailable > 0:
if memAvailable > memTotal {
memAvailable = memTotal
}
memUsed = memTotal - memAvailable
case detailedStatus.FreeMem > 0 && memTotal >= detailedStatus.FreeMem:
memUsed = memTotal - detailedStatus.FreeMem memUsed = memTotal - detailedStatus.FreeMem
memorySource = "status-freemem" memorySource = "status-freemem"
} else if detailedStatus.Mem > 0 { case detailedStatus.Mem > 0:
memUsed = detailedStatus.Mem memUsed = detailedStatus.Mem
memorySource = "status-mem" memorySource = "status-mem"
} }
@ -3180,10 +3204,28 @@ func (m *Monitor) pollVMsWithNodes(ctx context.Context, instanceName string, cli
guestRaw.Balloon = status.Balloon guestRaw.Balloon = status.Balloon
guestRaw.BalloonMin = status.BalloonMin guestRaw.BalloonMin = status.BalloonMin
guestRaw.Agent = status.Agent guestRaw.Agent = status.Agent
memAvailable := uint64(0)
if status.MemInfo != nil { if status.MemInfo != nil {
guestRaw.MemInfoUsed = status.MemInfo.Used guestRaw.MemInfoUsed = status.MemInfo.Used
guestRaw.MemInfoFree = status.MemInfo.Free guestRaw.MemInfoFree = status.MemInfo.Free
guestRaw.MemInfoTotal = status.MemInfo.Total guestRaw.MemInfoTotal = status.MemInfo.Total
guestRaw.MemInfoAvailable = status.MemInfo.Available
guestRaw.MemInfoBuffers = status.MemInfo.Buffers
guestRaw.MemInfoCached = status.MemInfo.Cached
guestRaw.MemInfoShared = status.MemInfo.Shared
switch {
case status.MemInfo.Available > 0:
memAvailable = status.MemInfo.Available
memorySource = "meminfo-available"
case status.MemInfo.Free > 0 ||
status.MemInfo.Buffers > 0 ||
status.MemInfo.Cached > 0:
memAvailable = status.MemInfo.Free +
status.MemInfo.Buffers +
status.MemInfo.Cached
memorySource = "meminfo-derived"
}
} }
// Use actual disk I/O values from detailed status // Use actual disk I/O values from detailed status
@ -3201,18 +3243,28 @@ func (m *Monitor) pollVMsWithNodes(ctx context.Context, instanceName string, cli
} }
// If we have free memory from guest agent, calculate actual usage // If we have free memory from guest agent, calculate actual usage
if status.FreeMem > 0 { switch {
case memAvailable > 0:
if memAvailable > memTotal {
memAvailable = memTotal
}
memUsed = memTotal - memAvailable
case status.FreeMem > 0:
// Guest agent reports free memory, so calculate used // Guest agent reports free memory, so calculate used
memUsed = memTotal - status.FreeMem memUsed = memTotal - status.FreeMem
memorySource = "status-freemem" memorySource = "status-freemem"
} else if status.Mem > 0 { case status.Mem > 0:
// No guest agent free memory data, but we have actual memory usage // No guest agent free memory data, but we have actual memory usage
// Use the reported memory usage from Proxmox // Use the reported memory usage from Proxmox
memUsed = status.Mem memUsed = status.Mem
memorySource = "status-mem" memorySource = "status-mem"
} else { default:
// No memory data available at all - show 0% usage // No memory data available at all - show 0% usage
memUsed = 0 memUsed = 0
memorySource = "status-unavailable"
}
if memUsed > memTotal {
memUsed = memTotal
} }
guestIPs, guestIfaces, guestOSName, guestOSVersion := fetchGuestAgentMetadata(ctx, client, instanceName, node.Node, vm.Name, vm.VMID, status) guestIPs, guestIfaces, guestOSName, guestOSVersion := fetchGuestAgentMetadata(ctx, client, instanceName, node.Node, vm.Name, vm.VMID, status)
@ -3456,6 +3508,25 @@ func (m *Monitor) pollVMsWithNodes(ctx context.Context, instanceName string, cli
} }
diskReadRate, diskWriteRate, netInRate, netOutRate := m.rateTracker.CalculateRates(guestID, currentMetrics) diskReadRate, diskWriteRate, netInRate, netOutRate := m.rateTracker.CalculateRates(guestID, currentMetrics)
memTotalBytes := clampToInt64(memTotal)
memUsedBytes := clampToInt64(memUsed)
if memTotalBytes > 0 && memUsedBytes > memTotalBytes {
memUsedBytes = memTotalBytes
}
memFreeBytes := memTotalBytes - memUsedBytes
if memFreeBytes < 0 {
memFreeBytes = 0
}
memory := models.Memory{
Total: memTotalBytes,
Used: memUsedBytes,
Free: memFreeBytes,
Usage: safePercentage(float64(memUsed), float64(memTotal)),
}
if guestRaw.Balloon > 0 {
memory.Balloon = clampToInt64(guestRaw.Balloon)
}
modelVM := models.VM{ modelVM := models.VM{
ID: guestID, ID: guestID,
VMID: vm.VMID, VMID: vm.VMID,
@ -3466,12 +3537,7 @@ func (m *Monitor) pollVMsWithNodes(ctx context.Context, instanceName string, cli
Type: "qemu", Type: "qemu",
CPU: cpuUsage, // Already in percentage CPU: cpuUsage, // Already in percentage
CPUs: vm.CPUs, CPUs: vm.CPUs,
Memory: models.Memory{ Memory: memory,
Total: int64(memTotal),
Used: int64(memUsed),
Free: int64(memTotal - memUsed),
Usage: safePercentage(float64(memUsed), float64(memTotal)),
},
Disk: models.Disk{ Disk: models.Disk{
Total: int64(diskTotal), Total: int64(diskTotal),
Used: int64(diskUsed), Used: int64(diskUsed),

View file

@ -163,21 +163,51 @@ func (m *Monitor) pollVMsWithNodesOptimized(ctx context.Context, instanceName st
guestRaw.Balloon = status.Balloon guestRaw.Balloon = status.Balloon
guestRaw.BalloonMin = status.BalloonMin guestRaw.BalloonMin = status.BalloonMin
guestRaw.Agent = status.Agent guestRaw.Agent = status.Agent
memAvailable := uint64(0)
if status.MemInfo != nil { if status.MemInfo != nil {
guestRaw.MemInfoUsed = status.MemInfo.Used guestRaw.MemInfoUsed = status.MemInfo.Used
guestRaw.MemInfoFree = status.MemInfo.Free guestRaw.MemInfoFree = status.MemInfo.Free
guestRaw.MemInfoTotal = status.MemInfo.Total guestRaw.MemInfoTotal = status.MemInfo.Total
guestRaw.MemInfoAvailable = status.MemInfo.Available
guestRaw.MemInfoBuffers = status.MemInfo.Buffers
guestRaw.MemInfoCached = status.MemInfo.Cached
guestRaw.MemInfoShared = status.MemInfo.Shared
switch {
case status.MemInfo.Available > 0:
memAvailable = status.MemInfo.Available
memorySource = "meminfo-available"
case status.MemInfo.Free > 0 ||
status.MemInfo.Buffers > 0 ||
status.MemInfo.Cached > 0:
memAvailable = status.MemInfo.Free +
status.MemInfo.Buffers +
status.MemInfo.Cached
memorySource = "meminfo-derived"
}
} }
if vmStatus.Balloon > 0 && vmStatus.Balloon < vmStatus.MaxMem { if vmStatus.Balloon > 0 && vmStatus.Balloon < vmStatus.MaxMem {
memTotal = vmStatus.Balloon memTotal = vmStatus.Balloon
guestRaw.DerivedFromBall = true guestRaw.DerivedFromBall = true
} }
if vmStatus.FreeMem > 0 { switch {
case memAvailable > 0:
if memAvailable > memTotal {
memAvailable = memTotal
}
memUsed = memTotal - memAvailable
case vmStatus.FreeMem > 0:
memUsed = memTotal - vmStatus.FreeMem memUsed = memTotal - vmStatus.FreeMem
memorySource = "status-freemem" memorySource = "status-freemem"
} else if vmStatus.Mem > 0 { case vmStatus.Mem > 0:
memUsed = vmStatus.Mem memUsed = vmStatus.Mem
memorySource = "status-mem" memorySource = "status-mem"
default:
memUsed = 0
memorySource = "status-unavailable"
}
if memUsed > memTotal {
memUsed = memTotal
} }
// Use actual disk I/O values from detailed status // Use actual disk I/O values from detailed status
diskReadBytes = int64(vmStatus.DiskRead) diskReadBytes = int64(vmStatus.DiskRead)
@ -461,14 +491,23 @@ func (m *Monitor) pollVMsWithNodesOptimized(ctx context.Context, instanceName st
diskStatusReason = "no-status" diskStatusReason = "no-status"
} }
memTotalBytes := clampToInt64(memTotal)
memUsedBytes := clampToInt64(memUsed)
if memTotalBytes > 0 && memUsedBytes > memTotalBytes {
memUsedBytes = memTotalBytes
}
memFreeBytes := memTotalBytes - memUsedBytes
if memFreeBytes < 0 {
memFreeBytes = 0
}
memory := models.Memory{ memory := models.Memory{
Total: int64(memTotal), Total: memTotalBytes,
Used: int64(memUsed), Used: memUsedBytes,
Free: int64(memTotal - memUsed), Free: memFreeBytes,
Usage: safePercentage(float64(memUsed), float64(memTotal)), Usage: safePercentage(float64(memUsed), float64(memTotal)),
} }
if vmStatus != nil && vmStatus.Balloon > 0 { if guestRaw.Balloon > 0 {
memory.Balloon = int64(vmStatus.Balloon) memory.Balloon = clampToInt64(guestRaw.Balloon)
} }
// Create VM model // Create VM model

View file

@ -1243,6 +1243,21 @@ type ZFSPoolDevice struct {
} }
// VMStatus represents detailed VM status // VMStatus represents detailed VM status
// VMMemInfo describes memory statistics reported by the guest agent.
// Proxmox surfaces guest /proc/meminfo values (in bytes). The available
// field is only present on newer agent versions, so we keep the raw
// components to reconstruct it when missing.
type VMMemInfo struct {
Total uint64 `json:"total,omitempty"`
Used uint64 `json:"used,omitempty"`
Free uint64 `json:"free,omitempty"`
Available uint64 `json:"available,omitempty"`
Buffers uint64 `json:"buffers,omitempty"`
Cached uint64 `json:"cached,omitempty"`
Shared uint64 `json:"shared,omitempty"`
}
// VMStatus represents detailed VM status returned by Proxmox.
type VMStatus struct { type VMStatus struct {
Status string `json:"status"` Status string `json:"status"`
CPU float64 `json:"cpu"` CPU float64 `json:"cpu"`
@ -1252,11 +1267,7 @@ type VMStatus struct {
Balloon uint64 `json:"balloon"` Balloon uint64 `json:"balloon"`
BalloonMin uint64 `json:"balloon_min"` BalloonMin uint64 `json:"balloon_min"`
FreeMem uint64 `json:"freemem"` FreeMem uint64 `json:"freemem"`
MemInfo *struct { MemInfo *VMMemInfo `json:"meminfo,omitempty"`
Used uint64 `json:"used"`
Free uint64 `json:"free"`
Total uint64 `json:"total"`
} `json:"meminfo,omitempty"`
Disk uint64 `json:"disk"` Disk uint64 `json:"disk"`
MaxDisk uint64 `json:"maxdisk"` MaxDisk uint64 `json:"maxdisk"`
DiskRead uint64 `json:"diskread"` DiskRead uint64 `json:"diskread"`