Add disk-load: no tool said a machine's iowait, disk load or who does the I/O (hq issue 312)
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
)
|
||||
|
||||
// Tools is the module's tools over one machine. Every one only reads.
|
||||
func Tools(m *Machine) []stdio.Tool {
|
||||
window := map[string]any{"type": "integer", "description": "how long to sample, in seconds (default 2, at most 10)"}
|
||||
return []stdio.Tool{
|
||||
{
|
||||
Name: "disk_load",
|
||||
Description: "How loaded this machine's storage is, sampled over a few seconds: the share of CPU time spent waiting on I/O (iowait), " +
|
||||
"I/O pressure stall (PSI), and for each disk how busy it was (%), read and write throughput (MiB/s), IOPS, the average wait " +
|
||||
"per request (ms) and the queue depth, with the filesystems it holds; then the filesystems' space and the processes " +
|
||||
"and the units moving the most bytes (from their cgroups), with their container. Each finding is said in a line under `said`. Replaces iostat, vmstat, " +
|
||||
"iotop and df. (r)",
|
||||
Input: map[string]any{
|
||||
"seconds": window,
|
||||
"processes": map[string]any{"type": "integer", "description": "how many of the busiest units and processes to name (default 5, 0 for none, at most 50)"},
|
||||
"all": map[string]any{"type": "boolean", "description": "every block device and partition, not only the whole disks (default false)"},
|
||||
},
|
||||
Run: func(args map[string]any) (any, error) {
|
||||
w, err := seconds(args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
n, err := count(args, "processes", 5, 0, 50)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m.LoadOver(w, n, flag(args, "all"), true)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "disk_top",
|
||||
Description: "What moved the most bytes to and from storage over a few seconds: by=unit (the default) each systemd unit " +
|
||||
"and container from its cgroup's io.stat, with MiB/s and IOPS, complete whoever runs the tools; by=container only the " +
|
||||
"containers, by name; by=process each process from /proc/<pid>/io, which misses another account's unless the tool " +
|
||||
"runner is root. Replaces iotop. (r)",
|
||||
Input: map[string]any{
|
||||
"seconds": window,
|
||||
"limit": map[string]any{"type": "integer", "description": "how many (default 10, at most 50)"},
|
||||
"by": map[string]any{"type": "string", "enum": []string{"process", "unit", "container"}, "description": "what to rank (default unit)"},
|
||||
},
|
||||
Run: func(args map[string]any) (any, error) {
|
||||
w, err := seconds(args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
limit, err := count(args, "limit", 10, 1, 50)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
by := text(args, "by")
|
||||
switch by {
|
||||
case "":
|
||||
by = "unit"
|
||||
case "process", "unit", "container":
|
||||
default:
|
||||
return nil, fmt.Errorf("by is process, unit or container, not %q", by)
|
||||
}
|
||||
out := map[string]any{"window_s": w.Seconds(), "by": by}
|
||||
if by != "process" {
|
||||
counted := m.bottomDevices()
|
||||
a := m.unitIO(counted)
|
||||
m.Wait(w)
|
||||
out["top"] = m.topUnits(a, m.unitIO(counted), w, by, limit)
|
||||
return out, nil
|
||||
}
|
||||
a, _ := m.processIO()
|
||||
m.Wait(w)
|
||||
b, unreadable := m.processIO()
|
||||
out["top"] = m.topProcesses(a, b, w, limit)
|
||||
if unreadable > 0 {
|
||||
out["processes_unreadable"] = unreadable
|
||||
out["note"] = unreadableNote(unreadable)
|
||||
}
|
||||
return out, nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "disk_filesystems",
|
||||
Description: "The space of every filesystem that stores data (not proc, tmpfs or overlay layers): size, used and free in GiB, " +
|
||||
"used % as df counts it, and inodes used %. A filesystem mounted at several places is said once. Replaces df. (r)",
|
||||
Run: func(map[string]any) (any, error) {
|
||||
fss := m.Filesystems()
|
||||
said := []string{}
|
||||
for _, fs := range fss {
|
||||
switch {
|
||||
case fs.Unanswered != "":
|
||||
said = append(said, fmt.Sprintf("%s: not read (%s)", fs.Mount, fs.Unanswered))
|
||||
default:
|
||||
line := fmt.Sprintf("%s (%s, %s): %.1f %% used, %.1f GiB free of %.1f GiB", fs.Mount, fs.Type, fs.Source, fs.UsedPct, fs.AvailGiB, fs.SizeGiB)
|
||||
if fs.UsedPct >= fullLine || fs.InodesUsed >= fullLine {
|
||||
line += " — NEARLY FULL"
|
||||
}
|
||||
said = append(said, line)
|
||||
}
|
||||
}
|
||||
return map[string]any{"said": said, "filesystems": fss}, nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "disk_devices",
|
||||
Description: "Each disk as it is, without sampling: model, size (GiB), whether it spins, its I/O scheduler and queue limit, what " +
|
||||
"it is built on (for LVM, encryption, RAID), and since the machine started: GiB read and written, the share of the time it " +
|
||||
"was busy and the average wait per read and per write (ms). Replaces lsblk and cat /proc/diskstats. (r)",
|
||||
Input: map[string]any{
|
||||
"all": map[string]any{"type": "boolean", "description": "every block device and partition, not only the whole disks (default false)"},
|
||||
},
|
||||
Run: func(args map[string]any) (any, error) {
|
||||
ds, up := m.DevicesNow(flag(args, "all"))
|
||||
return map[string]any{"up_hours": up, "devices": ds}, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func seconds(args map[string]any) (time.Duration, error) {
|
||||
n, err := count(args, "seconds", 2, 1, 10)
|
||||
return time.Duration(n) * time.Second, err
|
||||
}
|
||||
|
||||
func text(args map[string]any, key string) string {
|
||||
s, _ := args[key].(string)
|
||||
return strings.TrimSpace(s)
|
||||
}
|
||||
|
||||
func flag(args map[string]any, key string) bool {
|
||||
switch v := args[key].(type) {
|
||||
case bool:
|
||||
return v
|
||||
case string:
|
||||
return v == "true"
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// count is a whole-number argument, defaulted, refused below least, held at most.
|
||||
func count(args map[string]any, key string, fallback, least, most int) (int, error) {
|
||||
v, given := args[key]
|
||||
if !given || v == nil {
|
||||
return fallback, nil
|
||||
}
|
||||
var n int
|
||||
switch x := v.(type) {
|
||||
case float64:
|
||||
if x != math.Trunc(x) {
|
||||
return 0, fmt.Errorf("%s must be a whole number, not %v", key, x)
|
||||
}
|
||||
n = int(x)
|
||||
case string:
|
||||
i, err := strconv.Atoi(strings.TrimSpace(x))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("%s must be a whole number, not %q", key, x)
|
||||
}
|
||||
n = i
|
||||
default:
|
||||
return 0, fmt.Errorf("%s must be a whole number", key)
|
||||
}
|
||||
if n < least {
|
||||
return 0, fmt.Errorf("%s must be at least %d", key, least)
|
||||
}
|
||||
return min(n, most), nil
|
||||
}
|
||||
Reference in New Issue
Block a user