Skip to content

Commit 744523c

Browse files
authored
Merge pull request #351 from areed/concurrent-iops
Benchmark write latency with background IOPS
2 parents ae63ccb + 477cde7 commit 744523c

File tree

6 files changed

+226
-141
lines changed

6 files changed

+226
-141
lines changed

examples/preflight/host/filesystem-performance.yaml

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,25 @@ spec:
1010
fileSize: 22Mi
1111
operationSizeBytes: 2300
1212
datasync: true
13+
enableBackgroundIOPS: true
14+
backgroundIOPSWarmupSeconds: 60
15+
backgroundWriteIOPS: 300
16+
backgroundWriteIOPSJobs: 6
17+
backgroundReadIOPS: 50
18+
backgroundReadIOPSJobs: 1
1319
analyzers:
1420
- filesystemPerformance:
1521
collectorName: etcd-perf
1622
outcomes:
17-
- fail:
18-
when: "iops < 50"
19-
message: Insufficient random read IOPS
2023
- pass:
2124
when: "p99 < 3ms"
22-
message: Write latency is great!
25+
message: "Write latency is great! (p99: {{ .P99 }})"
2326
- pass:
2427
when: "p99 < 5ms"
25-
message: Write latency is ok
28+
message: "Write latency is ok (p99: {{ .P99 }})"
2629
- warn:
2730
when: "p99 < 8ms"
28-
message: Write latency is high
31+
message: "Write latency is high {{ .String }}"
2932
- fail:
3033
when: "p99 >= 8ms"
31-
message: Write latency is too high
34+
message: "Write latency is too high {{ .String }}"

pkg/analyze/host_filesystem_performance.go

Lines changed: 21 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package analyzer
22

