package main import ( "context" "fmt" "os" "strconv" "strings" "time" ) // IOArgv samples every pool's I/O for some seconds: -y leaves out the report since boot, so the one report // is the window alone. func IOArgv(seconds int) []string { return []string{"iostat", "-y", "-Hpv", strconv.Itoa(seconds), "1"} } // IORow is one line of the sample: a pool, a vdev or a device. type IORow struct { Name string `json:"name"` Pool string `json:"pool"` // Kind is pool, vdev or device, and Role data, log, cache, special or dedup, from the pool's layout: // iostat -H prints no section lines and no indentation. Kind string `json:"kind"` Role string `json:"role,omitempty"` // Allocated and Free are a top-level vdev's; a vdev without capacity of its own has neither. Allocated *uint64 `json:"allocated_bytes,omitempty"` Free *uint64 `json:"free_bytes,omitempty"` ReadOps float64 `json:"read_ops_per_s"` WriteOps float64 `json:"write_ops_per_s"` ReadBytes float64 `json:"read_bytes_per_s"` WriteBytes float64 `json:"write_bytes_per_s"` } // ParseIO reads `zpool iostat -Hpv`: name, allocated, free, read and write operations, read and write // bandwidth, tab-separated. Under -H there are no section lines (a blank line stands before each class) // and under -p a vdev without capacity of its own prints 0 for both (zpool_main.c, print_iostat_default). // Lines of a pool follow the pool's own line; its layout names each line's kind and role. func ParseIO(out string, layout []PoolStatus) ([]IORow, error) { var rows []IORow pool := "" for _, line := range lines(out) { f := strings.Split(line, "\t") if len(f) != 7 { f = strings.Fields(line) } if len(f) != 7 { return nil, fmt.Errorf("zpool iostat answered a line with %d columns, not 7: %q", len(f), line) } r := IORow{Name: strings.TrimSpace(f[0]), Kind: "device"} if statusOf(layout, r.Name) != nil { pool = r.Name r.Kind = "" } r.Pool = pool if r.Kind == "" { r.Kind = "pool" } else if p := statusOf(layout, pool); p != nil { walk(p.Root, func(d *Device, _ int) { if d.Name == r.Name && d.Kind != "pool" && d.Kind != "section" { r.Kind, r.Role = d.Kind, d.Role } }, 0) } if !(f[1] == "0" && f[2] == "0") { r.Allocated, r.Free = optionalNumber(f[1]), optionalNumber(f[2]) } vals := []*float64{&r.ReadOps, &r.WriteOps, &r.ReadBytes, &r.WriteBytes} for i, v := range vals { s := f[3+i] if s == "-" { continue } x, err := strconv.ParseFloat(s, 64) if err != nil { return nil, fmt.Errorf("zpool iostat: %q of %s is not a number", s, r.Name) } *v = x } rows = append(rows, r) } return rows, nil } func optionalNumber(s string) *uint64 { if s == "-" { return nil } v, err := strconv.ParseUint(s, 10, 64) if err != nil { return nil } return &v } // IOAnswer is what zfs_io answers. type IOAnswer struct { Applicable bool `json:"applicable"` Seconds int `json:"window_s"` Rows []IORow `json:"rows"` Said []string `json:"said"` } // IO samples every pool's operations and bandwidth for a few seconds. func (m *Machine) IO(ctx context.Context, seconds int) (any, error) { st, err := m.Status(ctx) if err != nil { return notHere(err) } sctx, cancel := context.WithTimeout(ctx, time.Duration(seconds)*time.Second+CommandTimeout) defer cancel() out, err := m.ask(sctx, "zpool", IOArgv(seconds)...) if err != nil { return notHere(err) } rows, err := ParseIO(out, st) if err != nil { return nil, err } a := IOAnswer{Applicable: true, Seconds: seconds, Rows: rows, Said: []string{}} for _, r := range rows { kind := r.Kind if r.Role != "" && r.Role != "data" { kind += ", " + r.Role } a.Said = append(a.Said, fmt.Sprintf("%s %s (%s): read %.0f ops/s %s/s, write %.0f ops/s %s/s", r.Pool, r.Name, kind, r.ReadOps, bytesSaid(uint64(r.ReadBytes)), r.WriteOps, bytesSaid(uint64(r.WriteBytes)))) } return a, nil } // ArcStats is where the kernel module keeps the ARC's counters. const ArcStats = "/proc/spl/kstat/zfs/arcstats" // ParseArc reads arcstats: two header lines, then `name type value`. func ParseArc(text string) map[string]uint64 { out := map[string]uint64{} for i, line := range strings.Split(text, "\n") { f := strings.Fields(line) if i < 2 || len(f) != 3 { continue } if v, err := strconv.ParseUint(f[2], 10, 64); err == nil { out[f[0]] = v } } return out } // ArcAnswer is what zfs_arc answers. type ArcAnswer struct { Applicable bool `json:"applicable"` Size uint64 `json:"size_bytes"` Target uint64 `json:"target_bytes"` Min uint64 `json:"min_bytes"` Max uint64 `json:"max_bytes"` Hits uint64 `json:"hits"` Misses uint64 `json:"misses"` HitRatio float64 `json:"hit_ratio_pct"` // DemandHitRatio counts only what was asked for, not what ZFS read ahead. DemandHitRatio *float64 `json:"demand_hit_ratio_pct,omitempty"` L2 *L2Arc `json:"l2arc,omitempty"` Said []string `json:"said"` } // L2Arc is the cache device's share of the ARC, where the pool has one. type L2Arc struct { Size uint64 `json:"size_bytes"` Allocated uint64 `json:"allocated_bytes"` Hits uint64 `json:"hits"` Misses uint64 `json:"misses"` HitRatio float64 `json:"hit_ratio_pct"` } // Arc is the ARC's size and hit ratio since the machine started. func (m *Machine) Arc() (any, error) { raw, err := m.ReadFile(ArcStats) if err != nil { if os.IsNotExist(err) { return NotApplicable{Applicable: false, Said: "there is no " + ArcStats + ": ZFS's kernel module is not loaded on this machine, so this module does not apply here"}, nil } return nil, fmt.Errorf("%s could not be read: %v", ArcStats, err) } s := ParseArc(string(raw)) if _, ok := s["size"]; !ok { return nil, fmt.Errorf("%s has no size line", ArcStats) } a := ArcAnswer{Applicable: true, Size: s["size"], Target: s["c"], Min: s["c_min"], Max: s["c_max"], Hits: s["hits"], Misses: s["misses"]} a.HitRatio = pct(a.Hits, a.Hits+a.Misses) dh := s["demand_data_hits"] + s["demand_metadata_hits"] dm := s["demand_data_misses"] + s["demand_metadata_misses"] if dh+dm > 0 { r := pct(dh, dh+dm) a.DemandHitRatio = &r } a.Said = []string{fmt.Sprintf("ARC: %s of a %s target (min %s, max %s); %.1f %% of %d reads since the machine started were hits", bytesSaid(a.Size), bytesSaid(a.Target), bytesSaid(a.Min), bytesSaid(a.Max), a.HitRatio, a.Hits+a.Misses)} if s["l2_size"] > 0 || s["l2_hits"]+s["l2_misses"] > 0 { l := &L2Arc{Size: s["l2_size"], Allocated: s["l2_asize"], Hits: s["l2_hits"], Misses: s["l2_misses"]} l.HitRatio = pct(l.Hits, l.Hits+l.Misses) a.L2 = l a.Said = append(a.Said, fmt.Sprintf("L2ARC: %s cached (%s on the device), %.1f %% of %d reads were hits", bytesSaid(l.Size), bytesSaid(l.Allocated), l.HitRatio, l.Hits+l.Misses)) } else { a.Said = append(a.Said, "no L2ARC (no cache device)") } return a, nil } func pct(part, whole uint64) float64 { if whole == 0 { return 0 } return round1(float64(part) * 100 / float64(whole)) }