Repository: prometheus/procfs
Pull request #277 (2020-03-31 15:06:19)

Bcache: add bdev/writeback_rate_debug stats
Hi! This PR adds support for bache writeback_rate_debug stats.
It's called debug, but it has useful information. It shows current writeback rate, target size of dirty data and PI-controller values. It is very usefull for debugging and helps to make decision to tweak bcache's PI-controller.
@pgier @discordianfish , could you approve this?


Patch:
diff --git a/bcache/bcache.go b/bcache/bcache.go
index df724ed..1176a55 100644
--- a/bcache/bcache.go
+++ b/bcache/bcache.go
@@ -43,10 +43,11 @@ type BcacheStats struct { // nolint:golint
 
 // BdevStats contains statistics for one backing device.
 type BdevStats struct {
-	Name      string
-	DirtyData uint64
-	FiveMin   PeriodStats
-	Total     PeriodStats
+	Name               string
+	DirtyData          uint64
+	FiveMin            PeriodStats
+	Total              PeriodStats
+	WritebackRateDebug WritebackRateDebugStats
 }
 
 // CacheStats contains statistics for one cache device.
@@ -82,3 +83,14 @@ type PeriodStats struct {
 	CacheMisses         uint64
 	CacheReadaheads     uint64
 }
+
+// WritebackRateDebugStats contains bcache writeback statistics.
+type WritebackRateDebugStats struct {
+	Rate         uint64
+	Dirty        uint64
+	Target       uint64
+	Proportional int64
+	Integral     int64
+	Change       int64
+	NextIO       int64
+}
diff --git a/bcache/get.go b/bcache/get.go
index 67fcf8b..c4747ea 100644
--- a/bcache/get.go
+++ b/bcache/get.go
@@ -174,6 +174,17 @@ func dehumanize(hbytes []byte) (uint64, error) {
 	return res, nil
 }
 
+func dehumanizeSigned(str string) (int64, error) {
+	value, err := dehumanize([]byte(strings.TrimPrefix(str, "-")))
+	if err != nil {
+		return 0, err
+	}
+	if strings.HasPrefix(str, "-") {
+		return int64(-value), nil
+	}
+	return int64(value), nil
+}
+
 type parser struct {
 	uuidPath   string
 	subDir     string
@@ -232,6 +243,72 @@ func parsePriorityStats(line string, ps *PriorityStats) error {
 	return nil
 }
 
+// ParseWritebackRateDebug parses lines from the writeback_rate_debug file.
+func parseWritebackRateDebug(line string, wrd *WritebackRateDebugStats) error {
+	switch {
+	case strings.HasPrefix(line, "rate:"):
+		fields := strings.Fields(line)
+		rawValue := fields[len(fields)-1]
+		valueStr := strings.TrimSuffix(rawValue, "/sec")
+		value, err := dehumanize([]byte(valueStr))
+		if err != nil {
+			return err
+		}
+		wrd.Rate = value
+	case strings.HasPrefix(line, "dirty:"):
+		fields := strings.Fields(line)
+		valueStr := fields[len(fields)-1]
+		value, err := dehumanize([]byte(valueStr))
+		if err != nil {
+			return err
+		}
+		wrd.Dirty = value
+	case strings.HasPrefix(line, "target:"):
+		fields := strings.Fields(line)
+		valueStr := fields[len(fields)-1]
+		value, err := dehumanize([]byte(valueStr))
+		if err != nil {
+			return err
+		}
+		wrd.Target = value
+	case strings.HasPrefix(line, "proportional:"):
+		fields := strings.Fields(line)
+		valueStr := fields[len(fields)-1]
+		value, err := dehumanizeSigned(valueStr)
+		if err != nil {
+			return err
+		}
+		wrd.Proportional = value
+	case strings.HasPrefix(line, "integral:"):
+		fields := strings.Fields(line)
+		valueStr := fields[len(fields)-1]
+		value, err := dehumanizeSigned(valueStr)
+		if err != nil {
+			return err
+		}
+		wrd.Integral = value
+	case strings.HasPrefix(line, "change:"):
+		fields := strings.Fields(line)
+		rawValue := fields[len(fields)-1]
+		valueStr := strings.TrimSuffix(rawValue, "/sec")
+		value, err := dehumanizeSigned(valueStr)
+		if err != nil {
+			return err
+		}
+		wrd.Change = value
+	case strings.HasPrefix(line, "next io:"):
+		fields := strings.Fields(line)
+		rawValue := fields[len(fields)-1]
+		valueStr := strings.TrimSuffix(rawValue, "ms")
+		value, err := strconv.ParseInt(valueStr, 10, 64)
+		if err != nil {
+			return err
+		}
+		wrd.NextIO = value
+	}
+	return nil
+}
+
 func (p *parser) getPriorityStats() PriorityStats {
 	var res PriorityStats
 
@@ -263,6 +340,35 @@ func (p *parser) getPriorityStats() PriorityStats {
 	return res
 }
 
+func (p *parser) getWritebackRateDebug() WritebackRateDebugStats {
+	var res WritebackRateDebugStats
+
+	if p.err != nil {
+		return res
+	}
+	path := path.Join(p.currentDir, "writeback_rate_debug")
+	file, err := os.Open(path)
+	if err != nil {
+		p.err = fmt.Errorf("failed to read: %s", path)
+		return res
+	}
+	defer file.Close()
+
+	scanner := bufio.NewScanner(file)
+	for scanner.Scan() {
+		err = parseWritebackRateDebug(scanner.Text(), &res)
+		if err != nil {
+			p.err = fmt.Errorf("failed to parse: %s (%s)", path, err)
+			return res
+		}
+	}
+	if err := scanner.Err(); err != nil {
+		p.err = fmt.Errorf("failed to parse: %s (%s)", path, err)
+		return res
+	}
+	return res
+}
+
 // GetStats collects from sysfs files data tied to one bcache ID.
 func GetStats(uuidPath string, priorityStats bool) (*Stats, error) {
 	var bs Stats
@@ -339,6 +445,9 @@ func GetStats(uuidPath string, priorityStats bool) (*Stats, error) {
 		par.setSubDir(bds.Name)
 		bds.DirtyData = par.readValue("dirty_data")
 
+		wrd := par.getWritebackRateDebug()
+		bds.WritebackRateDebug = wrd
+
 		// dir <uuidPath>/<bds.Name>/stats_five_minute
 		par.setSubDir(bds.Name, "stats_five_minute")
 		bds.FiveMin.Bypassed = par.readValue("bypassed")
diff --git a/fixtures.ttar b/fixtures.ttar
index 45a7321..0cd7836 100644
--- a/fixtures.ttar
+++ b/fixtures.ttar
@@ -4115,6 +4115,17 @@ Lines: 1
 0
 Mode: 644
 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/sys/fs/bcache/deaddd54-c735-46d5-868e-f331c5fd7c74/bdev0/writeback_rate_debug
+Lines: 7
+rate:           1.1M/sec
+dirty:          20.4G
+target:         20.4G
+proportional:   427.5k
+integral:       790.0k
+change:         321.5k/sec
+next io:        17ms
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 Directory: fixtures/sys/fs/bcache/deaddd54-c735-46d5-868e-f331c5fd7c74/bdev0/stats_day
 Mode: 755
 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -


Test patch:
diff --git a/bcache/get_test.go b/bcache/get_test.go
index e448797..22f562e 100644
--- a/bcache/get_test.go
+++ b/bcache/get_test.go
@@ -158,3 +158,61 @@ func TestPriorityStats(t *testing.T) {
 		t.Errorf("parsePriorityStats: '%s', want %d, got %d", in, want.UnusedPercent, got.UnusedPercent)
 	}
 }
+
+func TestWritebackRateDebug(t *testing.T) {
+	var want = WritebackRateDebugStats{
+		Rate:         1765376,
+		Dirty:        21789409280,
+		Target:       21894266880,
+		Proportional: -1124,
+		Integral:     -257624,
+		Change:       2648,
+		NextIO:       -150773,
+	}
+	var (
+		in     string
+		gotErr error
+		got    WritebackRateDebugStats
+	)
+	in = "rate:           1.7M/sec"
+	gotErr = parseWritebackRateDebug(in, &got)
+	if gotErr != nil || got.Rate != want.Rate {
+		t.Errorf("parsePriorityStats: '%s', want %d, got %d", in, want.Rate, got.Rate)
+	}
+
+	in = "dirty:           20.3G"
+	gotErr = parseWritebackRateDebug(in, &got)
+	if gotErr != nil || got.Dirty != want.Dirty {
+		t.Errorf("parsePriorityStats: '%s', want %d, got %d", in, want.Dirty, got.Dirty)
+	}
+
+	in = "target:           20.4G"
+	gotErr = parseWritebackRateDebug(in, &got)
+	if gotErr != nil || got.Target != want.Target {
+		t.Errorf("parsePriorityStats: '%s', want %d, got %d", in, want.Target, got.Target)
+	}
+
+	in = "proportional:           -1.1k"
+	gotErr = parseWritebackRateDebug(in, &got)
+	if gotErr != nil || got.Proportional != want.Proportional {
+		t.Errorf("parsePriorityStats: '%s', want %d, got %d", in, want.Proportional, got.Proportional)
+	}
+
+	in = "integral:           -251.6k"
+	gotErr = parseWritebackRateDebug(in, &got)
+	if gotErr != nil || got.Integral != want.Integral {
+		t.Errorf("parsePriorityStats: '%s', want %d, got %d", in, want.Integral, got.Integral)
+	}
+
+	in = "change:           2.6k/sec"
+	gotErr = parseWritebackRateDebug(in, &got)
+	if gotErr != nil || got.Change != want.Change {
+		t.Errorf("parsePriorityStats: '%s', want %d, got %d", in, want.Change, got.Change)
+	}
+
+	in = "next io:           -150773ms"
+	gotErr = parseWritebackRateDebug(in, &got)
+	if gotErr != nil || got.NextIO != want.NextIO {
+		t.Errorf("parsePriorityStats: '%s', want %d, got %d", in, want.NextIO, got.NextIO)
+	}
+}