ZFS tab: pools, caching tiers and writes full width; write and drain rates measured between real updates
A txg records its bytes only when it syncs and the mover reports every 2 s, so a per-sample delta read 0 most of the time. Rates are now taken over the gap between syncs (or mover updates) and held until the next one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
86cfcd2bad
commit
50768e2d94
@@ -109,10 +109,14 @@ type zfsWritePrev struct {
|
|||||||
at time.Time
|
at time.Time
|
||||||
delay uint64
|
delay uint64
|
||||||
overMax uint64
|
overMax uint64
|
||||||
txg map[string]uint64 // pool -> highest synced txg counted
|
txg map[string]uint64 // pool -> highest synced txg counted
|
||||||
logical map[string]uint64 // pool -> sum of objset nwritten
|
txgAt map[string]time.Time // pool -> when that txg was first seen
|
||||||
drained map[string]uint64 // landing branch -> moved + partial bytes
|
txgRate map[string]float64 // pool -> bytes/s over the last gap between syncs
|
||||||
landing map[string]Landing // branch -> last walk
|
logical map[string]uint64 // pool -> sum of objset nwritten
|
||||||
|
drained map[string]uint64 // landing branch -> moved + partial bytes, at the mover's last update
|
||||||
|
drainedAt map[string]int64 // landing branch -> that update's time, unix ms
|
||||||
|
drainRate map[string]float64 // landing branch -> bytes/s between its last two updates
|
||||||
|
landing map[string]Landing // branch -> last walk
|
||||||
landingAt time.Time
|
landingAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -378,6 +382,7 @@ func (c *Collector) zfsWrites(now time.Time, arc *ARC, disks []Disk) *ZFSWrites
|
|||||||
p.delay, p.overMax = tx["dmu_tx_dirty_delay"], tx["dmu_tx_dirty_over_max"]
|
p.delay, p.overMax = tx["dmu_tx_dirty_delay"], tx["dmu_tx_dirty_over_max"]
|
||||||
if p.txg == nil {
|
if p.txg == nil {
|
||||||
p.txg, p.logical, p.drained, p.landing = map[string]uint64{}, map[string]uint64{}, map[string]uint64{}, map[string]Landing{}
|
p.txg, p.logical, p.drained, p.landing = map[string]uint64{}, map[string]uint64{}, map[string]uint64{}, map[string]Landing{}
|
||||||
|
p.txgAt, p.txgRate, p.drainedAt, p.drainRate = map[string]time.Time{}, map[string]float64{}, map[string]int64{}, map[string]float64{}
|
||||||
}
|
}
|
||||||
|
|
||||||
ms := readMounts()
|
ms := readMounts()
|
||||||
@@ -394,11 +399,22 @@ func (c *Collector) zfsWrites(now time.Time, arc *ARC, disks []Disk) *ZFSWrites
|
|||||||
|
|
||||||
for _, pool := range poolsWithKstats() {
|
for _, pool := range poolsWithKstats() {
|
||||||
pw := PoolWrites{Pool: pool}
|
pw := PoolWrites{Pool: pool}
|
||||||
|
// A txg records what it wrote only when it has synced — every few seconds, not every
|
||||||
|
// sample — so the rate is taken over the gap between syncs and held until the next one.
|
||||||
prevTop, had := p.txg[pool]
|
prevTop, had := p.txg[pool]
|
||||||
written, top := parseTxgs(readStr(zfsKstatDir+pool+"/txgs"), prevTop)
|
written, top := parseTxgs(readStr(zfsKstatDir+pool+"/txgs"), prevTop)
|
||||||
if had && dt > 0 {
|
switch {
|
||||||
pw.WriteBps = round1(float64(written) / dt)
|
case !had:
|
||||||
|
p.txgAt[pool] = now
|
||||||
|
case top > prevTop:
|
||||||
|
if gap := now.Sub(p.txgAt[pool]).Seconds(); gap > 0 {
|
||||||
|
p.txgRate[pool] = round1(float64(written) / gap)
|
||||||
|
}
|
||||||
|
p.txgAt[pool] = now
|
||||||
|
case now.Sub(p.txgAt[pool]) > time.Duration(2*max(w.TxgTimeoutSec, 5))*time.Second:
|
||||||
|
p.txgRate[pool] = 0 // no sync for two timeouts: nothing is being written
|
||||||
}
|
}
|
||||||
|
pw.WriteBps = p.txgRate[pool]
|
||||||
p.txg[pool] = top
|
p.txg[pool] = top
|
||||||
lw := objsetWritten(pool)
|
lw := objsetWritten(pool)
|
||||||
if prev, ok := p.logical[pool]; ok && dt > 0 && lw >= prev {
|
if prev, ok := p.logical[pool]; ok && dt > 0 && lw >= prev {
|
||||||
@@ -418,11 +434,18 @@ func (c *Collector) zfsWrites(now time.Time, arc *ARC, disks []Disk) *ZFSWrites
|
|||||||
l.InBps = diskW[l.Device]
|
l.InBps = diskW[l.Device]
|
||||||
if mv := movers[filepath.Clean(l.Branch)]; mv != nil {
|
if mv := movers[filepath.Clean(l.Branch)]; mv != nil {
|
||||||
l.Mover = mv
|
l.Mover = mv
|
||||||
|
// the mover writes its status every couple of seconds: measure between its updates
|
||||||
done := mv.MovedBytes + mv.Partial
|
done := mv.MovedBytes + mv.Partial
|
||||||
if prev, ok := p.drained[l.Branch]; ok && dt > 0 && done >= prev {
|
if at, ok := p.drainedAt[l.Branch]; !ok || mv.UpdatedMs != at {
|
||||||
l.DrainBps = round1(float64(done-prev) / dt)
|
if ok && mv.UpdatedMs > at && done >= p.drained[l.Branch] {
|
||||||
|
p.drainRate[l.Branch] = round1(float64(done-p.drained[l.Branch]) / (float64(mv.UpdatedMs-at) / 1000))
|
||||||
|
}
|
||||||
|
p.drained[l.Branch], p.drainedAt[l.Branch] = done, mv.UpdatedMs
|
||||||
}
|
}
|
||||||
p.drained[l.Branch] = done
|
if mv.State != "moving" {
|
||||||
|
p.drainRate[l.Branch] = 0
|
||||||
|
}
|
||||||
|
l.DrainBps = p.drainRate[l.Branch]
|
||||||
}
|
}
|
||||||
pw.Landing = &l
|
pw.Landing = &l
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -568,13 +568,13 @@ function ARCSection() {
|
|||||||
<span><b>All reads</b></span><span className="num"><b>{fmtPct(hitPct(L.Hits, L.Misses))}</b></span><span className="num">{fmtN(L.Hits)}</span><span className="num">{fmtN(L.Misses)}</span>
|
<span><b>All reads</b></span><span className="num"><b>{fmtPct(hitPct(L.Hits, L.Misses))}</b></span><span className="num">{fmtN(L.Hits)}</span><span className="num">{fmtN(L.Misses)}</span>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{d.pools.length > 0 && <div className="cards" style={{ marginTop: 8 }}>{d.pools.map((p) => <PoolTiers key={p.pool} p={p} v={v} />)}</div>}
|
{d.pools.length > 0 && <div className="cards full" style={{ marginTop: 8 }}>{d.pools.map((p) => <PoolTiers key={p.pool} p={p} v={v} />)}</div>}
|
||||||
{v.writes && v.writes.pools.length > 0 && (
|
{v.writes && v.writes.pools.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<div className="dash-bar" style={{ marginTop: 12 }}><h2 className="section">Writes</h2><span className="spacer" />
|
<div className="dash-bar" style={{ marginTop: 12 }}><h2 className="section">Writes</h2><span className="spacer" />
|
||||||
<span className="muted small">what is waiting to reach each pool, and how fast it is getting there</span>
|
<span className="muted small">what is waiting to reach each pool, and how fast it is getting there</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="cards">{v.writes.pools.map((p) => <WritePath key={p.pool} w={v.writes!} p={p} h={whist} />)}</div>
|
<div className="cards full">{v.writes.pools.map((p) => <WritePath key={p.pool} w={v.writes!} p={p} h={whist} />)}</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@@ -607,7 +607,7 @@ export default function ZFS() {
|
|||||||
<div className="dash">
|
<div className="dash">
|
||||||
<div className="dash-bar"><h2 className="section">Pools</h2></div>
|
<div className="dash-bar"><h2 className="section">Pools</h2></div>
|
||||||
{zfs.pools.length === 0 && <div className="notice">ZFS is present but no pools are imported.</div>}
|
{zfs.pools.length === 0 && <div className="notice">ZFS is present but no pools are imported.</div>}
|
||||||
<div className="cards">{zfs.pools.map((p) => <PoolCard key={p.name} p={p} onChanged={load} />)}</div>
|
<div className="cards full">{zfs.pools.map((p) => <PoolCard key={p.name} p={p} onChanged={load} />)}</div>
|
||||||
|
|
||||||
<div className="dash-bar" style={{ marginTop: 12 }}><h2 className="section">Cache</h2><span className="spacer" />
|
<div className="dash-bar" style={{ marginTop: 12 }}><h2 className="section">Cache</h2><span className="spacer" />
|
||||||
<span className="muted small">the ARC in RAM, and what each pool has in front of its disks</span>
|
<span className="muted small">the ARC in RAM, and what each pool has in front of its disks</span>
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ button { font: inherit; color: inherit; background: none; border: 0; cursor: poi
|
|||||||
.dash > * { flex-shrink: 0; }
|
.dash > * { flex-shrink: 0; }
|
||||||
.kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 12px; }
|
.kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 12px; }
|
||||||
.cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 12px; align-items: start; }
|
.cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 12px; align-items: start; }
|
||||||
|
.cards.full { grid-template-columns: minmax(0, 1fr); } /* one card per row, the width of the page */
|
||||||
.tile, .card { background: var(--bg2); border: 1px solid var(--border); border-radius: 8px; padding: 12px; min-width: 0; }
|
.tile, .card { background: var(--bg2); border: 1px solid var(--border); border-radius: 8px; padding: 12px; min-width: 0; }
|
||||||
.tile .label, .card h2 { margin: 0 0 6px; font-size: 12px; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: .5px; }
|
.tile .label, .card h2 { margin: 0 0 6px; font-size: 12px; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: .5px; }
|
||||||
.tile .value { font-size: 22px; font-weight: 600; font-variant-numeric: tabular-nums; line-height: 1.2; }
|
.tile .value { font-size: 22px; font-weight: 600; font-variant-numeric: tabular-nums; line-height: 1.2; }
|
||||||
|
|||||||
Reference in New Issue
Block a user