Port fio benchmark

PiperOrigin-RevId: 323810654
This commit is contained in:
Zach Koopmans
2020-07-29 10:15:53 -07:00
committed by gVisor bot
parent 6b4e11ab50
commit 1715896fc8
4 changed files with 388 additions and 4 deletions
+7
View File
@@ -0,0 +1,7 @@
FROM ubuntu:18.04
RUN set -x \
&& apt-get update \
&& apt-get install -y \
fio \
&& rm -rf /var/lib/apt/lists/*
+9 -2
View File
@@ -12,12 +12,19 @@ go_library(
go_test(
name = "fs_test",
size = "large",
srcs = ["bazel_test.go"],
srcs = [
"bazel_test.go",
"fio_test.go",
],
library = ":fs",
tags = [
# Requires docker and runsc to be configured before test runs.
"local",
"manual",
],
deps = ["//pkg/test/dockerutil"],
deps = [
"//pkg/test/dockerutil",
"//test/benchmarks/harness",
"@com_github_docker_docker//api/types/mount:go_default_library",
],
)
+3 -2
View File
@@ -20,6 +20,7 @@ import (
"testing"
"gvisor.dev/gvisor/pkg/test/dockerutil"
"gvisor.dev/gvisor/test/benchmarks/harness"
)
// Note: CleanCache versions of this test require running with root permissions.
@@ -77,8 +78,8 @@ func BenchmarkABSL(b *testing.B) {
b.StopTimer()
// Drop Caches for clear cache runs.
if bm.clearCache {
if out, err := machine.RunCommand("/bin/sh", "-c", "sync && sysctl vm.drop_caches=3"); err != nil {
b.Skipf("failed to drop caches: %v %s. You probably need root.", err, out)
if err := harness.DropCaches(machine); err != nil {
b.Skipf("failed to drop caches: %v. You probably need root.", err)
}
}
b.StartTimer()
+369
View File
@@ -0,0 +1,369 @@
// Copyright 2020 The gVisor 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 fs
import (
"context"
"encoding/json"
"fmt"
"path/filepath"
"strconv"
"strings"
"testing"
"github.com/docker/docker/api/types/mount"
"gvisor.dev/gvisor/pkg/test/dockerutil"
"gvisor.dev/gvisor/test/benchmarks/harness"
)
type fioTestCase struct {
test string // test to run: read, write, randread, randwrite.
size string // total size to be read/written of format N[GMK] (e.g. 5G).
blocksize string // blocksize to be read/write of format N[GMK] (e.g. 4K).
iodepth int // iodepth for reads/writes.
time int // time to run the test in seconds, usually for rand(read/write).
}
// makeCmdFromTestcase makes a fio command.
func (f *fioTestCase) makeCmdFromTestcase(filename string) []string {
cmd := []string{"fio", "--output-format=json", "--ioengine=sync"}
cmd = append(cmd, fmt.Sprintf("--name=%s", f.test))
cmd = append(cmd, fmt.Sprintf("--size=%s", f.size))
cmd = append(cmd, fmt.Sprintf("--blocksize=%s", f.blocksize))
cmd = append(cmd, fmt.Sprintf("--filename=%s", filename))
cmd = append(cmd, fmt.Sprintf("--iodepth=%d", f.iodepth))
cmd = append(cmd, fmt.Sprintf("--rw=%s", f.test))
if f.time != 0 {
cmd = append(cmd, "--time_based")
cmd = append(cmd, fmt.Sprintf("--runtime=%d", f.time))
}
return cmd
}
// BenchmarkFio runs fio on the runtime under test. There are 4 basic test
// cases each run on a tmpfs mount and a bind mount. Fio requires root so that
// caches can be dropped.
func BenchmarkFio(b *testing.B) {
testCases := []fioTestCase{
fioTestCase{
test: "write",
size: "5G",
blocksize: "1M",
iodepth: 4,
},
fioTestCase{
test: "read",
size: "5G",
blocksize: "1M",
iodepth: 4,
},
fioTestCase{
test: "randwrite",
size: "5G",
blocksize: "4K",
iodepth: 4,
time: 30,
},
fioTestCase{
test: "randread",
size: "5G",
blocksize: "4K",
iodepth: 4,
time: 30,
},
}
machine, err := h.GetMachine()
if err != nil {
b.Fatalf("failed to get machine with: %v", err)
}
defer machine.CleanUp()
for _, fsType := range []mount.Type{mount.TypeBind, mount.TypeTmpfs} {
for _, tc := range testCases {
testName := strings.Title(tc.test) + strings.Title(string(fsType))
b.Run(testName, func(b *testing.B) {
ctx := context.Background()
container := machine.GetContainer(ctx, b)
defer container.CleanUp(ctx)
// Directory and filename inside container where fio will read/write.
outdir := "/data"
outfile := filepath.Join(outdir, "test.txt")
// Make the required mount and grab a cleanup for bind mounts
// as they are backed by a temp directory (mktemp).
mnt, mountCleanup, err := makeMount(machine, fsType, outdir)
if err != nil {
b.Fatalf("failed to make mount: %v", err)
}
defer mountCleanup()
cmd := tc.makeCmdFromTestcase(outfile)
// Start the container with the mount.
if err := container.Spawn(
ctx,
dockerutil.RunOpts{
Image: "benchmarks/fio",
Mounts: []mount.Mount{
mnt,
},
},
// Sleep on the order of b.N.
"sleep", fmt.Sprintf("%d", 1000*b.N),
); err != nil {
b.Fatalf("failed to start fio container with: %v", err)
}
// For reads, we need a file to read so make one inside the container.
if strings.Contains(tc.test, "read") {
fallocateCmd := fmt.Sprintf("fallocate -l %s %s", tc.size, outfile)
if out, err := container.Exec(ctx, dockerutil.ExecOpts{},
strings.Split(fallocateCmd, " ")...); err != nil {
b.Fatalf("failed to create readable file on mount: %v, %s", err, out)
}
}
// Drop caches just before running.
if err := harness.DropCaches(machine); err != nil {
b.Skipf("failed to drop caches with %v. You probably need root.", err)
}
container.RestartProfiles()
b.ResetTimer()
for i := 0; i < b.N; i++ {
// Run fio.
data, err := container.Exec(ctx, dockerutil.ExecOpts{}, cmd...)
if err != nil {
b.Fatalf("failed to run cmd %v: %v", cmd, err)
}
b.StopTimer()
// Parse the output and report the metrics.
isRead := strings.Contains(tc.test, "read")
bw, err := parseBandwidth(data, isRead)
if err != nil {
b.Fatalf("failed to parse bandwidth from %s with: %v", data, err)
}
b.ReportMetric(bw, "bandwidth") // in b/s.
iops, err := parseIOps(data, isRead)
if err != nil {
b.Fatalf("failed to parse iops from %s with: %v", data, err)
}
b.ReportMetric(iops, "iops")
// If b.N is used (i.e. we run for an hour), we should drop caches
// after each run.
if err := harness.DropCaches(machine); err != nil {
b.Fatalf("failed to drop caches: %v", err)
}
b.StartTimer()
}
})
}
}
}
// makeMount makes a mount and cleanup based on the requested type. Bind
// and volume mounts are backed by a temp directory made with mktemp.
// tmpfs mounts require no such backing and are just made.
// It is up to the caller to call the returned cleanup.
func makeMount(machine harness.Machine, mountType mount.Type, target string) (mount.Mount, func(), error) {
switch mountType {
case mount.TypeVolume, mount.TypeBind:
dir, err := machine.RunCommand("mktemp", "-d")
if err != nil {
return mount.Mount{}, func() {}, fmt.Errorf("failed to create tempdir: %v", err)
}
dir = strings.TrimSuffix(dir, "\n")
out, err := machine.RunCommand("chmod", "777", dir)
if err != nil {
machine.RunCommand("rm", "-rf", dir)
return mount.Mount{}, func() {}, fmt.Errorf("failed modify directory: %v %s", err, out)
}
return mount.Mount{
Target: target,
Source: dir,
Type: mount.TypeBind,
}, func() { machine.RunCommand("rm", "-rf", dir) }, nil
case mount.TypeTmpfs:
return mount.Mount{
Target: target,
Type: mount.TypeTmpfs,
}, func() {}, nil
default:
return mount.Mount{}, func() {}, fmt.Errorf("illegal mount time not supported: %v", mountType)
}
}
// parseBandwidth reports the bandwidth in b/s.
func parseBandwidth(data string, isRead bool) (float64, error) {
if isRead {
result, err := parseFioJSON(data, "read", "bw")
if err != nil {
return 0, err
}
return 1024 * result, nil
}
result, err := parseFioJSON(data, "write", "bw")
if err != nil {
return 0, err
}
return 1024 * result, nil
}
// parseIOps reports the write IO per second metric.
func parseIOps(data string, isRead bool) (float64, error) {
if isRead {
return parseFioJSON(data, "read", "iops")
}
return parseFioJSON(data, "write", "iops")
}
// fioResult is for parsing FioJSON.
type fioResult struct {
Jobs []fioJob
}
// fioJob is for parsing FioJSON.
type fioJob map[string]json.RawMessage
// fioMetrics is for parsing FioJSON.
type fioMetrics map[string]json.RawMessage
// parseFioJSON parses data and grabs "op" (read or write) and "metric"
// (bw or iops) from the JSON.
func parseFioJSON(data, op, metric string) (float64, error) {
var result fioResult
if err := json.Unmarshal([]byte(data), &result); err != nil {
return 0, fmt.Errorf("could not unmarshal data: %v", err)
}
if len(result.Jobs) < 1 {
return 0, fmt.Errorf("no jobs present to parse")
}
var metrics fioMetrics
if err := json.Unmarshal(result.Jobs[0][op], &metrics); err != nil {
return 0, fmt.Errorf("could not unmarshal jobs: %v", err)
}
if _, ok := metrics[metric]; !ok {
return 0, fmt.Errorf("no metric found for op: %s", op)
}
return strconv.ParseFloat(string(metrics[metric]), 64)
}
// TestParsers tests that the parsers work on sampleData.
func TestParsers(t *testing.T) {
sampleData := `
{
"fio version" : "fio-3.1",
"timestamp" : 1554837456,
"timestamp_ms" : 1554837456621,
"time" : "Tue Apr 9 19:17:36 2019",
"jobs" : [
{
"jobname" : "test",
"groupid" : 0,
"error" : 0,
"eta" : 2147483647,
"elapsed" : 1,
"job options" : {
"name" : "test",
"ioengine" : "sync",
"size" : "1073741824",
"filename" : "/disk/file.dat",
"iodepth" : "4",
"bs" : "4096",
"rw" : "write"
},
"read" : {
"io_bytes" : 0,
"io_kbytes" : 0,
"bw" : 123456,
"iops" : 1234.5678,
"runtime" : 0,
"total_ios" : 0,
"short_ios" : 0,
"bw_min" : 0,
"bw_max" : 0,
"bw_agg" : 0.000000,
"bw_mean" : 0.000000,
"bw_dev" : 0.000000,
"bw_samples" : 0,
"iops_min" : 0,
"iops_max" : 0,
"iops_mean" : 0.000000,
"iops_stddev" : 0.000000,
"iops_samples" : 0
},
"write" : {
"io_bytes" : 1073741824,
"io_kbytes" : 1048576,
"bw" : 1753471,
"iops" : 438367.892977,
"runtime" : 598,
"total_ios" : 262144,
"bw_min" : 1731120,
"bw_max" : 1731120,
"bw_agg" : 98.725328,
"bw_mean" : 1731120.000000,
"bw_dev" : 0.000000,
"bw_samples" : 1,
"iops_min" : 432780,
"iops_max" : 432780,
"iops_mean" : 432780.000000,
"iops_stddev" : 0.000000,
"iops_samples" : 1
}
}
]
}
`
// WriteBandwidth.
got, err := parseBandwidth(sampleData, false)
var want float64 = 1753471.0 * 1024
if err != nil {
t.Fatalf("parse failed with err: %v", err)
} else if got != want {
t.Fatalf("got: %f, want: %f", got, want)
}
// ReadBandwidth.
got, err = parseBandwidth(sampleData, true)
want = 123456 * 1024
if err != nil {
t.Fatalf("parse failed with err: %v", err)
} else if got != want {
t.Fatalf("got: %f, want: %f", got, want)
}
// WriteIOps.
got, err = parseIOps(sampleData, false)
want = 438367.892977
if err != nil {
t.Fatalf("parse failed with err: %v", err)
} else if got != want {
t.Fatalf("got: %f, want: %f", got, want)
}
// ReadIOps.
got, err = parseIOps(sampleData, true)
want = 1234.5678
if err != nil {
t.Fatalf("parse failed with err: %v", err)
} else if got != want {
t.Fatalf("got: %f, want: %f", got, want)
}
}