diff --git a/testdata/fixtures.ttar b/testdata/fixtures.ttar index c6dd8b96..5b1d5506 100644 --- a/testdata/fixtures.ttar +++ b/testdata/fixtures.ttar @@ -3368,6 +3368,64 @@ Lines: 11 RCU: 605871 532783 Mode: 444 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Directory: fixtures/proc/spl +Mode: 755 +# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Directory: fixtures/proc/spl/kstat +Mode: 755 +# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Directory: fixtures/proc/spl/kstat/zfs +Mode: 755 +# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Directory: fixtures/proc/spl/kstat/zfs/pool1 +Mode: 755 +# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Path: fixtures/proc/spl/kstat/zfs/pool1/ddt_stats_blake3 +Lines: 19 +107 1 0x01 17 4624 2297413542 625908480381 +name type data +lookup 4 0 +lookup_new 4 0 +lookup_existing 4 0 +lookup_live_hit 4 0 +lookup_live_wait 4 0 +lookup_live_miss 4 0 +lookup_log_hit 4 0 +lookup_log_active_hit 4 0 +lookup_log_flushing_hit 4 0 +lookup_log_miss 4 0 +lookup_stored_hit 4 0 +lookup_stored_miss 4 0 +log_active_entries 4 0 +log_flushing_entries 4 0 +log_ingest_rate 2 0 +log_flush_rate 2 0 +log_flush_time_rate 2 0 +Mode: 644 +# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Path: fixtures/proc/spl/kstat/zfs/pool1/ddt_stats_sha256 +Lines: 19 +103 1 0x01 17 4624 2297405876 621083650087 +name type data +lookup 4 800 +lookup_new 4 400 +lookup_existing 4 400 +lookup_live_hit 4 0 +lookup_live_wait 4 0 +lookup_live_miss 4 800 +lookup_log_hit 4 148 +lookup_log_active_hit 4 136 +lookup_log_flushing_hit 4 12 +lookup_log_miss 4 652 +lookup_stored_hit 4 252 +lookup_stored_miss 4 400 +log_active_entries 4 0 +log_flushing_entries 4 0 +log_ingest_rate 2 26 +log_flush_rate 2 37 +log_flush_time_rate 2 0 +Mode: 644 +# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Path: fixtures/proc/stat Lines: 16 cpu 301854 612 111922 8979004 3552 2 3944 0 0 0 diff --git a/zfs_ddt.go b/zfs_ddt.go new file mode 100644 index 00000000..230272c7 --- /dev/null +++ b/zfs_ddt.go @@ -0,0 +1,168 @@ +// Copyright The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package procfs + +import ( + "bytes" + "fmt" + "path/filepath" + "strconv" + "strings" + + "github.com/prometheus/procfs/internal/parsers" +) + +// constants from https://github.com/openzfs/zfs/blob/master/lib/libspl/include/sys/kstat.h +// Kept as strings for comparison thus avoiding conversion to int. +const ( + kstatDataUint32 = "2" + kstatDataUint64 = "4" +) + +// ZFSDDTStats holds OpenZFS dedup table statistics for one pool and checksum, +// read from /proc/spl/kstat/zfs//ddt_stats_ (OpenZFS 2.3+). +// See https://github.com/openzfs/zfs/blob/master/module/zfs/ddt.c (ddt_kstats_template). +type ZFSDDTStats struct { + Pool string + Checksum string + + Lookup uint64 + LookupNew uint64 + LookupExisting uint64 + LookupLiveHit uint64 + LookupLiveWait uint64 + LookupLiveMiss uint64 + LookupLogHit uint64 + LookupLogActiveHit uint64 + LookupLogFlushingHit uint64 + LookupLogMiss uint64 + LookupStoredHit uint64 + LookupStoredMiss uint64 + LogActiveEntries uint64 + LogFlushingEntries uint64 + LogIngestRate uint64 + LogFlushRate uint64 + LogFlushTimeRate uint64 +} + +// ZFSDDTStats reads /proc/spl/kstat/zfs/*/ddt_stats_*. It returns an empty +// slice if no such files exist (OpenZFS older than 2.3, or no ZFS). +func (fs FS) ZFSDDTStats() ([]ZFSDDTStats, error) { + paths, err := filepath.Glob(fs.proc.Path("spl/kstat/zfs/*/ddt_stats_*")) + if err != nil { + return nil, err + } + + // Glob returns nil on no match, so older ZFS or no dedup gives an empty result for free. + var stats []ZFSDDTStats + for _, path := range paths { + data, err := parsers.ReadFileNoStat(path) + if err != nil { + return nil, err + } + + pool := filepath.Base(filepath.Dir(path)) + checksum := strings.TrimPrefix(filepath.Base(path), "ddt_stats_") + + stat, err := parseZFSDDTStats(pool, checksum, data) + if err != nil { + return nil, err + } + + stats = append(stats, stat) + } + + return stats, nil +} + +func parseZFSDDTStats(pool, checksum string, data []byte) (ZFSDDTStats, error) { + var stat ZFSDDTStats + stat.Pool = pool + stat.Checksum = checksum + inTable := false + + // The kstat data is in the form of a table with 3 columns: name, type, value. + lines := bytes.Split(data, []byte{'\n'}) + for _, line := range lines { + fields := bytes.Fields(line) + + if !inTable && len(fields) == 3 && bytes.Equal(fields[0], []byte("name")) && bytes.Equal(fields[1], []byte("type")) && bytes.Equal(fields[2], []byte("data")) { + inTable = true + continue + } + + if !inTable || len(fields) != 3 { + continue + } + + name := string(fields[0]) + var value uint64 + var err error + + switch string(fields[1]) { + case kstatDataUint32, kstatDataUint64: + value, err = strconv.ParseUint(string(fields[2]), 10, 64) + if err != nil { + return ZFSDDTStats{}, fmt.Errorf("%w: %s/%s: couldn't parse %s=%q: %w", ErrFileParse, pool, checksum, name, fields[2], err) + } + default: + continue + } + + switch name { + case "lookup": + stat.Lookup = value + case "lookup_new": + stat.LookupNew = value + case "lookup_existing": + stat.LookupExisting = value + case "lookup_live_hit": + stat.LookupLiveHit = value + case "lookup_live_wait": + stat.LookupLiveWait = value + case "lookup_live_miss": + stat.LookupLiveMiss = value + case "lookup_log_hit": + stat.LookupLogHit = value + case "lookup_log_active_hit": + stat.LookupLogActiveHit = value + case "lookup_log_flushing_hit": + stat.LookupLogFlushingHit = value + case "lookup_log_miss": + stat.LookupLogMiss = value + case "lookup_stored_hit": + stat.LookupStoredHit = value + case "lookup_stored_miss": + stat.LookupStoredMiss = value + case "log_active_entries": + stat.LogActiveEntries = value + case "log_flushing_entries": + stat.LogFlushingEntries = value + case "log_ingest_rate": + stat.LogIngestRate = value + case "log_flush_rate": + stat.LogFlushRate = value + case "log_flush_time_rate": + stat.LogFlushTimeRate = value + default: + // Ignore unknown fields, as they may be added in future versions of ZFS. + } + } + + if !inTable { + return ZFSDDTStats{}, fmt.Errorf("%w: %s/%s: missing name/type/data header", ErrFileParse, pool, checksum) + } + + return stat, nil +} diff --git a/zfs_ddt_test.go b/zfs_ddt_test.go new file mode 100644 index 00000000..9fddf3d5 --- /dev/null +++ b/zfs_ddt_test.go @@ -0,0 +1,132 @@ +// Copyright The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package procfs + +import ( + "errors" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestZFSDDTStats(t *testing.T) { + stats, err := getProcFixtures(t).ZFSDDTStats() + if err != nil { + t.Fatal(err) + } + + want := []ZFSDDTStats{ + { + // Every counter is zero in the fixture, so we don't need to check them all. + Pool: "pool1", + Checksum: "blake3", + }, + { + Pool: "pool1", + Checksum: "sha256", + Lookup: 800, + LookupNew: 400, + LookupExisting: 400, + LookupLiveHit: 0, + LookupLiveWait: 0, + LookupLiveMiss: 800, + LookupLogHit: 148, + LookupLogActiveHit: 136, + LookupLogFlushingHit: 12, + LookupLogMiss: 652, + LookupStoredHit: 252, + LookupStoredMiss: 400, + LogActiveEntries: 0, + LogFlushingEntries: 0, + LogIngestRate: 26, // type 2 (UINT32) rows + LogFlushRate: 37, + LogFlushTimeRate: 0, + }, + } + + if len(stats) != len(want) { + t.Fatalf("want %d ZFSDDTStats, have %d", len(want), len(stats)) + } + + for i := range want { + if diff := cmp.Diff(want[i], stats[i]); diff != "" { + t.Errorf("unexpected ZFSDDTStats (-want +got):\n%s", diff) + } + } +} + +func TestParseZFSDDTStats(t *testing.T) { + const header = "103 1 0x01 17 4624 2297405876 621083650087\nname type data\n" + + tests := []struct { + name string + input string + want ZFSDDTStats + wantErr bool + }{ + { + name: "empty input", + input: "", + wantErr: true, + }, + { + name: "missing name/type/data header", + input: "lookup 4 800\n", + wantErr: true, + }, + { + name: "header only", + input: header, + want: ZFSDDTStats{Pool: "pool1", Checksum: "sha256"}, + }, + { + name: "non-numeric value", + input: header + "lookup 4 abc\n", + wantErr: true, + }, + { + name: "unknown field is ignored", + input: header + "lookup 4 800\nfuture_field 4 5\n", + want: ZFSDDTStats{Pool: "pool1", Checksum: "sha256", Lookup: 800}, + }, + { + name: "unexpected type is skipped", + input: header + "lookup 4 800\nlookup_new 7 400\n", + want: ZFSDDTStats{Pool: "pool1", Checksum: "sha256", Lookup: 800}, + }, + { + name: "uint32 row is parsed", + input: header + "log_ingest_rate 2 26\n", + want: ZFSDDTStats{Pool: "pool1", Checksum: "sha256", LogIngestRate: 26}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseZFSDDTStats("pool1", "sha256", []byte(tt.input)) + if tt.wantErr { + if !errors.Is(err, ErrFileParse) { + t.Fatalf("expected ErrFileParse, got %v", err) + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("unexpected ZFSDDTStats (-want +got):\n%s", diff) + } + }) + } +}