Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 45 additions & 2 deletions cloud/hostmetrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,8 +221,10 @@ type memInfo struct {

// readMemInfo parses /proc/meminfo. Used is MemTotal-MemAvailable (the kernel's
// own pressure estimate, which discounts reclaimable page cache), falling back
// to free+buffers+cached on kernels too old for MemAvailable. Swap used is
// SwapTotal-SwapFree. meminfo reports kibibytes.
// to free+buffers+cached on kernels too old for MemAvailable. The shrinkable
// part of the ZFS ARC is discounted too: the kernel counts it as used, but it
// is cache that ZFS hands back under pressure. Swap used is SwapTotal-SwapFree.
// meminfo reports kibibytes.
func readMemInfo() memInfo {
f, err := os.Open(filepath.Join(procDir, "meminfo"))
if err != nil {
Expand All @@ -249,6 +251,7 @@ func readMemInfo() memInfo {
} else {
used = total - (vals["MemFree"] + vals["Buffers"] + vals["Cached"])
}
used -= zfsARCShrinkableBytes() / kib
if used < 0 {
used = 0
}
Expand Down Expand Up @@ -282,6 +285,46 @@ func parseMeminfoLine(line string) (key string, kb int64, ok bool) {
return line[:colon], v, true
}

// ---- /proc/spl/kstat/zfs/arcstats (ZFS ARC) -------------------------------

// zfsARCShrinkableBytes is how much of the ZFS ARC could be reclaimed right
// now: its current size above its floor (c_min), the same figure htop and
// btop discount. The ARC lives outside the page cache, so MemAvailable counts
// it as used; on a ZFS host (Proxmox, TrueNAS) that alone can read as memory
// pressure. Zero when ZFS isn't loaded or the stats are unreadable.
func zfsARCShrinkableBytes() int64 {
f, err := os.Open(filepath.Join(procDir, "spl", "kstat", "zfs", "arcstats"))
if err != nil {
return 0
}
defer func() { _ = f.Close() }()

var size, cMin int64
var haveSize, haveMin bool
sc := bufio.NewScanner(f)
for sc.Scan() {
// Data lines are "name type value", e.g. "size 4 2408644504".
fields := strings.Fields(sc.Text())
if len(fields) != 3 {
continue
}
v, err := strconv.ParseInt(fields[2], 10, 64)
if err != nil {
continue
}
switch fields[0] {
case "size":
size, haveSize = v, true
case "c_min":
cMin, haveMin = v, true
}
}
if !haveSize || !haveMin || size <= cMin {
return 0
}
return size - cMin
}

// ---- /proc/loadavg (load) -------------------------------------------------

// readLoadAvg parses /proc/loadavg: "0.52 0.58 0.59 1/843 12345".
Expand Down
73 changes: 73 additions & 0 deletions cloud/hostmetrics_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package cloud

import (
"os"
"path/filepath"
"testing"
)

func writeProcFile(t *testing.T, root, rel, content string) {
t.Helper()
p := filepath.Join(root, rel)
if err := os.MkdirAll(filepath.Dir(p), 0o750); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(p, []byte(content), 0o600); err != nil {
t.Fatal(err)
}
}

func useProcDir(t *testing.T, dir string) {
t.Helper()
prev := procDir
procDir = dir
t.Cleanup(func() { procDir = prev })
}

const testMeminfo = `MemTotal: 16000000 kB
MemFree: 2000000 kB
MemAvailable: 4000000 kB
SwapTotal: 0 kB
SwapFree: 0 kB
`

func TestReadMemInfoWithoutZFS(t *testing.T) {
dir := t.TempDir()
writeProcFile(t, dir, "meminfo", testMeminfo)
useProcDir(t, dir)

if got, want := readMemInfo().usedBytes, int64(12000000*1024); got != want {
t.Fatalf("used = %d, want %d", got, want)
}
}

func TestReadMemInfoDiscountsShrinkableARC(t *testing.T) {
dir := t.TempDir()
writeProcFile(t, dir, "meminfo", testMeminfo)
writeProcFile(t, dir, "spl/kstat/zfs/arcstats", `13 1 0x01 123 33456 1234 5678
name type data
hits 4 123456
size 4 6144000000
c_min 4 1024000000
c_max 4 8192000000
`)
useProcDir(t, dir)

// 12,000,000 kB used minus (6,144,000,000 - 1,024,000,000) B / 1024 = 5,000,000 kB.
if got, want := readMemInfo().usedBytes, int64(7000000*1024); got != want {
t.Fatalf("used = %d, want %d", got, want)
}
}

func TestZFSARCShrinkableAtFloor(t *testing.T) {
dir := t.TempDir()
writeProcFile(t, dir, "spl/kstat/zfs/arcstats", `name type data
size 4 1000
c_min 4 2000
`)
useProcDir(t, dir)

if got := zfsARCShrinkableBytes(); got != 0 {
t.Fatalf("shrinkable = %d, want 0 when the ARC is at its floor", got)
}
}
Loading