Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions testdata/fixtures.ttar
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
168 changes: 168 additions & 0 deletions zfs_ddt.go
Original file line number Diff line number Diff line change
@@ -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/<pool>/ddt_stats_<checksum> (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
}
132 changes: 132 additions & 0 deletions zfs_ddt_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}