Refs #533: add total-minus-used memory fallback
This commit is contained in:
parent
5ce47a72ec
commit
32421b36b8
5 changed files with 148 additions and 39 deletions
|
|
@ -19,6 +19,7 @@ type NodeMemoryRaw struct {
|
||||||
Cached uint64 `json:"cached"`
|
Cached uint64 `json:"cached"`
|
||||||
Shared uint64 `json:"shared"`
|
Shared uint64 `json:"shared"`
|
||||||
EffectiveAvailable uint64 `json:"effectiveAvailable"`
|
EffectiveAvailable uint64 `json:"effectiveAvailable"`
|
||||||
|
TotalMinusUsed uint64 `json:"totalMinusUsed,omitempty"`
|
||||||
FallbackTotal uint64 `json:"fallbackTotal,omitempty"`
|
FallbackTotal uint64 `json:"fallbackTotal,omitempty"`
|
||||||
FallbackUsed uint64 `json:"fallbackUsed,omitempty"`
|
FallbackUsed uint64 `json:"fallbackUsed,omitempty"`
|
||||||
FallbackFree uint64 `json:"fallbackFree,omitempty"`
|
FallbackFree uint64 `json:"fallbackFree,omitempty"`
|
||||||
|
|
@ -39,22 +40,23 @@ type NodeMemorySnapshot struct {
|
||||||
|
|
||||||
// VMMemoryRaw captures both the listing and detailed status memory fields for a VM/CT.
|
// VMMemoryRaw captures both the listing and detailed status memory fields for a VM/CT.
|
||||||
type VMMemoryRaw struct {
|
type VMMemoryRaw struct {
|
||||||
ListingMem uint64 `json:"listingMem"`
|
ListingMem uint64 `json:"listingMem"`
|
||||||
ListingMaxMem uint64 `json:"listingMaxmem"`
|
ListingMaxMem uint64 `json:"listingMaxmem"`
|
||||||
StatusMem uint64 `json:"statusMem,omitempty"`
|
StatusMem uint64 `json:"statusMem,omitempty"`
|
||||||
StatusFreeMem uint64 `json:"statusFreemem,omitempty"`
|
StatusFreeMem uint64 `json:"statusFreemem,omitempty"`
|
||||||
StatusMaxMem uint64 `json:"statusMaxmem,omitempty"`
|
StatusMaxMem uint64 `json:"statusMaxmem,omitempty"`
|
||||||
Balloon uint64 `json:"balloon,omitempty"`
|
Balloon uint64 `json:"balloon,omitempty"`
|
||||||
BalloonMin uint64 `json:"balloonMin,omitempty"`
|
BalloonMin uint64 `json:"balloonMin,omitempty"`
|
||||||
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"`
|
MemInfoAvailable uint64 `json:"meminfoAvailable,omitempty"`
|
||||||
MemInfoBuffers uint64 `json:"meminfoBuffers,omitempty"`
|
MemInfoBuffers uint64 `json:"meminfoBuffers,omitempty"`
|
||||||
MemInfoCached uint64 `json:"meminfoCached,omitempty"`
|
MemInfoCached uint64 `json:"meminfoCached,omitempty"`
|
||||||
MemInfoShared uint64 `json:"meminfoShared,omitempty"`
|
MemInfoShared uint64 `json:"meminfoShared,omitempty"`
|
||||||
Agent int `json:"agent,omitempty"`
|
MemInfoTotalMinusUsed uint64 `json:"meminfoTotalMinusUsed,omitempty"`
|
||||||
DerivedFromBall bool `json:"derivedFromBalloon,omitempty"`
|
Agent int `json:"agent,omitempty"`
|
||||||
|
DerivedFromBall bool `json:"derivedFromBalloon,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GuestMemorySnapshot records the memory calculation for a guest (VM/LXC).
|
// GuestMemorySnapshot records the memory calculation for a guest (VM/LXC).
|
||||||
|
|
|
||||||
|
|
@ -2055,12 +2055,37 @@ func (m *Monitor) pollPVEInstance(ctx context.Context, instanceName string, clie
|
||||||
var actualUsed uint64
|
var actualUsed uint64
|
||||||
effectiveAvailable := nodeInfo.Memory.EffectiveAvailable()
|
effectiveAvailable := nodeInfo.Memory.EffectiveAvailable()
|
||||||
usedRRDFallback := false
|
usedRRDFallback := false
|
||||||
|
componentAvailable := nodeInfo.Memory.Free
|
||||||
|
if nodeInfo.Memory.Buffers > 0 {
|
||||||
|
if math.MaxUint64-componentAvailable < nodeInfo.Memory.Buffers {
|
||||||
|
componentAvailable = math.MaxUint64
|
||||||
|
} else {
|
||||||
|
componentAvailable += nodeInfo.Memory.Buffers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if nodeInfo.Memory.Cached > 0 {
|
||||||
|
if math.MaxUint64-componentAvailable < nodeInfo.Memory.Cached {
|
||||||
|
componentAvailable = math.MaxUint64
|
||||||
|
} else {
|
||||||
|
componentAvailable += nodeInfo.Memory.Cached
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if nodeInfo.Memory.Total > 0 && componentAvailable > nodeInfo.Memory.Total {
|
||||||
|
componentAvailable = nodeInfo.Memory.Total
|
||||||
|
}
|
||||||
|
|
||||||
if effectiveAvailable == 0 &&
|
availableFromUsed := uint64(0)
|
||||||
nodeInfo.Memory.Available == 0 &&
|
if nodeInfo.Memory.Total > 0 && nodeInfo.Memory.Used > 0 && nodeInfo.Memory.Total >= nodeInfo.Memory.Used {
|
||||||
|
availableFromUsed = nodeInfo.Memory.Total - nodeInfo.Memory.Used
|
||||||
|
}
|
||||||
|
nodeSnapshotRaw.TotalMinusUsed = availableFromUsed
|
||||||
|
|
||||||
|
missingCacheMetrics := nodeInfo.Memory.Available == 0 &&
|
||||||
nodeInfo.Memory.Avail == 0 &&
|
nodeInfo.Memory.Avail == 0 &&
|
||||||
nodeInfo.Memory.Buffers == 0 &&
|
nodeInfo.Memory.Buffers == 0 &&
|
||||||
nodeInfo.Memory.Cached == 0 {
|
nodeInfo.Memory.Cached == 0
|
||||||
|
|
||||||
|
if effectiveAvailable == 0 && missingCacheMetrics {
|
||||||
if memAvail, err := m.getNodeRRDMemAvailable(ctx, client, node.Node); err == nil && memAvail > 0 {
|
if memAvail, err := m.getNodeRRDMemAvailable(ctx, client, node.Node); err == nil && memAvail > 0 {
|
||||||
effectiveAvailable = memAvail
|
effectiveAvailable = memAvail
|
||||||
usedRRDFallback = true
|
usedRRDFallback = true
|
||||||
|
|
@ -2073,6 +2098,21 @@ func (m *Monitor) pollPVEInstance(ctx context.Context, instanceName string, clie
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const totalMinusUsedGapTolerance uint64 = 16 * 1024 * 1024
|
||||||
|
gapGreaterThanComponents := false
|
||||||
|
if availableFromUsed > componentAvailable {
|
||||||
|
gap := availableFromUsed - componentAvailable
|
||||||
|
if componentAvailable == 0 || gap >= totalMinusUsedGapTolerance {
|
||||||
|
gapGreaterThanComponents = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
derivedFromTotalMinusUsed := !usedRRDFallback &&
|
||||||
|
missingCacheMetrics &&
|
||||||
|
availableFromUsed > 0 &&
|
||||||
|
gapGreaterThanComponents &&
|
||||||
|
effectiveAvailable == availableFromUsed
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case effectiveAvailable > 0 && effectiveAvailable <= nodeInfo.Memory.Total:
|
case effectiveAvailable > 0 && effectiveAvailable <= nodeInfo.Memory.Total:
|
||||||
// Prefer available/avail fields or derived buffers+cache values when present.
|
// Prefer available/avail fields or derived buffers+cache values when present.
|
||||||
|
|
@ -2087,29 +2127,36 @@ func (m *Monitor) pollPVEInstance(ctx context.Context, instanceName string, clie
|
||||||
Uint64("effectiveAvailable", effectiveAvailable).
|
Uint64("effectiveAvailable", effectiveAvailable).
|
||||||
Uint64("actualUsed", actualUsed).
|
Uint64("actualUsed", actualUsed).
|
||||||
Float64("usage", safePercentage(float64(actualUsed), float64(nodeInfo.Memory.Total)))
|
Float64("usage", safePercentage(float64(actualUsed), float64(nodeInfo.Memory.Total)))
|
||||||
|
|
||||||
if usedRRDFallback {
|
if usedRRDFallback {
|
||||||
logCtx.Msg("Node memory: using RRD memavailable fallback (excludes reclaimable cache)")
|
logCtx.Msg("Node memory: using RRD memavailable fallback (excludes reclaimable cache)")
|
||||||
nodeMemorySource = "rrd-memavailable"
|
nodeMemorySource = "rrd-memavailable"
|
||||||
nodeFallbackReason = "rrd-memavailable"
|
nodeFallbackReason = "rrd-memavailable"
|
||||||
nodeSnapshotRaw.FallbackCalculated = true
|
nodeSnapshotRaw.FallbackCalculated = true
|
||||||
nodeSnapshotRaw.ProxmoxMemorySource = "rrd-memavailable"
|
nodeSnapshotRaw.ProxmoxMemorySource = "rrd-memavailable"
|
||||||
} else {
|
} else if nodeInfo.Memory.Available > 0 {
|
||||||
switch {
|
logCtx.Msg("Node memory: using available field (excludes reclaimable cache)")
|
||||||
case nodeInfo.Memory.Available > 0:
|
nodeMemorySource = "available-field"
|
||||||
logCtx.Msg("Node memory: using available field (excludes reclaimable cache)")
|
} else if nodeInfo.Memory.Avail > 0 {
|
||||||
nodeMemorySource = "available-field"
|
logCtx.Msg("Node memory: using avail field (excludes reclaimable cache)")
|
||||||
case nodeInfo.Memory.Avail > 0:
|
nodeMemorySource = "avail-field"
|
||||||
logCtx.Msg("Node memory: using avail field (excludes reclaimable cache)")
|
} else if derivedFromTotalMinusUsed {
|
||||||
nodeMemorySource = "avail-field"
|
logCtx.
|
||||||
default:
|
Uint64("availableFromUsed", availableFromUsed).
|
||||||
logCtx.
|
Uint64("reportedFree", nodeInfo.Memory.Free).
|
||||||
Uint64("free", nodeInfo.Memory.Free).
|
Msg("Node memory: derived available from total-used gap (cache fields missing)")
|
||||||
Uint64("buffers", nodeInfo.Memory.Buffers).
|
nodeMemorySource = "derived-total-minus-used"
|
||||||
Uint64("cached", nodeInfo.Memory.Cached).
|
if nodeFallbackReason == "" {
|
||||||
Msg("Node memory: derived available from free+buffers+cached (excludes reclaimable cache)")
|
nodeFallbackReason = "node-status-total-minus-used"
|
||||||
nodeMemorySource = "derived-free-buffers-cached"
|
|
||||||
}
|
}
|
||||||
|
nodeSnapshotRaw.FallbackCalculated = true
|
||||||
|
nodeSnapshotRaw.ProxmoxMemorySource = "node-status-total-minus-used"
|
||||||
|
} else {
|
||||||
|
logCtx.
|
||||||
|
Uint64("free", nodeInfo.Memory.Free).
|
||||||
|
Uint64("buffers", nodeInfo.Memory.Buffers).
|
||||||
|
Uint64("cached", nodeInfo.Memory.Cached).
|
||||||
|
Msg("Node memory: derived available from free+buffers+cached (excludes reclaimable cache)")
|
||||||
|
nodeMemorySource = "derived-free-buffers-cached"
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
// Fallback to traditional used memory if no cache-aware data is exposed
|
// Fallback to traditional used memory if no cache-aware data is exposed
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package monitoring
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
@ -173,6 +174,34 @@ func (m *Monitor) pollVMsWithNodesOptimized(ctx context.Context, instanceName st
|
||||||
guestRaw.MemInfoBuffers = status.MemInfo.Buffers
|
guestRaw.MemInfoBuffers = status.MemInfo.Buffers
|
||||||
guestRaw.MemInfoCached = status.MemInfo.Cached
|
guestRaw.MemInfoCached = status.MemInfo.Cached
|
||||||
guestRaw.MemInfoShared = status.MemInfo.Shared
|
guestRaw.MemInfoShared = status.MemInfo.Shared
|
||||||
|
componentAvailable := status.MemInfo.Free
|
||||||
|
if status.MemInfo.Buffers > 0 {
|
||||||
|
if math.MaxUint64-componentAvailable < status.MemInfo.Buffers {
|
||||||
|
componentAvailable = math.MaxUint64
|
||||||
|
} else {
|
||||||
|
componentAvailable += status.MemInfo.Buffers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if status.MemInfo.Cached > 0 {
|
||||||
|
if math.MaxUint64-componentAvailable < status.MemInfo.Cached {
|
||||||
|
componentAvailable = math.MaxUint64
|
||||||
|
} else {
|
||||||
|
componentAvailable += status.MemInfo.Cached
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if status.MemInfo.Total > 0 && componentAvailable > status.MemInfo.Total {
|
||||||
|
componentAvailable = status.MemInfo.Total
|
||||||
|
}
|
||||||
|
|
||||||
|
availableFromUsed := uint64(0)
|
||||||
|
if status.MemInfo.Total > 0 && status.MemInfo.Used > 0 && status.MemInfo.Total >= status.MemInfo.Used {
|
||||||
|
availableFromUsed = status.MemInfo.Total - status.MemInfo.Used
|
||||||
|
guestRaw.MemInfoTotalMinusUsed = availableFromUsed
|
||||||
|
}
|
||||||
|
|
||||||
|
missingCacheMetrics := status.MemInfo.Available == 0 &&
|
||||||
|
status.MemInfo.Buffers == 0 &&
|
||||||
|
status.MemInfo.Cached == 0
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case status.MemInfo.Available > 0:
|
case status.MemInfo.Available > 0:
|
||||||
|
|
@ -186,6 +215,17 @@ func (m *Monitor) pollVMsWithNodesOptimized(ctx context.Context, instanceName st
|
||||||
status.MemInfo.Cached
|
status.MemInfo.Cached
|
||||||
memorySource = "meminfo-derived"
|
memorySource = "meminfo-derived"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if memAvailable == 0 && availableFromUsed > 0 && missingCacheMetrics {
|
||||||
|
const vmTotalMinusUsedGapTolerance uint64 = 4 * 1024 * 1024
|
||||||
|
if availableFromUsed > componentAvailable {
|
||||||
|
gap := availableFromUsed - componentAvailable
|
||||||
|
if componentAvailable == 0 || gap >= vmTotalMinusUsedGapTolerance {
|
||||||
|
memAvailable = availableFromUsed
|
||||||
|
memorySource = "meminfo-total-minus-used"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if vmStatus.Balloon > 0 && vmStatus.Balloon < vmStatus.MaxMem {
|
if vmStatus.Balloon > 0 && vmStatus.Balloon < vmStatus.MaxMem {
|
||||||
memTotal = vmStatus.Balloon
|
memTotal = vmStatus.Balloon
|
||||||
|
|
|
||||||
|
|
@ -447,6 +447,13 @@ func (m *MemoryStatus) EffectiveAvailable() uint64 {
|
||||||
}
|
}
|
||||||
|
|
||||||
derived := m.Free + m.Buffers + m.Cached
|
derived := m.Free + m.Buffers + m.Cached
|
||||||
|
if m.Total > 0 && m.Used > 0 && m.Total >= m.Used {
|
||||||
|
availableFromUsed := m.Total - m.Used
|
||||||
|
if availableFromUsed > derived {
|
||||||
|
derived = availableFromUsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if derived == 0 {
|
if derived == 0 {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -168,9 +168,22 @@ func TestMemoryStatusEffectiveAvailable_RegressionIssue435(t *testing.T) {
|
||||||
Cached: 4294967296, // 4GB
|
Cached: 4294967296, // 4GB
|
||||||
},
|
},
|
||||||
wantAvailable: 6979321856, // Free + Buffers + Cached = ~6.5GB
|
wantAvailable: 6979321856, // Free + Buffers + Cached = ~6.5GB
|
||||||
wantUsedPct: 58.4, // Should be ~58%, not ~80%
|
wantUsedPct: 58.4, // Should be ~58%, not ~80%
|
||||||
description: "When available/avail missing, derive from free+buffers+cached",
|
description: "When available/avail missing, derive from free+buffers+cached",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "proxmox 8.4 hides cache fields - derive from total-minus-used gap",
|
||||||
|
status: MemoryStatus{
|
||||||
|
Total: 134794743808, // ~125.6GB
|
||||||
|
Used: 107351023616, // ~100GB actual usage
|
||||||
|
Free: 6471057408, // ~6GB bare free reported
|
||||||
|
Buffers: 0,
|
||||||
|
Cached: 0,
|
||||||
|
},
|
||||||
|
wantAvailable: 27443720192, // total - used => ~25.6GB reclaimable (free + cache)
|
||||||
|
wantUsedPct: 79.6, // Matches Proxmox node dashboard
|
||||||
|
description: "Proxmox 8.4 stops reporting buffers/cached; use total-used gap to recover cache-aware metric",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "issue #435 specific case - 86% vs 42% real usage",
|
name: "issue #435 specific case - 86% vs 42% real usage",
|
||||||
status: MemoryStatus{
|
status: MemoryStatus{
|
||||||
|
|
@ -190,9 +203,9 @@ func TestMemoryStatusEffectiveAvailable_RegressionIssue435(t *testing.T) {
|
||||||
Used: 6871947674,
|
Used: 6871947674,
|
||||||
Free: 0, // All fields missing
|
Free: 0, // All fields missing
|
||||||
},
|
},
|
||||||
wantAvailable: 0,
|
wantAvailable: 1717986918, // Derived from total - used
|
||||||
wantUsedPct: 80.0, // Falls back to cache-inclusive calculation
|
wantUsedPct: 80.0, // Still aligns with cache-inclusive calculation when nothing else reported
|
||||||
description: "When all cache fields missing, EffectiveAvailable returns 0",
|
description: "When all cache fields missing, fall back to total-used gap instead of zero",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue