F1: read cgroup memory limit, not host RAM (restores deploy OOM guard)
/api/system/info reported the Proxmox host's 16GB (the controller container reads host /proc/meminfo with no lxcfs), defeating the deploy memory-headroom hard-block (deploy.go uses GetMemoryMB). readMemInfo now prefers the cgroup memory limit (v2 memory.max / v1 memory.limit_in_bytes; sentinels = unlimited) when finite and below the host total; used = memory.current/usage_in_bytes. Test info_cgroup_test.go (cgroup v2 cap wins, v2 max sentinel, v1 unlimited, v1 finite) — fails on pre-fix code.
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
@@ -77,6 +78,9 @@ func GetMemoryMB() (totalMB, usedMB int, err error) {
|
||||
return int(info.TotalMemMB), int(info.UsedMemMB), nil
|
||||
}
|
||||
|
||||
// cgroupRoot is the cgroup mount point. Overridable in tests.
|
||||
var cgroupRoot = "/sys/fs/cgroup"
|
||||
|
||||
func readMemInfo(info *SystemInfo) {
|
||||
f, err := os.Open("/proc/meminfo")
|
||||
if err != nil {
|
||||
@@ -100,16 +104,80 @@ func readMemInfo(info *SystemInfo) {
|
||||
}
|
||||
}
|
||||
|
||||
if totalKB > 0 {
|
||||
info.TotalMemMB = totalKB / 1024
|
||||
info.AvailMemMB = availKB / 1024
|
||||
info.UsedMemMB = info.TotalMemMB - info.AvailMemMB
|
||||
info.MemPercent = float64(info.UsedMemMB) / float64(info.TotalMemMB) * 100
|
||||
debugf("[DEBUG] [system] readMemInfo: totalKB=%d availKB=%d → total=%dMB avail=%dMB used=%dMB (%.1f%%)",
|
||||
totalKB, availKB, info.TotalMemMB, info.AvailMemMB, info.UsedMemMB, info.MemPercent)
|
||||
} else {
|
||||
if totalKB == 0 {
|
||||
debugf("[DEBUG] [system] readMemInfo: could not parse MemTotal from /proc/meminfo")
|
||||
return
|
||||
}
|
||||
|
||||
info.TotalMemMB = totalKB / 1024
|
||||
info.AvailMemMB = availKB / 1024
|
||||
info.UsedMemMB = info.TotalMemMB - info.AvailMemMB
|
||||
|
||||
// F1: the controller runs as a Docker container inside an LXC. /proc/meminfo reports the HOST's
|
||||
// RAM (no lxcfs in the container), which massively overstates the guest's real ceiling and defeats
|
||||
// the deploy memory-headroom guard. Prefer the cgroup memory LIMIT when it is finite and below the
|
||||
// host total — that is the amount this guest can actually use. Fall back to /proc/meminfo otherwise.
|
||||
if limitMB, ok := readCgroupMemLimitMB(cgroupRoot); ok && limitMB > 0 && limitMB < info.TotalMemMB {
|
||||
info.TotalMemMB = limitMB
|
||||
if curMB, okC := readCgroupMemCurrentMB(cgroupRoot); okC && curMB <= limitMB {
|
||||
info.UsedMemMB = curMB
|
||||
} else if info.UsedMemMB > limitMB {
|
||||
info.UsedMemMB = limitMB
|
||||
}
|
||||
info.AvailMemMB = info.TotalMemMB - info.UsedMemMB
|
||||
debugf("[DEBUG] [system] readMemInfo: using cgroup limit=%dMB (host total was %dKB) → used=%dMB avail=%dMB",
|
||||
limitMB, totalKB, info.UsedMemMB, info.AvailMemMB)
|
||||
}
|
||||
|
||||
if info.TotalMemMB > 0 {
|
||||
info.MemPercent = float64(info.UsedMemMB) / float64(info.TotalMemMB) * 100
|
||||
}
|
||||
debugf("[DEBUG] [system] readMemInfo: totalKB=%d availKB=%d → total=%dMB avail=%dMB used=%dMB (%.1f%%)",
|
||||
totalKB, availKB, info.TotalMemMB, info.AvailMemMB, info.UsedMemMB, info.MemPercent)
|
||||
}
|
||||
|
||||
// readCgroupMemLimitMB returns the cgroup memory limit in MB. It tries cgroup v2 (memory.max) first,
|
||||
// then v1 (memory/memory.limit_in_bytes). A sentinel ("max" on v2, or a near-uint64-max value on v1)
|
||||
// means "unlimited" → ok=false so the caller keeps the /proc/meminfo value.
|
||||
func readCgroupMemLimitMB(root string) (mb uint64, ok bool) {
|
||||
// cgroup v2
|
||||
if b, err := os.ReadFile(filepath.Join(root, "memory.max")); err == nil {
|
||||
s := strings.TrimSpace(string(b))
|
||||
if s == "max" {
|
||||
return 0, false
|
||||
}
|
||||
if v, err := strconv.ParseUint(s, 10, 64); err == nil && v > 0 {
|
||||
return v / (1024 * 1024), true
|
||||
}
|
||||
}
|
||||
// cgroup v1
|
||||
if b, err := os.ReadFile(filepath.Join(root, "memory", "memory.limit_in_bytes")); err == nil {
|
||||
s := strings.TrimSpace(string(b))
|
||||
if v, err := strconv.ParseUint(s, 10, 64); err == nil && v > 0 {
|
||||
// v1 "unlimited" is a huge page-aligned value near uint64 max; treat >= 1 PiB as unlimited.
|
||||
if v >= (1 << 50) {
|
||||
return 0, false
|
||||
}
|
||||
return v / (1024 * 1024), true
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// readCgroupMemCurrentMB returns the cgroup current memory usage in MB (v2 memory.current, v1
|
||||
// memory/memory.usage_in_bytes). ok=false if unreadable.
|
||||
func readCgroupMemCurrentMB(root string) (mb uint64, ok bool) {
|
||||
for _, p := range []string{
|
||||
filepath.Join(root, "memory.current"),
|
||||
filepath.Join(root, "memory", "memory.usage_in_bytes"),
|
||||
} {
|
||||
if b, err := os.ReadFile(p); err == nil {
|
||||
if v, err := strconv.ParseUint(strings.TrimSpace(string(b)), 10, 64); err == nil {
|
||||
return v / (1024 * 1024), true
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// parseMemLine extracts the kB value from a /proc/meminfo line like "MemTotal: 16384000 kB"
|
||||
|
||||
Reference in New Issue
Block a user