Files
hamlab/internal/metrics/arc.go
T
Mitchell-JohnsonandClaude Fable 5.1 b44faf3ac9 The ZFS tab shows the ARC, and names every caching tier a pool has or lacks
A Cache section between the pools and the schedules. One card for the ARC:
size against its ceiling with the target and floor, sparklines of size and
hit rate from the sampler's history, what it holds (MRU/MFU/dirty, data vs
metadata, compression, the ghost lists), the prefetcher and dbuf cache, the
memory-pressure counters, and the since-boot breakdown by demand/prefetch
and data/metadata. Then a card per pool for L2ARC, SLOG, special vdev,
dedup table and spares — the devices and their fill where they exist, and
where they don't, plainly none and what that means (the ZIL writing to the
pool's own vdevs, with how much it has written).

Live numbers come from the sampler, which reads arcstats every tick and
publishes zfs.arc.* sensors; the fuller picture is read on request, with
zpool list -Hpv and zfs get run without sudo and cached for thirty seconds.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 18:30:02 -07:00

114 lines
4.7 KiB
Go

package metrics
// The ZFS ARC — the read cache in RAM — read live from /proc/spl/kstat/zfs/arcstats on a
// machine that has the module loaded, and left out of the sample everywhere else. The kstat
// file is counters since boot; the hit rate here is over the sampling interval, which is the
// number that says whether the cache is doing its job *now*. The lifetime figures and the
// pool's caching tiers (L2ARC, SLOG, special vdevs) are the storage package's business.
import (
"strconv"
"strings"
"time"
)
const arcstatsPath = "/proc/spl/kstat/zfs/arcstats"
// ARC is the read cache as one sample sees it. Sizes are bytes.
type ARC struct {
Size uint64 `json:"size"`
Target uint64 `json:"target"` // c: what the ARC is currently allowed to hold
Min uint64 `json:"min"` // c_min: the floor it will not shrink below
Max uint64 `json:"max"` // c_max: the ceiling (zfs_arc_max, or half of RAM)
// what is in it
MRU uint64 `json:"mru"` // recently used once
MFU uint64 `json:"mfu"` // used more than once
Anon uint64 `json:"anon"` // in flight: dirty, not yet on disk
Data uint64 `json:"data"` // file contents
Metadata uint64 `json:"metadata"` // dnodes, indirect blocks, dbufs, bonus buffers
MetaUsed uint64 `json:"meta_used"`
Headers uint64 `json:"headers"`
Compressed uint64 `json:"compressed"`
Uncompressed uint64 `json:"uncompressed"`
MRUGhost uint64 `json:"mru_ghost"` // what it recently evicted and still remembers
MFUGhost uint64 `json:"mfu_ghost"`
// L2ARC, the cache devices (0 when there are none)
L2Size uint64 `json:"l2_size"`
L2ASize uint64 `json:"l2_asize"` // as stored on the device, after compression
// over this interval
ReadsPerSec float64 `json:"reads_per_sec"`
MissesPerSec float64 `json:"misses_per_sec"`
HitPct float64 `json:"hit_pct"` // of the interval's reads; an idle interval keeps the last one
HitPctKnown bool `json:"hit_pct_known"` // false until an interval has had any reads
L2HitPct float64 `json:"l2_hit_pct"`
// since boot
Hits uint64 `json:"hits"`
Misses uint64 `json:"misses"`
LifetimeHitPct float64 `json:"lifetime_hit_pct"`
// pressure
NoGrow bool `json:"no_grow"` // arc_no_grow: memory pressure is stopping it growing
Throttles uint64 `json:"throttles"` // memory_throttle_count since boot
Prunes uint64 `json:"prunes"` // arc_prune: metadata evictions asked of the filesystems
EvictSkip uint64 `json:"evict_skip"`
}
type arcPrev struct {
hits, misses, l2hits, l2misses uint64
hitPct float64
known bool
at time.Time
}
// readKstat parses a kstat table ("name type data" rows after two header lines).
func readKstat(p string) map[string]uint64 {
s := readStr(p)
if s == "" {
return nil
}
out := map[string]uint64{}
for _, line := range strings.Split(s, "\n") {
f := strings.Fields(line)
if len(f) != 3 || f[1] != "4" { // type 4 is KSTAT_DATA_UINT64
continue
}
v, err := strconv.ParseUint(f[2], 10, 64)
if err == nil {
out[f[0]] = v
}
}
return out
}
// arc reads the ARC; nil when this machine has no ZFS module loaded.
func (c *Collector) arc(now time.Time) *ARC {
k := readKstat(arcstatsPath)
if k == nil {
return nil
}
a := &ARC{Size: k["size"], Target: k["c"], Min: k["c_min"], Max: k["c_max"],
MRU: k["mru_size"], MFU: k["mfu_size"], Anon: k["anon_size"], Data: k["data_size"], Metadata: k["metadata_size"],
MetaUsed: k["arc_meta_used"], Headers: k["hdr_size"], Compressed: k["compressed_size"], Uncompressed: k["uncompressed_size"],
MRUGhost: k["mru_ghost_size"], MFUGhost: k["mfu_ghost_size"], L2Size: k["l2_size"], L2ASize: k["l2_asize"],
Hits: k["hits"], Misses: k["misses"], NoGrow: k["arc_no_grow"] != 0, Throttles: k["memory_throttle_count"], Prunes: k["arc_prune"], EvictSkip: k["evict_skip"]}
if n := a.Hits + a.Misses; n > 0 {
a.LifetimeHitPct = round1(float64(a.Hits) / float64(n) * 100)
}
p := c.prevARC
if !p.at.IsZero() && a.Hits >= p.hits && a.Misses >= p.misses {
if dt := now.Sub(p.at).Seconds(); dt > 0 {
hits, misses := a.Hits-p.hits, a.Misses-p.misses
a.ReadsPerSec = round1(float64(hits+misses) / dt)
a.MissesPerSec = round1(float64(misses) / dt)
if hits+misses > 0 {
p.hitPct, p.known = round1(float64(hits)/float64(hits+misses)*100), true
}
if l2 := (k["l2_hits"] - p.l2hits) + (k["l2_misses"] - p.l2misses); l2 > 0 && k["l2_hits"] >= p.l2hits {
a.L2HitPct = round1(float64(k["l2_hits"]-p.l2hits) / float64(l2) * 100)
}
}
}
a.HitPct, a.HitPctKnown = p.hitPct, p.known
c.prevARC = arcPrev{hits: a.Hits, misses: a.Misses, l2hits: k["l2_hits"], l2misses: k["l2_misses"], hitPct: p.hitPct, known: p.known, at: now}
return a
}