33
import (
4+
"bytes"
45
"encoding/json"
56
"fmt"
7+
"html/template"
8+
"log"
69
"path/filepath"
7-
"strconv"
810
"strings"
911
"time"
1012

@@ -51,7 +53,7 @@ func (a *AnalyzeHostFilesystemPerformance) Analyze(getCollectedFileContents func
5153
if outcome.Fail != nil {
5254
if outcome.Fail.When == "" {
5355
result.IsFail = true
54-
result.Message = outcome.Fail.Message
56+
result.Message = renderFSPerfOutcome(outcome.Fail.Message, fsPerf)
5557
result.URI = outcome.Fail.URI
5658

5759
return &result, nil
@@ -64,15 +66,15 @@ func (a *AnalyzeHostFilesystemPerformance) Analyze(getCollectedFileContents func
6466

6567
if isMatch {
6668
result.IsFail = true
67-
result.Message = outcome.Fail.Message
69+
result.Message = renderFSPerfOutcome(outcome.Fail.Message, fsPerf)
6870
result.URI = outcome.Fail.URI
6971

7072
return &result, nil
7173
}
7274
} else if outcome.Warn != nil {
7375
if outcome.Warn.When == "" {
7476
result.IsWarn = true
75-
result.Message = outcome.Warn.Message
77+
result.Message = renderFSPerfOutcome(outcome.Warn.Message, fsPerf)
7678
result.URI = outcome.Warn.URI
7779

7880
return &result, nil
@@ -85,15 +87,15 @@ func (a *AnalyzeHostFilesystemPerformance) Analyze(getCollectedFileContents func
8587

8688
if isMatch {
8789
result.IsWarn = true
88-
result.Message = outcome.Warn.Message
90+
result.Message = renderFSPerfOutcome(outcome.Warn.Message, fsPerf)
8991
result.URI = outcome.Warn.URI
9092

9193
return &result, nil
9294
}
9395
} else if outcome.Pass != nil {
9496
if outcome.Pass.When == "" {
9597
result.IsPass = true
96-
result.Message = outcome.Pass.Message
98+
result.Message = renderFSPerfOutcome(outcome.Pass.Message, fsPerf)
9799
result.URI = outcome.Pass.URI
98100

99101
return &result, nil
@@ -106,7 +108,7 @@ func (a *AnalyzeHostFilesystemPerformance) Analyze(getCollectedFileContents func
106108

107109
if isMatch {
108110
result.IsPass = true
109-
result.Message = outcome.Pass.Message
111+
result.Message = renderFSPerfOutcome(outcome.Pass.Message, fsPerf)
110112
result.URI = outcome.Pass.URI
111113

112114
return &result, nil
@@ -125,14 +127,6 @@ func compareHostFilesystemPerformanceConditionalToActual(conditional string, fsP
125127
keyword := strings.ToLower(parts[0])
126128
comparator := parts[1]
127129

128-
if keyword == "iops" {
129-
desiredInt, err := strconv.ParseInt(parts[2], 10, 64)
130-
if err != nil {
131-
return false, errors.Wrapf(err, "failed to parse desired iops %q as integer", parts[2])
132-
}
133-
return doCompareHostFilesystemIOPS(comparator, fsPerf.IOPS, int(desiredInt))
134-
}
135-
136130
desiredDuration, err := time.ParseDuration(parts[2])
137131
if err != nil {
138132
return false, errors.Wrapf(err, "failed to parse duration %q", parts[2])
@@ -201,19 +195,17 @@ func doCompareHostFilesystemPerformance(operator string, actual time.Duration, d
201195
return false, fmt.Errorf("Unknown filesystem performance operator %q", operator)
202196
}
203197

204-
func doCompareHostFilesystemIOPS(operator string, actual int, desired int) (bool, error) {
205-
switch operator {
206-
case "<":
207-
return actual < desired, nil
208-
case "<=":
209-
return actual <= desired, nil
210-
case ">":
211-
return actual > desired, nil
212-
case ">=":
213-
return actual >= desired, nil
214-
case "=", "==", "===":
215-
return actual == desired, nil
198+
func renderFSPerfOutcome(outcome string, fsPerf collect.FSPerfResults) string {
199+
t, err := template.New("").Parse(outcome)
200+
if err != nil {
201+
log.Printf("Failed to parse filesystem performance outcome: %v", err)
202+
return outcome
216203
}
217-
218-
return false, fmt.Errorf("Unknown filesystem IOPS operator %q", operator)
204+
var buf bytes.Buffer
205+
err = t.Execute(&buf, fsPerf)
206+
if err != nil {
207+
log.Printf("Failed to render filesystem performance outcome: %v", err)
208+
return outcome
209+
}
210+
return buf.String()
219211
}

pkg/analyze/host_filesystem_performance_test.go

Lines changed: 0 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -19,51 +19,6 @@ func TestAnalyzeHostFilesystemPerformance(t *testing.T) {
1919
result *AnalyzeResult
2020
expectErr bool
2121
}{
22-
{
23-
name: "IOPS",
24-
fsPerf: &collect.FSPerfResults{
25-
IOPS: 50,
26-
},
27-
hostAnalyzer: &troubleshootv1beta2.FilesystemPerformanceAnalyze{
28-
Outcomes: []*troubleshootv1beta2.Outcome{
29-
{
30-
Fail: &troubleshootv1beta2.SingleOutcome{
31-
When: "iops < 20",
32-
Message: "IOPS < 20",
33-
},
34-
},
35-
{
36-
Fail: &troubleshootv1beta2.SingleOutcome{
37-
When: "iops <= 20",
38-
Message: "IOPS <= 30",
39-
},
40-
},
41-
{
42-
Fail: &troubleshootv1beta2.SingleOutcome{
43-
When: "iops > 70",
44-
Message: "IOPS > 70",
45-
},
46-
},
47-
{
48-
Fail: &troubleshootv1beta2.SingleOutcome{
49-
When: "iops >= 100",
50-
Message: "IOPS >= 100",
51-
},
52-
},
53-
{
54-
Pass: &troubleshootv1beta2.SingleOutcome{
55-
When: "iops == 50",
56-
Message: "IOPS == 50",
57-
},
58-
},
59-
},
60-
},
61-
result: &AnalyzeResult{
62-
Title: "Filesystem Performance",
63-
IsPass: true,
64-
Message: "IOPS == 50",
65-
},
66-
},
6722
{
6823
name: "Cover",
6924
fsPerf: &collect.FSPerfResults{

pkg/apis/troubleshoot/v1beta2/hostcollector_shared.go

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,43 @@ type TCPConnect struct {
7373
Timeout string `json:"timeout,omitempty"`
7474
}
7575

76+
// FilesystemPerformance benchmarks sequential write latency on a single file.
77+
// The optional background IOPS feature attempts to mimic real-world conditions by running read and
78+
// write workloads prior to and during benchmark execution.
7679
type FilesystemPerformance struct {
77-
HostCollectorMeta `json:",inline" yaml:",inline"`
80+
HostCollectorMeta `json:",inline" yaml:",inline"`
81+
// The size of each write operation performed while benchmarking. This does not apply to the
82+
// background IOPS feature if enabled, since those must be fixed at 4096.
7883
OperationSizeBytes uint64 `json:"operationSize,omitempty"`
79-
Directory string `json:"directory,omitempty"`
80-
FileSize string `json:"fileSize,omitempty"`
81-
Sync bool `json:"sync,omitempty"`
82-
Datasync bool `json:"datasync,omitempty"`
84+
// The directory where the benchmark will create files.
85+
Directory string `json:"directory,omitempty"`
86+
// The size of the file used in the benchmark. The number of IO operations for the benchmark
87+
// will be FileSize / OperationSizeBytes. Accepts valid Kubernetes resource units such as Mi.
88+
FileSize string `json:"fileSize,omitempty"`
89+
// Whether to call sync on the file after each write. Does not apply to background IOPS task.
90+
Sync bool `json:"sync,omitempty"`
91+
// Whether to call datasync on the file after each write. Skipped if Sync is also true. Does not
92+
// apply to background IOPS task.
93+
Datasync bool `json:"datasync,omitempty"`
94+
95+
// Enable the background IOPS feature.
96+
EnableBackgroundIOPS bool `json:"enableBackgroundIOPS"`
97+
// How long to run the background IOPS read and write workloads prior to starting the benchmarks.
98+
BackgroundIOPSWarmupSeconds int `json:"backgroundIOPSWarmupSeconds"`
99+
// The target write IOPS to run while benchmarking. This is a limit and there is no guarantee
100+
// it will be reached. This is the total IOPS for all background write jobs.
101+
BackgroundWriteIOPS int `json:"backgroundWriteIOPS"`
102+
// The target read IOPS to run while benchmarking. This is a limit and there is no guarantee
103+
// it will be reached. This is the total IOPS for all background read jobs.
104+
BackgroundReadIOPS int `json:"backgroundReadIOPS"`
105+
// Number of threads to use for background write IOPS. This should be set high enough to reach
106+
// the target specified in BackgroundWriteIOPS.
107+
// Example: If BackgroundWriteIOPS is 100 and write latency is 10ms then a single job would
108+
// barely be able to reach 100 IOPS so this should be at least 2.
109+
BackgroundWriteIOPSJobs int `json:"backgroundWriteIOPSJobs"`
110+
// Number of threads to use for background read IOPS. This should be set high enough to reach
111+
// the target specified in BackgrounReadIOPS.
112+
BackgroundReadIOPSJobs int `json:"backgroundReadIOPSJobs"`
83113
}
84114

85115
type Certificate struct {

pkg/collect/host_filesystem_performance.go

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package collect
22

33
import (
4+
"bytes"
45
"math"
56
"math/rand"
7+
"text/template"
68
"time"
79

810
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
@@ -49,7 +51,6 @@ type FSPerfResults struct {
4951
P999 time.Duration
5052
P9995 time.Duration
5153
P9999 time.Duration
52-
IOPS int
5354
}
5455

5556
func getPercentileIndex(p float64, items int) int {
@@ -58,3 +59,33 @@ func getPercentileIndex(p float64, items int) int {
5859
}
5960
return int(math.Ceil(p*float64(items))) - 1
6061
}
62+
63+
var fsPerfTmpl = template.Must(template.New("").Parse(`
64+
Min: {{ .Min }}
65+
Max: {{ .Max }}
66+
Avg: {{ .Average }}
67+
p1: {{ .P1 }}
68+
p5: {{ .P5 }}
69+
p10: {{ .P10 }}
70+
p20: {{ .P20 }}
71+
p30: {{ .P30 }}
72+
p40: {{ .P40 }}
73+
p50: {{ .P50 }}
74+
p60: {{ .P60 }}
75+
p70: {{ .P70 }}
76+
p80: {{ .P80 }}
77+
p90: {{ .P90 }}
78+
p95: {{ .P95 }}
79+
p99: {{ .P99 }}
80+
p99.5: {{ .P995 }}
81+
p99.9: {{ .P999 }}
82+
p99.95: {{ .P9995 }}
83+
p99.99: {{ .P9999 }}`))
84+
85+
func (f FSPerfResults) String() string {
86+
var buf bytes.Buffer
87+
88+
fsPerfTmpl.Execute(&buf, f)
89+
90+
return buf.String()
91+
}

0 commit comments

Comments
 (0)