Add Kubernetes benchmarks suite.

This is a large set of Kubernetes-based performance benchmarks that has
been in use at Google for gVisor performance testing on GKE, and is now
open-source.

This set of benchmarks has been created and maintained by multiple
contributors over the last 2 years:
@zkoopmans, @EtiennePerot, @kevinGC, @ayushr2, @manninglucas,
@konstantin-s-bogom.

PiperOrigin-RevId: 686649688
This commit is contained in:
Etienne Perot
2024-10-16 14:51:32 -07:00
committed by gVisor bot
parent b7334af658
commit 4cff9027d6
39 changed files with 8059 additions and 10 deletions
+4 -10
View File
@@ -63,10 +63,10 @@ http_archive(
http_archive(
name = "googleapis",
sha256 = "9d1a930e767c93c825398b8f8692eca3fe353b9aaadedfbcf1fca2282c85df88",
strip_prefix = "googleapis-64926d52febbf298cb82a8f472ade4a3969ba922",
sha256 = "fd9e4d17b92be6b6718ee9b40062a4ce81feb6ea6cdd80fc723daf127ce3f350",
strip_prefix = "googleapis-3effbf23b1a1d1fe1306356e94397e20d01d31a0",
urls = [
"https://github.com/googleapis/googleapis/archive/64926d52febbf298cb82a8f472ade4a3969ba922.zip",
"https://github.com/googleapis/googleapis/archive/3effbf23b1a1d1fe1306356e94397e20d01d31a0.zip",
],
)
@@ -74,6 +74,7 @@ load("@googleapis//:repository_rules.bzl", "switched_rules_by_language")
switched_rules_by_language(
name = "com_google_googleapis_imports",
go = True, # Enable go_proto_library BUILD rules inside @googleapis repo.
)
http_archive(
@@ -271,13 +272,6 @@ http_archive(
load("@rules_proto//proto:repositories.bzl", "rules_proto_dependencies", "rules_proto_toolchains")
go_repository(
name = "com_github_google_martian_v3",
importpath = "github.com/google/martian/v3",
sum = "h1:d8MncMlErDFTwQGBK1xhv026j9kqhvw1Qv9IbWT1VLQ=",
version = "v3.2.1",
)
go_repository(
name = "org_golang_x_term",
importpath = "golang.org/x/term",
+18
View File
@@ -0,0 +1,18 @@
load("//tools:defs.bzl", "proto_library")
package(
default_applicable_licenses = ["//:license"],
licenses = ["notice"],
)
proto_library(
name = "test_range_config",
srcs = ["test_range_config.proto"],
has_services = 1,
visibility = [
"//visibility:public",
],
deps = [
"@googleapis//google/container/v1:container_proto",
],
)
+264
View File
@@ -0,0 +1,264 @@
load("//tools:defs.bzl", "go_test")
package(
default_applicable_licenses = ["//:license"],
licenses = ["notice"],
)
go_test(
name = "abslbuild_test",
srcs = ["abslbuild_test.go"],
tags = [
"local",
"noguitar",
"notap",
],
deps = [
"//test/kubernetes/benchmarks/profiling",
"//test/kubernetes/benchmetric",
"//test/kubernetes/k8sctx",
"//test/kubernetes/testcluster",
"@io_k8s_api//core/v1:go_default_library",
"@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library",
],
)
go_test(
name = "startup_test",
srcs = ["startup_test.go"],
tags = [
"local",
"noguitar",
"notap",
],
deps = [
"//test/kubernetes/benchmarks/profiling",
"//test/kubernetes/benchmetric",
"//test/kubernetes/k8sctx",
"//test/kubernetes/testcluster",
"@io_k8s_api//core/v1:go_default_library",
],
)
go_test(
name = "redis_test",
srcs = ["redis_test.go"],
tags = [
"local",
"noguitar",
"notap",
],
deps = [
"//test/kubernetes/benchmarks/profiling",
"//test/kubernetes/benchmetric",
"//test/kubernetes/k8sctx",
"//test/kubernetes/testcluster",
"@io_k8s_api//core/v1:go_default_library",
"@io_k8s_apimachinery//pkg/util/intstr:go_default_library",
],
)
go_test(
name = "ruby_dev_test",
srcs = ["ruby_dev_test.go"],
tags = [
"local",
"noguitar",
"notap",
],
deps = [
"//test/benchmarks/tools",
"//test/kubernetes/benchmarks/profiling",
"//test/kubernetes/benchmetric",
"//test/kubernetes/k8sctx",
"//test/kubernetes/testcluster",
"@io_k8s_api//core/v1:go_default_library",
"@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library",
],
)
go_test(
name = "ffmpeg_test",
srcs = ["ffmpeg_test.go"],
tags = [
"local",
"noguitar",
"notap",
],
deps = [
"//test/kubernetes/benchmarks/profiling",
"//test/kubernetes/benchmetric",
"//test/kubernetes/k8sctx",
"//test/kubernetes/testcluster",
"@io_k8s_api//core/v1:go_default_library",
"@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library",
],
)
go_test(
name = "grpc_test",
srcs = ["grpc_test.go"],
tags = [
"local",
"noguitar",
"notap",
],
deps = [
"//test/kubernetes/benchmarks/profiling",
"//test/kubernetes/benchmetric",
"//test/kubernetes/k8sctx",
"//test/kubernetes/testcluster",
"@io_k8s_api//core/v1:go_default_library",
"@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library",
],
)
go_test(
name = "nginx_test",
srcs = ["nginx_test.go"],
tags = [
"local",
"noguitar",
"notap",
],
deps = [
"//test/kubernetes/benchmarks/httpbench",
"//test/kubernetes/benchmarks/profiling",
"//test/kubernetes/k8sctx",
"//test/kubernetes/testcluster",
"@io_k8s_api//core/v1:go_default_library",
"@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library",
"@io_k8s_apimachinery//pkg/util/intstr:go_default_library",
],
)
go_test(
name = "postgresql_test",
srcs = ["postgresql_test.go"],
tags = [
"local",
"noguitar",
"notap",
],
deps = [
"//test/kubernetes/benchmarks/profiling",
"//test/kubernetes/benchmetric",
"//test/kubernetes/k8sctx",
"//test/kubernetes/testcluster",
"@io_k8s_api//core/v1:go_default_library",
"@io_k8s_apimachinery//pkg/util/intstr:go_default_library",
],
)
go_test(
name = "tensorflow_test",
srcs = ["tensorflow_test.go"],
tags = [
"local",
"noguitar",
"notap",
],
deps = [
"//test/kubernetes/benchmarks/profiling",
"//test/kubernetes/benchmetric",
"//test/kubernetes/k8sctx",
"//test/kubernetes/testcluster",
"@io_k8s_api//core/v1:go_default_library",
"@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library",
],
)
go_test(
name = "wordpress_test",
srcs = ["wordpress_test.go"],
tags = [
"local",
"noguitar",
"notap",
],
deps = [
"//test/kubernetes/benchmarks/httpbench",
"//test/kubernetes/benchmarks/profiling",
"//test/kubernetes/k8sctx",
"//test/kubernetes/testcluster",
"@io_k8s_api//core/v1:go_default_library",
"@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library",
"@io_k8s_apimachinery//pkg/util/intstr:go_default_library",
],
)
go_test(
name = "pytorch_test",
srcs = ["pytorch_test.go"],
tags = [
"local",
"noguitar",
"notap",
],
deps = [
"//test/kubernetes/benchmarks/profiling",
"//test/kubernetes/benchmetric",
"//test/kubernetes/k8sctx",
"//test/kubernetes/testcluster",
"@io_k8s_api//core/v1:go_default_library",
],
)
go_test(
name = "ollama_test",
srcs = ["ollama_test.go"],
embedsrcs = [
"//test/kubernetes/benchmarks/resources:files", # keep
],
tags = [
"local",
"noguitar",
"notap",
],
deps = [
"//test/gpu/ollama",
"//test/kubernetes/benchmetric",
"//test/kubernetes/k8sctx",
"//test/kubernetes/testcluster",
"@io_k8s_api//core/v1:go_default_library",
"@io_k8s_apimachinery//pkg/api/resource:go_default_library",
"@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library",
"@io_k8s_apimachinery//pkg/util/intstr:go_default_library",
],
)
go_test(
name = "stablediffusion_test",
srcs = ["stablediffusion_test.go"],
tags = [
"local",
"noguitar",
"notap",
],
deps = [
"//test/gpu/stablediffusion",
"//test/kubernetes/benchmetric",
"//test/kubernetes/k8sctx",
"//test/kubernetes/testcluster",
"@io_k8s_api//core/v1:go_default_library",
"@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library",
],
)
go_test(
name = "gsutil_test",
srcs = ["gsutil_test.go"],
tags = [
"local",
"noguitar",
"notap",
],
deps = [
"//test/kubernetes/benchmarks/profiling",
"//test/kubernetes/benchmetric",
"//test/kubernetes/k8sctx",
"//test/kubernetes/testcluster",
"@io_k8s_api//core/v1:go_default_library",
"@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library",
],
)
+45
View File
@@ -0,0 +1,45 @@
# gVisor Kubernetes benchmarks
The benchmarks in this directory are meant to run on a Kubernetes cluster
configured to run either gVisor or non-gVisor pods. These tests cannot run on
their own.
They currently only work for GKE clusters created with a specific set of
nodepools, which are used to distinguish which nodes the workload under test
runs. Specifically, you need:
* A set of 1 or more nodes where cluster-wide system dependencies will run,
such that all the other nodepools below do not run these cluster-wide
dependencies in order to avoid adding noise to the benchmarks.
* A set of 1 or more nodes labeled `nodepool-type=test-runtime-nodepool` and
`cloud.google.com/gke-nodepool=test-runtime-nodepool`, where the workloads
under test will run (e.g. `redis-server` for the Redis benchmark), and no
other.
* These nodes should also be labeled `runtime=$RUNTIME`, where `$RUNTIME`
is one of `runc`, `gvisor`, or their GPU/TPU-specific alternatives (see
`testcluster/objects.go`). This will determine the default runtime used
when benchmarking.
* A set of 1 or more nodes labeled `nodepool-type=client-nodepool` and
`cloud.google.com/gke-nodepool=client-nodepool`, where clients of the
workloads under test will run (e.g. `redis-benchmark` for the Redis
benchmark), and no other.
* These nodes may also be labeled `runtime=$RUNTIME`, but in most cases
this should be `runc` to mimic the common case of non-gVisor-sandboxed
clients.
* Optional: A set of 1 or more nodes labeled `nodepool-type=tertiary-nodepool`
and `cloud.google.com/gke-nodepool=tertiary-nodepool`, where backend
dependencies of the workload under test will run (e.g. the MariaDB database
for the WordPress/PHP benchmark), and no other (no cluster-wide system
dependencies should run there to minimize benchmark noise).
* These nodes may also be labeled `runtime=$RUNTIME`; this label will have
the same behavior as it does on other nodepools.
The cluster should also support setting `runtimeClassName` to `gvisor` to run
gVisor-sandboxed pods.
Once this cluster exists, dump its
[cluster proto](https://github.com/googleapis/googleapis/blob/master/google/container/v1/cluster_service.proto)
to a file, and ensure you can run workloads on this cluster via `kubectl`. Then,
you should be able to run benchmarks by pointing `--cluter-proto-path` to this
file, and `--kubectl-context-name` (set to the `kubectl` context name that
connects to this cluster in your `kubectl` config).
@@ -0,0 +1,211 @@
// Copyright 2024 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 abslbuild_test
import (
"context"
"fmt"
"path"
"strings"
"testing"
"gvisor.dev/gvisor/test/kubernetes/benchmarks/profiling"
"gvisor.dev/gvisor/test/kubernetes/benchmetric"
"gvisor.dev/gvisor/test/kubernetes/k8sctx"
"gvisor.dev/gvisor/test/kubernetes/testcluster"
v13 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1"
)
const (
imageAMD = "gcr.io/gvisor-presubmit/benchmarks/absl_x86_64:latest"
)
// TestABSLBuild benchmarks building various Abseil C++ targets.
func TestABSLBuild(t *testing.T) {
ctx := context.Background()
k8sCtx, err := k8sctx.Context(ctx)
if err != nil {
t.Fatalf("Failed to get kubernetes context: %v", err)
}
k8sCtx.ForEachCluster(ctx, t, func(cluster *testcluster.TestCluster) {
t.Run("ABSL", func(t *testing.T) {
t.Parallel()
doABSLBuild(ctx, t, k8sCtx, cluster)
})
})
}
func doABSLBuild(ctx context.Context, t *testing.T, k8sCtx k8sctx.KubernetesContext, cluster *testcluster.TestCluster) {
benchmarkNS := cluster.Namespace(testcluster.NamespaceBenchmark)
if err := benchmarkNS.Reset(ctx); err != nil {
t.Fatalf("cannot reset namespace: %v", err)
}
defer benchmarkNS.Cleanup(ctx)
const name = "absl"
persistentVol := benchmarkNS.GetPersistentVolume(name, "30Gi")
persistentVol, err := cluster.CreatePersistentVolume(ctx, persistentVol)
if err != nil {
t.Fatalf("Failed to create persistent volume: %v", err)
}
defer cluster.DeletePersistentVolume(ctx, persistentVol)
image := imageAMD
if cluster.RuntimeTestNodepoolIsARM() {
t.Skipf("Building ABSL is not supported on ARM")
return
}
if image, err = k8sCtx.ResolveImage(ctx, image); err != nil {
t.Fatalf("Failed to resolve image: %v", err)
}
for _, test := range []struct {
name string
volume *v13.Volume
}{
{
name: "RootFS",
volume: nil,
},
{
name: "EmptyDir",
volume: &v13.Volume{
Name: "emptydir",
VolumeSource: v13.VolumeSource{
EmptyDir: &v13.EmptyDirVolumeSource{},
},
},
},
{
name: "PersistentVolume",
volume: &v13.Volume{
Name: persistentVol.GetName(),
VolumeSource: v13.VolumeSource{
PersistentVolumeClaim: &v13.PersistentVolumeClaimVolumeSource{
ClaimName: persistentVol.GetName(),
},
},
},
},
} {
t.Run(test.name, func(t *testing.T) {
endProfiling, err := profiling.MaybeSetup(ctx, t, cluster, benchmarkNS)
if err != nil {
t.Fatalf("Failed to setup profiling: %v", err)
}
defer endProfiling()
pod := newABSLPod(benchmarkNS, name, image, test.volume)
pod, err = cluster.ConfigurePodForRuntimeTestNodepool(pod)
if err != nil {
t.Fatalf("Failed to set pod for test runtime: %v", err)
}
pod, err = testcluster.MaybeSetContainerResources(pod, name, testcluster.ContainerResourcesRequest{})
if err != nil {
t.Fatalf("Failed to set container resources: %v", err)
}
pod, err = cluster.CreatePod(ctx, pod)
if err != nil {
t.Fatalf("Failed to create pod: %v", err)
}
defer cluster.DeletePod(ctx, pod)
containerDuration, err := benchmetric.GetTimedContainerDuration(ctx, cluster, pod, name)
if err != nil {
t.Fatalf("Failed to get container duration: %v", err)
}
recorder, err := benchmetric.GetRecorder(ctx)
if err != nil {
t.Fatalf("Failed to initialize benchmark recorder: %v", err)
}
if err := recorder.Record(ctx, fmt.Sprintf("ABSL/%s", test.name), benchmetric.BenchmarkDuration(containerDuration)); err != nil {
t.Fatalf("Failed to record benchmark data: %v", err)
}
})
}
}
func newABSLPod(namespace *testcluster.Namespace, name, image string, volume *v13.Volume) *v13.Pod {
const workdir = "/workdir"
initCommand := []string{
"sh",
"-c",
strings.Join([]string{
"mkdir", "-p", workdir,
"&&",
"cp", "-r", "/abseil-cpp", fmt.Sprintf("%s/.", workdir),
}, " "),
}
command := []string{
"bazel",
"build",
"//absl/base",
"//absl/algorithm",
"//absl/container/...",
"//absl/debugging/...",
"//absl/flags:flag",
"//absl/hash",
"//absl/memory",
"//absl/meta:type_traits",
"//absl/numeric:int128",
"//absl/strings",
"//absl/synchronization",
"//absl/time",
"//absl/types/...",
"//absl/utility",
}
var volumes []v13.Volume
var volumeMounts []v13.VolumeMount
if volume != nil {
volumes = []v13.Volume{*volume}
volumeMounts = []v13.VolumeMount{{
MountPath: workdir,
Name: volume.Name,
}}
}
return &v13.Pod{
TypeMeta: v1.TypeMeta{
Kind: "Pod",
APIVersion: "v1",
},
ObjectMeta: v1.ObjectMeta{
Name: name,
Namespace: namespace.Namespace,
},
Spec: v13.PodSpec{
Volumes: volumes,
Containers: []v13.Container{
{
Name: name,
Image: image,
Command: benchmetric.CommandThenTimed(initCommand, path.Join(workdir, "abseil-cpp"), command),
VolumeMounts: volumeMounts,
},
},
RestartPolicy: v13.RestartPolicyNever,
},
}
}
func TestMain(m *testing.M) {
k8sctx.TestMain(m, map[string]k8sctx.TestFunc{
"TestABSLBuild": TestABSLBuild,
})
}
+199
View File
@@ -0,0 +1,199 @@
// Copyright 2024 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 ffmpeg_test
import (
"context"
"fmt"
"strings"
"testing"
"gvisor.dev/gvisor/test/kubernetes/benchmarks/profiling"
"gvisor.dev/gvisor/test/kubernetes/benchmetric"
"gvisor.dev/gvisor/test/kubernetes/k8sctx"
"gvisor.dev/gvisor/test/kubernetes/testcluster"
v13 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1"
)
const (
imageAMD = "gcr.io/gvisor-presubmit/benchmarks/ffmpeg_x86_64:latest"
imageARM = "gcr.io/gvisor-presubmit/benchmarks/ffmpeg_aarch64:latest"
ffmpegContainerName = "ffmpeg"
)
func TestFfmpeg(t *testing.T) {
ctx := context.Background()
k8sCtx, err := k8sctx.Context(ctx)
if err != nil {
t.Fatalf("Failed to get kubernetes context: %v", err)
}
k8sCtx.ForEachCluster(ctx, t, func(cluster *testcluster.TestCluster) {
t.Run("ffmpeg", func(t *testing.T) {
t.Parallel()
doFfmpegTest(ctx, t, k8sCtx, cluster)
})
})
}
func doFfmpegTest(ctx context.Context, t *testing.T, k8sCtx k8sctx.KubernetesContext, cluster *testcluster.TestCluster) {
benchmarkNS := cluster.Namespace(testcluster.NamespaceBenchmark)
if err := benchmarkNS.Reset(ctx); err != nil {
t.Fatalf("cannot reset namespace: %v", err)
}
defer benchmarkNS.Cleanup(ctx)
const name = "ffmpeg"
// create persistent volume
persistentVol := benchmarkNS.GetPersistentVolume(name, "30Gi")
persistentVol, err := cluster.CreatePersistentVolume(ctx, persistentVol)
if err != nil {
t.Fatalf("Failed to create persistent volume: %v", err)
}
defer cluster.DeletePersistentVolume(ctx, persistentVol)
image := imageAMD
if cluster.RuntimeTestNodepoolIsARM() {
image = imageARM
}
if image, err = k8sCtx.ResolveImage(ctx, image); err != nil {
t.Fatalf("Failed to resolve image: %v", err)
}
for _, test := range []struct {
name string
volume *v13.Volume
}{
{
name: "RootFS",
volume: nil,
},
{
name: "EmptyDir",
volume: &v13.Volume{
Name: "emptydir",
VolumeSource: v13.VolumeSource{
EmptyDir: &v13.EmptyDirVolumeSource{},
},
},
},
{
name: "PersistentVolume",
volume: &v13.Volume{
Name: persistentVol.GetName(),
VolumeSource: v13.VolumeSource{
PersistentVolumeClaim: &v13.PersistentVolumeClaimVolumeSource{
ClaimName: persistentVol.GetName(),
},
},
},
},
} {
t.Run(test.name, func(t *testing.T) {
endProfiling, err := profiling.MaybeSetup(ctx, t, cluster, benchmarkNS)
if err != nil {
t.Fatalf("Failed to setup profiling: %v", err)
}
defer endProfiling()
p := newFfmpegDevPod(benchmarkNS, name, image, test.volume)
p, err = cluster.ConfigurePodForRuntimeTestNodepool(p)
if err != nil {
t.Fatalf("Failed to configure pod for runtime: %v", err)
}
p, err = testcluster.MaybeSetContainerResources(p, ffmpegContainerName, testcluster.ContainerResourcesRequest{})
if err != nil {
t.Fatalf("Failed to set container resources: %v", err)
}
p, err = cluster.CreatePod(ctx, p)
if err != nil {
t.Fatalf("Failed to create pod: %v", err)
}
defer cluster.DeletePod(ctx, p)
recorder, err := benchmetric.GetRecorder(ctx)
if err != nil {
t.Fatalf("Failed to initialize benchmark recorder: %v", err)
}
containerDuration, err := benchmetric.GetTimedContainerDuration(ctx, cluster, p, ffmpegContainerName)
if err != nil {
t.Fatalf("Failed to get container duration: %v", err)
}
if recorder.Record(ctx, fmt.Sprintf("FFMPEG/%s", test.name), benchmetric.BenchmarkDuration(containerDuration)); err != nil {
t.Fatalf("Failed to record benchmark data: %v", err)
}
})
}
}
// newFfmpegPod creates a new ffmpeg dev pod spec for benchmarks.
func newFfmpegDevPod(namespace *testcluster.Namespace, name, image string, volume *v13.Volume) *v13.Pod {
const workdir = "/workdir"
initCommand := []string{
"sh",
"-c",
strings.Join([]string{
"mkdir", "-p", workdir,
"&&",
"cp", "/media/video.mp4", fmt.Sprintf("%s/.", workdir),
}, " "),
}
command := []string{
"ffmpeg",
"-i", "video.mp4",
"-c:v", "libx264",
"-preset", "veryslow",
"output.mp4",
}
var volumes []v13.Volume
var volumeMounts []v13.VolumeMount
if volume != nil {
volumes = []v13.Volume{*volume}
volumeMounts = []v13.VolumeMount{{
MountPath: workdir,
Name: volume.Name,
}}
}
return &v13.Pod{
TypeMeta: v1.TypeMeta{
Kind: "Pod",
APIVersion: "v1",
},
ObjectMeta: v1.ObjectMeta{
Name: name,
Namespace: namespace.Namespace,
},
Spec: v13.PodSpec{
Volumes: volumes,
Containers: []v13.Container{
{
Name: ffmpegContainerName,
Image: image,
Command: benchmetric.CommandThenTimed(initCommand, workdir, command),
VolumeMounts: volumeMounts,
},
},
RestartPolicy: v13.RestartPolicyNever,
},
}
}
func TestMain(m *testing.M) {
k8sctx.TestMain(m, map[string]k8sctx.TestFunc{
"TestFfmpeg": TestFfmpeg,
})
}
+192
View File
@@ -0,0 +1,192 @@
// Copyright 2024 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 grpc_test
import (
"context"
"fmt"
"path"
"strings"
"testing"
"gvisor.dev/gvisor/test/kubernetes/benchmarks/profiling"
"gvisor.dev/gvisor/test/kubernetes/benchmetric"
"gvisor.dev/gvisor/test/kubernetes/k8sctx"
"gvisor.dev/gvisor/test/kubernetes/testcluster"
v13 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1"
)
const (
imageAMD = "gcr.io/gvisor-presubmit/benchmarks/build-grpc_x86_64:latest"
imageARM = "gcr.io/gvisor-presubmit/benchmarks/build-grpc_aarch64:latest"
)
func TestGRPCBuild(t *testing.T) {
ctx := context.Background()
k8sCtx, err := k8sctx.Context(ctx)
if err != nil {
t.Fatalf("Failed to get kubernetes context: %v", err)
}
k8sCtx.ForEachCluster(ctx, t, func(cluster *testcluster.TestCluster) {
t.Run("gRPC", func(t *testing.T) {
t.Parallel()
doGRPCBuild(ctx, t, k8sCtx, cluster)
})
})
}
func doGRPCBuild(ctx context.Context, t *testing.T, k8sCtx k8sctx.KubernetesContext, cluster *testcluster.TestCluster) {
benchmarkNS := cluster.Namespace(testcluster.NamespaceBenchmark)
if err := benchmarkNS.Reset(ctx); err != nil {
t.Fatalf("cannot reset namespace: %v", err)
}
defer benchmarkNS.Cleanup(ctx)
const name = "grpc"
persistentVol := benchmarkNS.GetPersistentVolume(name, "30Gi")
persistentVol, err := cluster.CreatePersistentVolume(ctx, persistentVol)
if err != nil {
t.Fatalf("Failed to create persistent volume: %v", err)
}
defer cluster.DeletePersistentVolume(ctx, persistentVol)
image := imageAMD
if cluster.RuntimeTestNodepoolIsARM() {
image = imageARM
}
if image, err = k8sCtx.ResolveImage(ctx, image); err != nil {
t.Fatalf("Failed to resolve image: %v", err)
}
for _, test := range []struct {
name string
volume *v13.Volume
}{
{
name: "RootFS",
volume: nil,
},
{
name: "EmptyDir",
volume: &v13.Volume{
Name: "emptydir",
VolumeSource: v13.VolumeSource{
EmptyDir: &v13.EmptyDirVolumeSource{},
},
},
},
{
name: "PersistentVolume",
volume: &v13.Volume{
Name: persistentVol.GetName(),
VolumeSource: v13.VolumeSource{
PersistentVolumeClaim: &v13.PersistentVolumeClaimVolumeSource{
ClaimName: persistentVol.GetName(),
},
},
},
},
} {
t.Run(test.name, func(t *testing.T) {
endProfiling, err := profiling.MaybeSetup(ctx, t, cluster, benchmarkNS)
if err != nil {
t.Fatalf("Failed to setup profiling: %v", err)
}
defer endProfiling()
pod := newGRPCPod(benchmarkNS, name, image, test.volume)
pod, err = cluster.ConfigurePodForRuntimeTestNodepool(pod)
if err != nil {
t.Fatalf("Failed to set pod for test runtime: %v", err)
}
pod, err = testcluster.MaybeSetContainerResources(pod, name, testcluster.ContainerResourcesRequest{})
if err != nil {
t.Fatalf("Failed to set container resources: %v", err)
}
pod, err = cluster.CreatePod(ctx, pod)
if err != nil {
t.Fatalf("Failed to create pod: %v", err)
}
defer cluster.DeletePod(ctx, pod)
recorder, err := benchmetric.GetRecorder(ctx)
if err != nil {
t.Fatalf("Failed to initialize benchmark recorder: %v", err)
}
containerDuration, err := benchmetric.GetTimedContainerDuration(ctx, cluster, pod, name)
if err != nil {
t.Fatalf("Failed to get container duration: %v", err)
}
if err := recorder.Record(ctx, fmt.Sprintf("gRPC/%s", test.name), benchmetric.BenchmarkDuration(containerDuration)); err != nil {
t.Fatalf("Failed to record benchmark data: %v", err)
}
})
}
}
func newGRPCPod(namespace *testcluster.Namespace, name, image string, volume *v13.Volume) *v13.Pod {
const workdir = "/workdir"
initCommand := []string{
"sh",
"-c",
strings.Join([]string{
"mkdir", "-p", workdir,
"&&",
"cp", "-r", "/grpc", fmt.Sprintf("%s/.", workdir),
}, " "),
}
command := []string{"bazel", "build", ":grpc"}
var volumes []v13.Volume
var volumeMounts []v13.VolumeMount
if volume != nil {
volumes = []v13.Volume{*volume}
volumeMounts = []v13.VolumeMount{{
MountPath: workdir,
Name: volume.Name,
}}
}
return &v13.Pod{
TypeMeta: v1.TypeMeta{
Kind: "Pod",
APIVersion: "v1",
},
ObjectMeta: v1.ObjectMeta{
Name: name,
Namespace: namespace.Namespace,
},
Spec: v13.PodSpec{
Volumes: volumes,
Containers: []v13.Container{
{
Name: name,
Image: image,
Command: benchmetric.CommandThenTimed(initCommand, path.Join(workdir, "grpc"), command),
VolumeMounts: volumeMounts,
},
},
RestartPolicy: v13.RestartPolicyNever,
},
}
}
func TestMain(m *testing.M) {
k8sctx.TestMain(m, map[string]k8sctx.TestFunc{
"TestGRPCBuild": TestGRPCBuild,
})
}
+219
View File
@@ -0,0 +1,219 @@
// Copyright 2024 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 gsutil_test is used to benchmark the speed of large (10GB)
// downloads. It is intended for comparing runsc with runc.
package gsutil_test
import (
"context"
"fmt"
"path/filepath"
"strings"
"testing"
"gvisor.dev/gvisor/test/kubernetes/benchmarks/profiling"
"gvisor.dev/gvisor/test/kubernetes/benchmetric"
"gvisor.dev/gvisor/test/kubernetes/k8sctx"
"gvisor.dev/gvisor/test/kubernetes/testcluster"
v13 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1"
)
const (
imageAMD = "us-central1-docker.pkg.dev/gvisor-presubmit/gvisor-presubmit-images/benchmarks/gsutil_x86_64:7eba9c02d11172d4"
imageARM = "us-central1-docker.pkg.dev/gvisor-presubmit/gvisor-presubmit-images/benchmarks/gsutil_aarch64:7eba9c02d11172d4"
bigfile = "gs://gvisor-benchmark-testdata/bigrandomfile"
containerName = "gsutil"
)
func TestGSUtil(t *testing.T) {
ctx := context.Background()
k8sCtx, err := k8sctx.Context(ctx)
if err != nil {
t.Fatalf("Failed to get kubernetes context: %v", err)
}
k8sCtx.ForEachCluster(ctx, t, func(cluster *testcluster.TestCluster) {
t.Run("GSUtil", func(t *testing.T) {
t.Parallel()
doGSUtilTest(ctx, t, k8sCtx, cluster)
})
})
}
func doGSUtilTest(ctx context.Context, t *testing.T, k8sCtx k8sctx.KubernetesContext, cluster *testcluster.TestCluster) {
benchmarkNS := cluster.Namespace(testcluster.NamespaceBenchmark)
if err := benchmarkNS.Reset(ctx); err != nil {
t.Fatalf("cannot reset namespace: %v", err)
}
defer benchmarkNS.Cleanup(ctx)
const name = "gsutil"
// Create persistent volume.
persistentVol := benchmarkNS.GetPersistentVolume(name, "15Gi")
persistentVol, err := cluster.CreatePersistentVolume(ctx, persistentVol)
if err != nil {
t.Fatalf("Failed to create persistent volume: %v", err)
}
defer cluster.DeletePersistentVolume(ctx, persistentVol)
image := imageAMD
if cluster.RuntimeTestNodepoolIsARM() {
image = imageARM
}
if image, err = k8sCtx.ResolveImage(ctx, image); err != nil {
t.Fatalf("Failed to resolve image: %v", err)
}
// Run tests with different volume types.
// TODO(b/361182379): Use gsutil parallel sliced downloads as a test
// dimension.
for _, storage := range []struct {
name string
volume *v13.Volume
}{
{
name: "RootFS",
volume: nil,
},
{
name: "EmptyDir",
volume: &v13.Volume{
Name: "emptydir",
VolumeSource: v13.VolumeSource{
EmptyDir: &v13.EmptyDirVolumeSource{},
},
},
},
{
name: "PersistentVolume",
volume: &v13.Volume{
Name: persistentVol.GetName(),
VolumeSource: v13.VolumeSource{
PersistentVolumeClaim: &v13.PersistentVolumeClaimVolumeSource{
ClaimName: persistentVol.GetName(),
},
},
},
},
} {
t.Run(storage.name, func(t *testing.T) {
for _, slicing := range []struct {
name string
option string
}{
{
name: "slicing=false",
option: `-o "GSUtil:sliced_object_download_threshold=0"`,
},
{
// Slicing is enabled by default, so we
// don't set any extra options.
name: "slicing=true",
},
} {
t.Run(slicing.name, func(t *testing.T) {
// Setup profiling if requested by the user.
endProfiling, err := profiling.MaybeSetup(ctx, t, cluster, benchmarkNS)
if err != nil {
t.Fatalf("Failed to setup profiling: %v", err)
}
defer endProfiling()
// Create a pod that performs setup, then times
// downloading.
p := newGSUtilDevPod(benchmarkNS, name, image, storage.volume, slicing.option)
p, err = cluster.ConfigurePodForRuntimeTestNodepool(p)
if err != nil {
t.Fatalf("Failed to configure pod for runtime: %v", err)
}
p, err = testcluster.MaybeSetContainerResources(p, containerName, testcluster.ContainerResourcesRequest{})
if err != nil {
t.Fatalf("Failed to set container resources: %v", err)
}
// GetTimedContainerDuration waits for the container to
// finish.
recorder, err := benchmetric.GetRecorder(ctx)
if err != nil {
t.Fatalf("Failed to initialize benchmark recorder: %v", err)
}
containerDuration, err := benchmetric.GetTimedContainerDuration(ctx, cluster, p, containerName)
if err != nil {
t.Fatalf("Failed to get container duration: %v", err)
}
if err := recorder.Record(ctx, fmt.Sprintf("GSUtil/%s/%s", storage.name, slicing.name), benchmetric.BenchmarkDuration(containerDuration)); err != nil {
t.Fatalf("Failed to record benchmark data: %v", err)
}
})
}
})
}
}
// newGSUtilPod creates a new gsutil dev pod spec for benchmarks.
func newGSUtilDevPod(namespace *testcluster.Namespace, name, image string, volume *v13.Volume, gsutilFlags string) *v13.Pod {
const downloadDir = "/downloads"
initCommand := []string{
"sh",
"-c",
strings.Join([]string{"mkdir", "-p", downloadDir}, " "),
}
command := []string{
"sh", "-c",
fmt.Sprintf("gsutil %s cp %s %s && sync",
gsutilFlags,
bigfile,
filepath.Join(downloadDir, "randombigfile"),
),
}
var volumes []v13.Volume
var volumeMounts []v13.VolumeMount
if volume != nil {
volumes = []v13.Volume{*volume}
volumeMounts = []v13.VolumeMount{{
MountPath: downloadDir,
Name: volume.Name,
}}
}
return &v13.Pod{
TypeMeta: v1.TypeMeta{
Kind: "Pod",
APIVersion: "v1",
},
ObjectMeta: v1.ObjectMeta{
Name: name,
Namespace: namespace.Namespace,
},
Spec: v13.PodSpec{
Volumes: volumes,
Containers: []v13.Container{
{
Name: containerName,
Image: image,
Command: benchmetric.CommandThenTimed(initCommand, "", command),
VolumeMounts: volumeMounts,
},
},
RestartPolicy: v13.RestartPolicyNever,
},
}
}
func TestMain(m *testing.M) {
k8sctx.TestMain(m, map[string]k8sctx.TestFunc{
"TestGSUtil": TestGSUtil,
})
}
@@ -0,0 +1,23 @@
load("//tools:defs.bzl", "go_library")
package(
default_applicable_licenses = ["//:license"],
default_visibility = ["//test/kubernetes:__subpackages__"],
licenses = ["notice"],
)
go_library(
name = "httpbench",
testonly = 1,
srcs = ["httpbench.go"],
visibility = [
"//test/kubernetes/benchmarks:__subpackages__",
],
deps = [
"//test/kubernetes/benchmetric",
"//test/kubernetes/testcluster",
"@io_k8s_api//core/v1:go_default_library",
"@io_k8s_apimachinery//pkg/api/resource:go_default_library",
"@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library",
],
)
@@ -0,0 +1,421 @@
// Copyright 2024 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 httpbench provides a library for benchmarking an HTTP server.
package httpbench
import (
"context"
"fmt"
"io"
"regexp"
"strconv"
"strings"
"testing"
"time"
"gvisor.dev/gvisor/test/kubernetes/benchmetric"
"gvisor.dev/gvisor/test/kubernetes/testcluster"
v13 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/apis/meta/v1"
)
// InfiniteQPS is a stand-in value for "a lot of QPS".
// Running a benchmark round at this load level amounts to saturating
// the HTTP server with load on a single connection.
const InfiniteQPS = 1 << 16
const wrk2ImageAMD = "gcr.io/gvisor-presubmit/benchmarks/wrk2_x86_64:latest"
// MetricType is a type of metric to report.
type MetricType int
// List of metric types.
const (
Latency MetricType = iota
RequestsPerSecond
BytesPerSecond
)
// Round is a single round of benchmarking.
type Round struct {
// NumThreads is the number of concurrent threads and connections to make.
NumThreads int
// TargetQPS is the aggregate load on the server that will be spread across
// the `NumThreads` threads.
TargetQPS int
// Duration is the total duration of the round. It should be longer than 10s
// as wrk2 uses the first 10 seconds as calibration period.
Duration time.Duration
// If set, only report the metric types listed here.
OnlyReport []MetricType
}
// HTTPBenchmark helps manage an HTTP-based benchmark.
// A benchmark that wishes to run an HTTP-based benchmark should set up
// the HTTP server pod and a Kubernetes Service pointing at it, and this
// library takes care of the rest.
type HTTPBenchmark struct {
// Name is the name of the benchmark. It is used as a prefix for all
// benchstat output metrics.
Name string
// Cluster is the test cluster.
Cluster *testcluster.TestCluster
// Namespace is the benchmark namespace where pods are created.
Namespace *testcluster.Namespace
// Service is a Kubernetes service pointing to the HTTP server.
Service *v13.Service
// Port is the port that the HTTP server is bound to.
Port int
// Path is the HTTP path that the benchmark should use in its requests.
// It should start by "/", e.g. "/index.html".
Path string
// Timeout is the maximum allowable duration of requests for Path.
Timeout time.Duration
// Rounds is the set of rounds to run the benchmark for. Must be non-empty.
Rounds []Round
// WantPercentiles is the list of percentiles to report.
WantPercentiles []int
}
// Run runs the HTTP-based benchmark.
func (h *HTTPBenchmark) Run(ctx context.Context, t *testing.T) {
t.Helper()
if err := h.Cluster.WaitForServiceReady(ctx, h.Service); err != nil {
t.Fatalf("Failed to wait for service: %v", err)
}
ip := testcluster.GetIPFromService(h.Service)
if ip == "" {
t.Fatalf("did not get valid ip: %s", ip)
}
if err := h.waitForServer(ctx, ip); err != nil {
t.Fatalf("Failed to wait for server: %v", err)
}
for _, round := range h.Rounds {
qpsText := fmt.Sprintf("%d", round.TargetQPS)
if round.TargetQPS == InfiniteQPS {
qpsText = "max"
}
t.Run(fmt.Sprintf("%dthreads_%sqps", round.NumThreads, qpsText), func(t *testing.T) {
h.runRound(ctx, t, round, ip)
})
}
}
// runRound runs a single round of an HTTP benchmark.
func (h *HTTPBenchmark) runRound(ctx context.Context, t *testing.T, round Round, ip string) {
t.Helper()
qpsText := fmt.Sprintf("%d", round.TargetQPS)
if round.TargetQPS == InfiniteQPS {
qpsText = "max"
}
name := fmt.Sprintf("wrk2-%dthreads-%sqps", round.NumThreads, qpsText)
client := h.newWrk2Client(name, ip, round)
client, err := h.Cluster.ConfigurePodForClientNodepool(client)
if err != nil {
t.Fatalf("failed to configure wrk2 pod for client nodepool: %v", err)
}
client, err = h.Cluster.CreatePod(ctx, client)
if err != nil {
t.Fatalf("failed to create wrk2 pod: %v", err)
}
defer h.Cluster.DeletePod(ctx, client)
if err := h.Cluster.WaitForPodCompleted(ctx, client); err != nil {
t.Fatalf("failed to wait for wrk2 pod: %v", err)
}
rdr, err := h.Cluster.GetLogReader(ctx, client, v13.PodLogOptions{})
if err != nil {
t.Fatalf("failed to get log reader: %v", err)
}
out, err := io.ReadAll(rdr)
if err != nil {
t.Fatalf("failed to read log: %v", err)
}
numRequests, data, err := getMeasurements(string(out), round.OnlyReport, h.WantPercentiles)
if err != nil {
t.Fatalf("failed to get measurement: %v", err)
}
recorder, err := benchmetric.GetRecorder(ctx)
if err != nil {
t.Fatalf("Failed to initialize benchmark recorder: %v", err)
}
if err := recorder.RecordIters(ctx, fmt.Sprintf("%s/%dThreads/%sQPS", strings.Title(h.Name), round.NumThreads, qpsText), numRequests, data...); err != nil {
t.Fatalf("Failed to record benchmark data: %v", err)
}
}
// newWrk2Client returns a new pod that benchmarks the given HTTP server.
func (h *HTTPBenchmark) newWrk2Client(name, ip string, round Round) *v13.Pod {
cmd := []string{
"wrk2",
"--threads", fmt.Sprintf("%d", round.NumThreads), // Run N threads in parallel.
"--connections", fmt.Sprintf("%d", round.NumThreads), // Each with 1 connection.
"--rate", fmt.Sprintf("%d", round.TargetQPS), // Target QPS split across all threads.
"--duration", fmt.Sprintf("%d", uint64(round.Duration.Seconds())),
"--timeout", fmt.Sprintf("%d", uint64(h.Timeout.Seconds())),
"--latency", // Print detailed latency statistics.
fmt.Sprintf("http://%s:%d%s", ip, h.Port, h.Path),
}
return &v13.Pod{
TypeMeta: v1.TypeMeta{
Kind: "Pod",
APIVersion: "v1",
},
ObjectMeta: v1.ObjectMeta{
Name: name,
Namespace: h.Namespace.Namespace,
},
Spec: v13.PodSpec{
Containers: []v13.Container{
{
Name: name,
Image: wrk2ImageAMD,
Command: cmd,
},
},
RestartPolicy: v13.RestartPolicyNever,
},
}
}
// getWgetPod creates a wget spider pod that waits for timeout on IP/port and never fails.
func (h *HTTPBenchmark) getWgetPod(ip string) *v13.Pod {
name := fmt.Sprintf("wget-%d", time.Now().UnixNano())
// We don't use h.Path in the path here because the purpose of this pod is
// only to verify that the server is up, not that the page at h.Path exists.
cmd := fmt.Sprintf("wget --spider -T 10 http://%s:%d/", ip, h.Port)
return &v13.Pod{
TypeMeta: v1.TypeMeta{
Kind: "Pod",
APIVersion: "v1",
},
ObjectMeta: v1.ObjectMeta{
Name: name,
Namespace: h.Namespace.Namespace,
},
Spec: v13.PodSpec{
Containers: []v13.Container{
{
Name: name,
Image: "alpine",
Command: []string{"/bin/sh", "-c", cmd},
Resources: v13.ResourceRequirements{
Requests: v13.ResourceList{
v13.ResourceCPU: resource.MustParse("500m"),
},
},
},
},
RestartPolicy: v13.RestartPolicyNever,
},
}
}
// waitForServer waits for an HTTP server to start responding on the given
// IP and port.
func (h *HTTPBenchmark) waitForServer(ctx context.Context, ip string) error {
wget, err := h.Cluster.ConfigurePodForClientNodepool(h.getWgetPod(ip))
if err != nil {
return fmt.Errorf("failed to configure wget pod for client nodepool: %v", err)
}
wget, err = h.Cluster.CreatePod(ctx, wget)
if err != nil {
return fmt.Errorf("failed to create wget pod: %v", err)
}
defer h.Cluster.DeletePod(ctx, wget)
waitCtx, waitCancel := context.WithTimeout(ctx, 1*time.Minute)
defer waitCancel()
if err := h.Cluster.WaitForPodCompleted(waitCtx, wget); err != nil {
return fmt.Errorf("failed to wait for HTTP server %s:%d%s: %v", ip, h.Port, h.Path, err)
}
return nil
}
/*
Sample wrk2 output:
Running 30s test @ http://google.com
2 threads and 2 connections
Thread calibration: mean lat.: 25.351ms, rate sampling interval: 55ms
Thread calibration: mean lat.: 26.040ms, rate sampling interval: 56ms
Thread Stats Avg Stdev Max +/- Stdev
Latency 25.34ms 7.16ms 137.73ms 96.88%
Req/Sec 19.75 6.40 53.00 86.63%
Latency Distribution (HdrHistogram - Recorded Latency)
50.000% 24.09ms
75.000% 25.50ms
90.000% 27.50ms
99.000% 58.85ms
99.900% 111.74ms
99.990% 137.85ms
99.999% 137.85ms
100.000% 137.85ms
Detailed Percentile spectrum:
Value Percentile TotalCount 1/(1-Percentile)
20.783 0.000000 1 1.00
[...]
137.855 1.000000 800 inf
#[Mean = 25.341, StdDeviation = 7.155]
#[Max = 137.728, Total count = 800]
#[Buckets = 27, SubBuckets = 2048]
----------------------------------------------------------
1200 requests in 30.01s, 2.22MB read
Requests/sec: 39.99
Transfer/sec: 75.60KB
*/
var (
wrk2TotalRequestsRe = regexp.MustCompile(`^\s*([,\d]+) requests in .*$`)
wrk2LatencyPercentileRE = regexp.MustCompile(`^\s*(\d+\.?\d+?)%\s+([,\d]+\.?\d+?\w+)\s*$`)
wrk2ReqPerSecondRE = regexp.MustCompile(`^Requests/sec:\s*([,\d]+\.?\d+?)\s*$`)
wrk2TransferPerSecondRE = regexp.MustCompile(`^Transfer/sec:\s*([,\d]+\.?\d+?\w+)\s*$`)
)
// getMeasurements parses wrk2 output.
// It returns the number of requests that were made, and benchmark data.
func getMeasurements(data string, onlyReport []MetricType, wantPercentiles []int) (int, []benchmetric.MetricValue, error) {
report := func(m MetricType) bool {
if len(onlyReport) == 0 {
return true
}
for _, typ := range onlyReport {
if typ == m {
return true
}
}
return false
}
var metricValues []benchmetric.MetricValue
var totalRequests int
for _, line := range strings.Split(data, "\n") {
if match := wrk2TotalRequestsRe.FindStringSubmatch(line); match != nil {
gotRequests, err := strconv.ParseInt(strings.ReplaceAll(match[1], ",", ""), 10, 64)
if err != nil {
return 0, nil, fmt.Errorf("failed to parse %q from line %q: %v", match[1], line, err)
}
if totalRequests != 0 {
return 0, nil, fmt.Errorf("found multiple lines matching 'total requests' regex: %d vs %d (%q)", totalRequests, gotRequests, line)
}
totalRequests = int(gotRequests)
continue
}
if match := wrk2LatencyPercentileRE.FindStringSubmatch(line); match != nil {
pctile, err := strconv.ParseFloat(match[1], 64)
if err != nil {
return 0, nil, fmt.Errorf("failed to parse %q from line %q as float: %v", match[1], line, err)
}
wantPctile := 0
for _, want := range wantPercentiles {
if want*1e3 == int(pctile*1e3) {
wantPctile = want
break
}
}
if wantPctile == 0 {
continue
}
latency, err := time.ParseDuration(strings.ReplaceAll(match[2], ",", ""))
if err != nil {
return 0, nil, fmt.Errorf("failed to parse %q from line %q as duration: %v", match[2], line, err)
}
if report(Latency) {
metricValues = append(metricValues, benchmetric.SpecificDuration(latency, fmt.Sprintf("p%d", wantPctile)))
}
continue
}
if match := wrk2ReqPerSecondRE.FindStringSubmatch(line); match != nil {
qps, err := strconv.ParseFloat(strings.ReplaceAll(match[1], ",", ""), 64)
if err != nil {
return 0, nil, fmt.Errorf("failed to parse %q from line %q as float: %v", match[1], line, err)
}
if report(RequestsPerSecond) {
metricValues = append(metricValues, benchmetric.RequestsPerSecond(qps))
}
continue
}
if match := wrk2TransferPerSecondRE.FindStringSubmatch(line); match != nil {
bps, err := parseTransfer(match[1])
if err != nil {
return 0, nil, fmt.Errorf("failed to parse %q from line %q: %v", match[1], line, err)
}
if report(BytesPerSecond) {
metricValues = append(metricValues, benchmetric.BytesPerSecond(bps))
}
continue
}
}
if totalRequests == 0 {
return 0, nil, fmt.Errorf("could not find total requests in output: %q", data)
}
return totalRequests, metricValues, nil
}
// parseTransfer parses a string like "75.60KB" in the output above,
// and returns a bandwidth rate in bytes/sec.
func parseTransfer(s string) (float64, error) {
s = strings.ReplaceAll(s, ",", "")
var multiplier uint64
var suffix string
for unit, m := range map[string]uint64{
"KB": 1000,
"KiB": 1024,
"MB": 1000 * 1000,
"MiB": 1024 * 1024,
"GB": 1000 * 1000 * 1000,
"GiB": 1024 * 1024 * 1024,
"TB": 1000 * 1000 * 1000 * 1000,
"TiB": 1024 * 1024 * 1024 * 1024,
} {
if strings.HasSuffix(s, unit) {
suffix = unit
multiplier = m
break
}
}
if multiplier == 0 {
if !strings.HasSuffix(s, "B") {
return 0, fmt.Errorf("failed to parse %q: found no unit suffix", s)
}
// Otherwise, it's just bytes/sec.
// But we can't put this in the for loop above, otherwise it would
// match every suffix ("KB" ends in "B").
suffix = "B"
multiplier = 1
}
s = strings.TrimSuffix(s, suffix)
floatPart, err := strconv.ParseFloat(s, 64)
if err != nil {
return 0, fmt.Errorf("failed to parse %q as float: %v", s, err)
}
return floatPart * float64(multiplier), nil
}
+309
View File
@@ -0,0 +1,309 @@
// Copyright 2024 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 nginx_test
import (
"context"
"fmt"
"strings"
"testing"
"time"
"gvisor.dev/gvisor/test/kubernetes/benchmarks/httpbench"
"gvisor.dev/gvisor/test/kubernetes/benchmarks/profiling"
"gvisor.dev/gvisor/test/kubernetes/k8sctx"
"gvisor.dev/gvisor/test/kubernetes/testcluster"
v13 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
)
const (
nginxImageAMD = "gcr.io/gvisor-presubmit/benchmarks/nginx_x86_64:latest"
nginxImageARM = "gcr.io/gvisor-presubmit/benchmarks/nginx_aarch64:latest"
nginxPort = 80
nginxBenchmarkDuration = 70 * time.Second
nginxRequestTimeout = 3 * time.Second
nginxServingDir = "/tmp/html"
nginxServerLabelKey = "app.kubernetes.io/name"
nginxServerLabelValue = "nginx-server"
)
var (
// nginxCommand is the main server command.
// The test expects that it contains the files to be served at /local,
// and will serve files out of `nginxServingDir`.
nginxCommand = []string{"nginx", "-c", "/etc/nginx/nginx.conf"}
nginxDocKibibytes = []int{1, 10, 100, 10240}
threads = []int{1, 8, 64, 1000}
targetQPS = []int{1, 8, 64, httpbench.InfiniteQPS}
wantPercentiles = []int{50, 95, 99}
)
func TestNginx(t *testing.T) {
ctx := context.Background()
k8sCtx, err := k8sctx.Context(ctx)
if err != nil {
t.Fatalf("Failed to get kubernetes context: %v", err)
}
k8sCtx.ForEachCluster(ctx, t, func(cluster *testcluster.TestCluster) {
t.Run("nginx", func(t *testing.T) {
t.Parallel()
doNginxTest(ctx, t, k8sCtx, cluster)
})
})
}
func doNginxTest(ctx context.Context, t *testing.T, k8sCtx k8sctx.KubernetesContext, cluster *testcluster.TestCluster) {
benchmarkNS := cluster.Namespace(testcluster.NamespaceBenchmark)
if err := benchmarkNS.Reset(ctx); err != nil {
t.Fatalf("cannot reset namespace: %v", err)
}
defer benchmarkNS.Cleanup(ctx)
nginxImage := nginxImageAMD
if cluster.RuntimeTestNodepoolIsARM() {
nginxImage = nginxImageARM
}
nginxImage, err := k8sCtx.ResolveImage(ctx, nginxImage)
if err != nil {
t.Fatalf("Failed to resolve image: %v", err)
}
persistentVol, err := cluster.CreatePersistentVolume(ctx, benchmarkNS.GetPersistentVolume("nginx-data", "30Gi"))
if err != nil {
t.Fatalf("Failed to create persistent volume: %v", err)
}
defer cluster.DeletePersistentVolume(ctx, persistentVol)
for _, test := range []struct {
// Name of the test.
name string
// Suffix for pod names, must be short enough.
suffix string
// Volume to mount at /tmp/root.
volume *v13.Volume
}{
{
name: "RootFS",
suffix: "rootfs",
volume: nil,
},
{
name: "EmptyDir",
suffix: "emdir",
volume: &v13.Volume{
Name: "emptydir",
VolumeSource: v13.VolumeSource{
EmptyDir: &v13.EmptyDirVolumeSource{},
},
},
},
{
name: "PersistentVolume",
suffix: "pvol",
volume: &v13.Volume{
Name: persistentVol.GetName(),
VolumeSource: v13.VolumeSource{
PersistentVolumeClaim: &v13.PersistentVolumeClaimVolumeSource{
ClaimName: persistentVol.GetName(),
},
},
},
},
} {
t.Run(test.name, func(t *testing.T) {
endProfiling, err := profiling.MaybeSetup(ctx, t, cluster, benchmarkNS)
if err != nil {
t.Fatalf("Failed to setup profiling: %v", err)
}
defer endProfiling()
name := fmt.Sprintf("nginx-%s", test.suffix)
server := newNginxServer(benchmarkNS, name, nginxImage, test.volume)
server, err = cluster.ConfigurePodForRuntimeTestNodepool(server)
if err != nil {
t.Fatalf("Failed to configure pod for runtime nodepool: %v", err)
}
server, err = testcluster.MaybeSetContainerResources(server, name, testcluster.ContainerResourcesRequest{})
if err != nil {
t.Fatalf("Failed to set container resources: %v", err)
}
server, err = cluster.CreatePod(ctx, server)
if err != nil {
t.Fatalf("Failed to create pod: %v", err)
}
defer cluster.DeletePod(ctx, server)
if err := cluster.WaitForPodRunning(ctx, server); err != nil {
t.Fatalf("Failed to wait for pod: %v", err)
}
service := newNginxService(benchmarkNS, name)
service, err = cluster.CreateService(ctx, service)
if err != nil {
t.Fatalf("Failed to create service: %v", err)
}
defer cluster.DeleteService(ctx, service)
var rounds []httpbench.Round
for _, numThreads := range threads {
for _, qps := range targetQPS {
if qps < numThreads {
continue
}
var onlyReport []httpbench.MetricType
// If we're testing at max QPS, only report throughput,
// because all requests will time out.
// Otherwise, only report latency, because the throughput
// is exactly determined by the QPS target anyway.
if qps == httpbench.InfiniteQPS {
onlyReport = append(onlyReport, httpbench.RequestsPerSecond)
onlyReport = append(onlyReport, httpbench.BytesPerSecond)
} else {
onlyReport = append(onlyReport, httpbench.Latency)
}
rounds = append(rounds, httpbench.Round{
NumThreads: numThreads,
TargetQPS: qps,
Duration: nginxBenchmarkDuration,
OnlyReport: onlyReport,
})
}
}
t.Run("0KiB", func(t *testing.T) {
benchmark := &httpbench.HTTPBenchmark{
Name: fmt.Sprintf("nginx/%s/0KiB", test.name),
Cluster: cluster,
Namespace: benchmarkNS,
Service: service,
Port: nginxPort,
Path: "/index.html",
Rounds: rounds,
Timeout: nginxRequestTimeout,
WantPercentiles: wantPercentiles,
}
benchmark.Run(ctx, t)
})
for _, docKibibytes := range nginxDocKibibytes {
t.Run(fmt.Sprintf("%dKiB", docKibibytes), func(t *testing.T) {
benchmark := &httpbench.HTTPBenchmark{
Name: fmt.Sprintf("nginx/%s/%dKiB", test.name, docKibibytes),
Cluster: cluster,
Namespace: benchmarkNS,
Service: service,
Port: nginxPort,
Path: fmt.Sprintf("/latin%dk.txt", docKibibytes),
Rounds: rounds,
Timeout: nginxRequestTimeout,
WantPercentiles: wantPercentiles,
}
benchmark.Run(ctx, t)
})
}
t.Run("HTTP404", func(t *testing.T) {
benchmark := &httpbench.HTTPBenchmark{
Name: fmt.Sprintf("nginx/%s/HTTP404", test.name),
Cluster: cluster,
Namespace: benchmarkNS,
Service: service,
Port: nginxPort,
Path: "/404-this-page-does-not-exist.html",
Rounds: rounds,
Timeout: nginxRequestTimeout,
WantPercentiles: wantPercentiles,
}
benchmark.Run(ctx, t)
})
})
if t.Failed() {
break
}
}
}
func newNginxServer(namespace *testcluster.Namespace, name, image string, volume *v13.Volume) *v13.Pod {
var volumes []v13.Volume
var volumeMounts []v13.VolumeMount
if volume != nil {
volumes = []v13.Volume{*volume}
volumeMounts = []v13.VolumeMount{{
MountPath: nginxServingDir,
Name: volume.Name,
}}
}
return &v13.Pod{
TypeMeta: v1.TypeMeta{
Kind: "Pod",
APIVersion: "v1",
},
ObjectMeta: v1.ObjectMeta{
Name: name,
Namespace: namespace.Namespace,
Labels: map[string]string{nginxServerLabelKey: nginxServerLabelValue},
},
Spec: v13.PodSpec{
Containers: []v13.Container{
{
Name: name,
Image: image,
Command: []string{
"sh",
"-c",
strings.Join([]string{
strings.Join([]string{"mkdir", "-p", nginxServingDir}, " "),
strings.Join([]string{
"cp", "-r", "/local/*", fmt.Sprintf("%s/.", nginxServingDir),
}, " "),
strings.Join(nginxCommand, " "),
}, " && "),
},
VolumeMounts: volumeMounts,
Ports: []v13.ContainerPort{
{
Name: name,
ContainerPort: nginxPort,
},
},
},
},
Volumes: volumes,
RestartPolicy: v13.RestartPolicyNever,
},
}
}
func newNginxService(namespace *testcluster.Namespace, name string) *v13.Service {
return namespace.GetService(name, v13.ServiceSpec{
Selector: map[string]string{nginxServerLabelKey: nginxServerLabelValue},
Ports: []v13.ServicePort{
{
Name: name,
Protocol: v13.ProtocolTCP,
Port: nginxPort,
TargetPort: intstr.FromString(name),
},
},
})
}
func TestMain(m *testing.M) {
k8sctx.TestMain(m, map[string]k8sctx.TestFunc{
"TestNginx": TestNginx,
})
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,392 @@
// Copyright 2024 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 postgresql_test benchmarks a PostgreSQL database.
package postgresql_test
import (
"context"
"fmt"
"io"
"regexp"
"strconv"
"strings"
"testing"
"time"
"gvisor.dev/gvisor/test/kubernetes/benchmarks/profiling"
"gvisor.dev/gvisor/test/kubernetes/benchmetric"
"gvisor.dev/gvisor/test/kubernetes/k8sctx"
"gvisor.dev/gvisor/test/kubernetes/testcluster"
v13 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/intstr"
)
const (
postgresServerLabelKey = "app.kubernetes.io/name"
postgresServerLabelValue = "postgresql-server"
postgresPort = 5432
postgresImage = "postgres:15.3-alpine"
postgresUser = "benchman"
postgresPassword = "hunter2"
postgresDatabase = "benchpress"
postgresVolumeDir = "/var/lib/postgresql/data"
postgresDataDir = "/var/lib/postgresql/data/pgdata"
)
var (
numConnections = []int{1, 2, 6, 16, 32, 64}
)
// TestPostgresPGBench benchmarks a PostgreSQL database with pgbench.
func TestPostgresPGBench(t *testing.T) {
ctx := context.Background()
k8sCtx, err := k8sctx.Context(ctx)
if err != nil {
t.Fatalf("Failed to get kubernetes context: %v", err)
}
k8sCtx.ForEachCluster(ctx, t, func(cluster *testcluster.TestCluster) {
t.Run("PostgresPGBench", func(t *testing.T) {
t.Parallel()
doPostgresPGBenchTest(ctx, t, k8sCtx, cluster)
})
})
}
// doPostgresTest runs a PostgreSQL pgbench test.
func doPostgresPGBenchTest(ctx context.Context, t *testing.T, k8sCtx k8sctx.KubernetesContext, cluster *testcluster.TestCluster) {
benchmarkNS := cluster.Namespace(testcluster.NamespaceBenchmark)
if err := benchmarkNS.Reset(ctx); err != nil {
t.Fatalf("cannot reset namespace: %v", err)
}
defer benchmarkNS.Cleanup(ctx)
endProfiling, err := profiling.MaybeSetup(ctx, t, cluster, benchmarkNS)
if err != nil {
t.Fatalf("Failed to setup profiling: %v", err)
}
defer endProfiling()
// Create a persistent volume on which to store the database data.
persistentVol := benchmarkNS.GetPersistentVolume("pgdata", "30Gi")
persistentVol, err = cluster.CreatePersistentVolume(ctx, persistentVol)
if err != nil {
t.Fatalf("failed to create persistent volume: %v", err)
}
defer cluster.DeletePersistentVolume(ctx, persistentVol)
// Create a server on the runtime under test nodepool.
image, err := k8sCtx.ResolveImage(ctx, postgresImage)
if err != nil {
t.Fatalf("failed to resolve image: %v", err)
}
server := newPostgresPod(benchmarkNS, "postgresql", image, nil, true /* withPort */, persistentVol)
if server.ObjectMeta.Labels == nil {
server.ObjectMeta.Labels = make(map[string]string)
}
server.ObjectMeta.Labels[postgresServerLabelKey] = postgresServerLabelValue
server, err = cluster.ConfigurePodForRuntimeTestNodepool(server)
if err != nil {
t.Fatalf("ConfigurePodForRuntimeTestNodepool on cluster %q: %v", cluster.GetName(), err)
}
server, err = testcluster.MaybeSetContainerResources(server, server.Spec.Containers[0].Name, testcluster.ContainerResourcesRequest{})
if err != nil {
t.Fatalf("MaybeSetContainerResources on cluster %q: %v", cluster.GetName(), err)
}
server, err = cluster.CreatePod(ctx, server)
if err != nil {
t.Fatalf("CreatePod on cluster %q: %v", cluster.GetName(), err)
}
defer cluster.DeletePod(ctx, server)
if err := cluster.WaitForPodRunning(ctx, server); err != nil {
t.Fatalf("WaitForPodRunning on cluster %q pod: %q: %v", cluster.GetName(), server.GetName(), err)
}
// Create a service to service traffic to the pod.
service := newPostgresService(benchmarkNS, server.GetName())
service, err = cluster.CreateService(ctx, service)
if err != nil {
t.Fatalf("CreateService on cluster %q: %v", cluster.GetName(), err)
}
defer cluster.DeleteService(ctx, service)
if err := cluster.WaitForServiceReady(ctx, service); err != nil {
t.Fatalf("WaitForServiceReady on cluster %q: %v", cluster.GetName(), err)
}
ip := testcluster.GetIPFromService(service)
if ip == "" {
t.Fatalf("did not get valid ip: %s", ip)
}
// Run the 'pg_isready' command to ping the server and make sure it is up.
ensureUp := func() error {
pgIsReadyName := "pgisready"
pgIsReady := newPostgresPod(benchmarkNS, pgIsReadyName, image, []string{
"pg_isready",
"--timeout=10",
fmt.Sprintf("--host=%s", ip),
fmt.Sprintf("--port=%d", postgresPort),
fmt.Sprintf("--username=%s", postgresUser),
fmt.Sprintf("--dbname=%s", postgresDatabase),
}, false /* withPort */, nil /* pvc */)
pgIsReady, err = cluster.ConfigurePodForClientNodepool(pgIsReady)
if err != nil {
return fmt.Errorf("ConfigurePodForClientNodepool on cluster %q: pod: %q: %v", cluster.GetName(), pgIsReadyName, err)
}
pgIsReady, err = cluster.CreatePod(ctx, pgIsReady)
if err != nil {
return fmt.Errorf("CreatePod %q on cluster %q: %v", pgIsReady.GetName(), cluster.GetName(), err)
}
defer cluster.DeletePod(ctx, pgIsReady)
waitCtx, waitCancel := context.WithTimeout(ctx, 20*time.Second)
defer waitCancel()
if err := cluster.WaitForPodCompleted(waitCtx, pgIsReady); err != nil {
return fmt.Errorf("WaitForPodCompleted on cluster %q pod: %q: %v", cluster.GetName(), pgIsReadyName, err)
}
return nil
}
var isUpErr error
for i := 0; i < 5; i++ {
if isUpErr = ensureUp(); isUpErr == nil {
break
}
}
if isUpErr != nil {
t.Fatalf("postgresql did not come up: %v", isUpErr)
}
// pgbench has two steps: an "init step" which create and fills up a
// database with stuff, and then a main phase which does queries on that
// stuff.
// The initialization only needs to be done once per database.
initDatabase := func() error {
initDBName := "initdb"
initDB := newPostgresPod(benchmarkNS, initDBName, image, []string{
"pgbench",
"--initialize",
fmt.Sprintf("--host=%s", ip),
fmt.Sprintf("--port=%d", postgresPort),
fmt.Sprintf("--username=%s", postgresUser),
postgresDatabase,
}, false /* withPort */, nil /* pvc */)
initDB, err = cluster.ConfigurePodForClientNodepool(initDB)
if err != nil {
return fmt.Errorf("ConfigurePodForClientNodepool on cluster %q: pod: %q: %v", cluster.GetName(), initDBName, err)
}
initDB, err = cluster.CreatePod(ctx, initDB)
if err != nil {
return fmt.Errorf("CreatePod %q on cluster %q: %v", initDB.GetName(), cluster.GetName(), err)
}
defer cluster.DeletePod(ctx, initDB)
waitCtx, waitCancel := context.WithTimeout(ctx, 20*time.Second)
defer waitCancel()
if err := cluster.WaitForPodCompleted(waitCtx, initDB); err != nil {
return fmt.Errorf("WaitForPodCompleted on cluster %q pod: %q: %v", cluster.GetName(), initDBName, err)
}
return nil
}
if err := initDatabase(); err != nil {
t.Fatalf("cannot initialize database: %v", err)
}
for _, connections := range numConnections {
t.Run(fmt.Sprintf("%dClients", connections), func(t *testing.T) {
clientCmd := []string{
"pgbench",
"--time=90", // In seconds
"--report-per-command",
fmt.Sprintf("--host=%s", ip),
fmt.Sprintf("--port=%d", postgresPort),
fmt.Sprintf("--username=%s", postgresUser),
fmt.Sprintf("--client=%d", connections),
fmt.Sprintf("--jobs=%d", connections),
postgresDatabase,
}
client := newPostgresPod(benchmarkNS, "pgbench", image, clientCmd, false /* withPort */, nil /* pvc */)
client, err = cluster.ConfigurePodForClientNodepool(client)
if err != nil {
t.Fatalf("ConfigurePodForClientNodepool on cluster %q: pod: %q: %v", cluster.GetName(), client.GetName(), err)
}
client, err = cluster.CreatePod(ctx, client)
if err != nil {
t.Fatalf("CreatePod %q on cluster %q: %v", client.GetName(), cluster.GetName(), err)
}
defer cluster.DeletePod(ctx, client)
if err := cluster.WaitForPodCompleted(ctx, client); err != nil {
t.Fatalf("WaitForPodCompleted on cluster %q pod: %q: %v", cluster.GetName(), client.GetName(), err)
}
// get and parse the logs from the client to get the results
rdr, err := cluster.GetLogReader(ctx, client, v13.PodLogOptions{})
if err != nil {
t.Fatalf("GetLogReader on cluster %q: %v", cluster.GetName(), err)
}
out, err := io.ReadAll(rdr)
if err != nil {
t.Fatalf("failed to read from pod: %q: %v", client.GetName(), err)
}
postgresBenchmarkName := fmt.Sprintf("PostgresPGBench/%dClients", connections)
recorder, err := benchmetric.GetRecorder(ctx)
if err != nil {
t.Fatalf("Failed to initialize benchmark recorder: %v", err)
}
metrics, err := getMeasurements(string(out))
if err != nil {
t.Fatalf("failed to get metrics: out:\n\n%s\n\nerr: %v", string(out), err)
}
if err := recorder.Record(ctx, postgresBenchmarkName, metrics...); err != nil {
t.Fatalf("Failed to record benchmark data: %v", err)
}
})
}
}
// newPostgresService gets a service to serve traffic to the PostgreSQL server.
func newPostgresService(namespace *testcluster.Namespace, containerName string) *v13.Service {
name := fmt.Sprintf("postgresql-service-%d", time.Now().UnixNano())
return namespace.GetService(name, v13.ServiceSpec{
Selector: map[string]string{postgresServerLabelKey: postgresServerLabelValue},
Ports: []v13.ServicePort{
{
Name: name,
Protocol: v13.ProtocolTCP,
Port: postgresPort,
TargetPort: intstr.FromInt(postgresPort),
},
},
})
}
func newPostgresPod(namespace *testcluster.Namespace, containerName, image string, argv []string, withPort bool, pvc *v13.PersistentVolumeClaim) *v13.Pod {
pod := namespace.NewPod(containerName)
container := v13.Container{
Name: containerName,
Image: image,
Command: argv,
Env: []v13.EnvVar{
// Used by postgres server:
{Name: "POSTGRES_USER", Value: postgresUser},
{Name: "POSTGRES_PASSWORD", Value: postgresPassword},
{Name: "POSTGRES_DB", Value: postgresDatabase},
{Name: "PGDATA", Value: postgresDataDir},
// Used by pgbench:
{Name: "PGPASSWORD", Value: postgresPassword},
{Name: "sslmode", Value: "disable"},
},
}
if withPort {
container.Ports = append(container.Ports, v13.ContainerPort{ContainerPort: postgresPort})
}
if pvc != nil {
pod.Spec.Volumes = append(pod.Spec.Volumes, v13.Volume{
Name: pvc.GetName(),
VolumeSource: v13.VolumeSource{
PersistentVolumeClaim: &v13.PersistentVolumeClaimVolumeSource{
ClaimName: pvc.GetName(),
},
},
})
container.VolumeMounts = append(container.VolumeMounts, v13.VolumeMount{
MountPath: postgresVolumeDir,
Name: pvc.GetName(),
})
}
pod.Spec.Containers = append(pod.Spec.Containers, container)
return pod
}
var (
latencyRegex = regexp.MustCompile("^latency average = ([-,.\\d]+ .?s)$")
initialConnectionRegex = regexp.MustCompile("^initial connection time = ([-,.\\d]+ .?s)$")
tpsRegex = regexp.MustCompile("^tps = ([-,.\\d]+) \\(without initial connection time\\)$")
)
func stringToFloat64(s string) float64 {
f, err := strconv.ParseFloat(strings.ReplaceAll(s, ",", ""), 64)
if err != nil {
panic(fmt.Sprintf("cannot convert float %q: %v", s, err))
}
return f
}
func stringToDuration(s string) time.Duration {
parts := strings.SplitN(s, " ", 2)
floatStr, unit := parts[0], parts[1]
floatPart := stringToFloat64(floatStr)
switch unit {
case "s":
return time.Duration(floatPart * float64(time.Second))
case "ms":
return time.Duration(floatPart * float64(time.Millisecond))
case "us", "μs":
return time.Duration(floatPart * float64(time.Microsecond))
case "ns":
return time.Duration(floatPart * float64(time.Nanosecond))
default:
panic(fmt.Sprintf("unknown time unit %q", unit))
}
}
// getMeasurements parses the output of pgbench to get the stats.
func getMeasurements(out string) ([]benchmetric.MetricValue, error) {
var foundLatency, foundInitialConnection, foundTPS benchmetric.MetricValue
for _, line := range strings.Split(out, "\n") {
line = strings.TrimSpace(line)
if latencyMatch := latencyRegex.FindStringSubmatch(line); latencyMatch != nil {
if foundLatency != nil {
return nil, fmt.Errorf("found duplicate latency data: %v vs %q", foundLatency, line)
}
foundLatency = benchmetric.SpecificDuration(stringToDuration(latencyMatch[1]), "avg")
}
if initialConnectionMatch := initialConnectionRegex.FindStringSubmatch(line); initialConnectionMatch != nil {
if foundInitialConnection != nil {
return nil, fmt.Errorf("found duplicate initial connection data: %v vs %q", foundInitialConnection, line)
}
foundInitialConnection = benchmetric.SpecificDuration(stringToDuration(initialConnectionMatch[1]), "init")
}
if tpsMatch := tpsRegex.FindStringSubmatch(line); tpsMatch != nil {
if foundTPS != nil {
return nil, fmt.Errorf("found duplicate TPS data: %v vs %q", foundTPS, line)
}
foundTPS = benchmetric.RequestsPerSecond(stringToFloat64(tpsMatch[1]))
}
}
if foundLatency == nil || foundInitialConnection == nil || foundTPS == nil {
return nil, fmt.Errorf("did not find the data we wanted: foundLatency=%v foundInitialConnection=%v foundTPS=%v", foundLatency, foundInitialConnection, foundTPS)
}
return []benchmetric.MetricValue{
foundLatency,
foundInitialConnection,
foundTPS,
}, nil
}
func TestMain(m *testing.M) {
k8sctx.TestMain(m, map[string]k8sctx.TestFunc{
"TestPostgresPGBench": TestPostgresPGBench,
})
}
@@ -0,0 +1,28 @@
load("//tools:defs.bzl", "go_library")
package(
default_applicable_licenses = ["//:license"],
default_visibility = ["//test/kubernetes:__subpackages__"],
licenses = ["notice"],
)
go_library(
name = "profiling",
testonly = 1,
srcs = ["profiling.go"],
visibility = [
"//test/kubernetes:__subpackages__",
],
deps = [
"//pkg/atomicbitops",
"//runsc/flag",
"//test/kubernetes/testcluster",
"//test/metricsviz",
"@com_github_google_pprof//profile:go_default_library",
"@io_k8s_api//apps/v1:go_default_library",
"@io_k8s_api//core/v1:go_default_library",
"@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library",
"@org_golang_google_protobuf//proto:go_default_library",
"@org_golang_x_time//rate:go_default_library",
],
)
File diff suppressed because it is too large Load Diff
+426
View File
@@ -0,0 +1,426 @@
// Copyright 2024 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 pytorch_test contains benchmarks using the pytorch "torchbench" repo.
package pytorch_test
// These tests use pytorch's "torchbench" suite (https://github.com/pytorch/benchmark/tree/main).
// The Authors describe the benchmarks in this paper: https://arxiv.org/pdf/2304.14226.pdf
// The Authors list both the type of model and its profile (how GPU intensive).
// Note: The image for this test is about 7-8 GB as of writing. After you get your clusters up and
// running, start the test and make sure that the pods show the event of downloading the image. Then
// get a cup of coffee, chat with your co-workers for 5 min, and it will be about done 5 min after
// that. You'll only need to do this once for each cluster (in parallel).
import (
"context"
"fmt"
"io"
"regexp"
"strconv"
"strings"
"testing"
"time"
"gvisor.dev/gvisor/test/kubernetes/benchmarks/profiling"
"gvisor.dev/gvisor/test/kubernetes/benchmetric"
"gvisor.dev/gvisor/test/kubernetes/k8sctx"
"gvisor.dev/gvisor/test/kubernetes/testcluster"
v13 "k8s.io/api/core/v1"
)
const (
pytorchImage = "gcr.io/gvisor-presubmit/benchmarks/pytorch_x86_64:f6f280aeb1b07989"
)
// pytorchTestType is the method used, either training or evaluation, for the model.
type pytorchTestType string
const (
train = pytorchTestType("train")
eval = pytorchTestType("eval")
)
type pytorchMode string
// pytorchMode is the pytorch mode used, either script mode (jit) or eager mode.
// See: https://towardsdatascience.com/pytorch-jit-and-torchscript-c2a77bac0fff
const (
jit = pytorchMode("jit")
eager = pytorchMode("eager")
)
type pytorchTest struct {
module string
test pytorchTestType
mode pytorchMode
}
// Name returns the name of the test with the argument parameters included. It is formatted so
// that it can be used for the name of the pod.
func (p pytorchTest) Name() string {
// Kubernetes pod names cannot contain "_".
module := strings.ReplaceAll(strings.ToLower(p.module), "_", "-")
return fmt.Sprintf("%s-%s-%s", module, p.test, p.mode)
}
var snakeCase = regexp.MustCompile("_.")
// BenchName returns the name of the test with the argument parameters included.
// It is formatted so that it can be used for benchstat output.
func (p pytorchTest) BenchName() string {
// First letter of the module should be capitalized, as it will be
// concatenated with "Benchmark" and it's useful to mark it as a different
// word.
// Some modules use a lowercase first letter, e.g. "fastNLP_Bert".
moduleName := strings.ToUpper(p.module[:1]) + p.module[1:]
// We also replace "snake_case" with "snakeCase". Sorry snakes.
moduleName = snakeCase.ReplaceAllStringFunc(moduleName, func(s string) string {
return strings.ToUpper(strings.TrimPrefix(s, "_"))
})
test := strings.ToUpper(string(p.test)[:1]) + string(p.test[1:])
var mode string
switch p.mode {
case eager:
mode = "Eager"
case jit:
mode = "JIT"
default:
panic(fmt.Sprintf("Unknown mode: %v", p.mode))
}
return fmt.Sprintf("%s/%s/%s", moduleName, test, mode)
}
func (p pytorchTest) toPod(namespace *testcluster.Namespace, image string) (*v13.Pod, error) {
pod := namespace.NewPod(p.Name())
pod.Spec = v13.PodSpec{
RestartPolicy: v13.RestartPolicyNever,
Containers: []v13.Container{
{
Name: p.Name(),
Image: pytorchImage,
Command: benchmetric.TimedCommand(p.command()...),
},
},
}
return pod, nil
}
func (p pytorchTest) command() []string {
return []string{
"python",
"run.py",
p.module,
"--device", "cuda",
"--test", string(p.test),
"--mode", string(p.mode),
}
}
// TestFastNLPBert uses the fastNLP_Bert module, which is classified as a NLP Language Model.
// fastNLP_Bert taxes the GPU heavily with low data movement. See Figure 2 on
// page 5: https://arxiv.org/pdf/2304.14226.pdf
//
// https://github.com/pytorch/benchmark/tree/main/torchbenchmark/models/fastNLP_Bert
// Bert Blog Post: https://towardsdatascience.com/bert-explained-state-of-the-art-language-model-for-nlp-f8b21a9b6270
// Paper: https://arxiv.org/abs/1810.04805
func TestFastNLPBert(t *testing.T) {
ctx := context.Background()
const module = "fastNLP_Bert"
tests := []pytorchTest{
{
module: module,
test: train,
mode: eager,
},
{
module: module,
test: eval,
mode: eager,
},
}
runTests(ctx, t, tests)
}
// TestBigBird uses the hf_BigBird module, which is classified as a NLP Language Model.
// hf_BigBird taxes the GPU moderately with low data movement. See Figure 2 on
// page 5 (speech_tf): https://arxiv.org/pdf/2304.14226.pdf
//
// https://github.com/pytorch/benchmark/tree/main/torchbenchmark/models/hf_BigBird
// Paper: https://arxiv.org/abs/2007.14062
func TestBigBird(t *testing.T) {
ctx := context.Background()
const module = "hf_BigBird"
tests := []pytorchTest{
{
module: module,
test: train,
mode: eager,
},
{
module: module,
test: eval,
mode: eager,
},
}
runTests(ctx, t, tests)
}
// TestSpeechTransformer uses the speech_transformer module classified as "Speech Recognition"
// model. speech_transformer has a lot of idle time for the GPU. See Figure 2 on
// page 5 (speech_tf): https://arxiv.org/pdf/2304.14226.pdf
//
// https://github.com/pytorch/benchmark/pull/374
// Paper: https://arxiv.org/abs/1706.03762
func TestSpeechTransformer(t *testing.T) {
ctx := context.Background()
const module = "speech_transformer"
tests := []pytorchTest{
{
module: module,
test: train,
mode: eager,
},
{
module: module,
test: eval,
mode: eager,
},
}
runTests(ctx, t, tests)
}
// TestLearningToPaint uses the LearningToPaint module classified as "neural renderer in model-based
// Deep Reinforcement Learning (DRL)".
// Learning to paint has a lot of "data movement" and doesn't tax the GPU a lot. See Figure 2 on
// page 5: https://arxiv.org/pdf/2304.14226.pdf
//
// https://github.com/pytorch/benchmark/tree/main/torchbenchmark/models/LearningToPaint
func TestLearningToPaint(t *testing.T) {
ctx := context.Background()
const module = "LearningToPaint"
tests := []pytorchTest{
{
module: module,
test: train,
mode: jit,
},
{
module: module,
test: eval,
mode: jit,
},
}
runTests(ctx, t, tests)
}
// TestMobileNetV2 uses the mobilenet_v2 module classified as "Computer Vision: Image Classification".
// MobileNet has a lot of taxes the GPU. See Figure 2 on page 5: https://arxiv.org/pdf/2304.14226.pdf
//
// https://github.com/pytorch/benchmark/tree/main/torchbenchmark/models/mobilenet_v2
// Paper: https://paperswithcode.com/method/mobilenetv2
func TestMobileNetV2(t *testing.T) {
ctx := context.Background()
const module = "mobilenet_v2"
tests := []pytorchTest{
{
module: module,
test: train,
mode: jit,
},
{
module: module,
test: eval,
mode: jit,
},
}
runTests(ctx, t, tests)
}
// TestBackgroundMatting uses the Background_Matting module classified as "Computer Vision: Pattern Recognition".
// BackgroundMatting has a lot of GPU idle time. See Figure 2 on page 5: https://arxiv.org/pdf/2304.14226.pdf
//
// https://github.com/pytorch/benchmark/tree/main/torchbenchmark/models/Background_Matting (see README)
func TestBackgroundMatting(t *testing.T) {
ctx := context.Background()
const module = "Background_Matting"
tests := []pytorchTest{
{
module: module,
test: train,
mode: eager,
},
{
module: module,
test: eval,
mode: eager,
},
}
runTests(ctx, t, tests)
}
func runTests(ctx context.Context, t *testing.T, tests []pytorchTest) {
k8sCtx, err := k8sctx.Context(ctx)
if err != nil {
t.Fatalf("Failed to get kubernetes context: %v", err)
}
k8sCtx.ForEachCluster(ctx, t, func(cluster *testcluster.TestCluster) {
t.Run("PyTorch", func(t *testing.T) {
t.Parallel()
for _, p := range tests {
t.Run(p.Name(), func(t *testing.T) {
doPytorchRun(ctx, t, k8sCtx, cluster, p)
})
}
})
})
}
func doPytorchRun(ctx context.Context, t *testing.T, k8sCtx k8sctx.KubernetesContext, cluster *testcluster.TestCluster, params pytorchTest) {
benchmarkNS := cluster.Namespace(testcluster.NamespaceBenchmark)
endProfiling, err := profiling.MaybeSetup(ctx, t, cluster, benchmarkNS)
if err != nil {
t.Fatalf("Failed to setup profiling: %v", err)
}
defer endProfiling()
if err := benchmarkNS.Reset(ctx); err != nil {
t.Fatalf("Failed to reset namespace: %v", err)
}
defer benchmarkNS.Cleanup(ctx)
image, err := k8sCtx.ResolveImage(ctx, pytorchImage)
if err != nil {
t.Fatalf("Failed to resolve image: %v", err)
}
pod, err := params.toPod(benchmarkNS, image)
if err != nil {
t.Fatalf("Failed to create pod: %v", err)
}
pod, err = cluster.ConfigurePodForRuntimeTestNodepool(pod)
if err != nil {
t.Fatalf("Failed to configure pod for test-nodepool: %v", err)
}
pod, err = testcluster.MaybeSetContainerResources(pod, pod.Name, testcluster.ContainerResourcesRequest{GPU: true})
if err != nil {
t.Fatalf("Failed to set container resources: %v", err)
}
pod, err = cluster.CreatePod(ctx, pod)
if err != nil {
t.Fatalf("Failed to create pod: %v", err)
}
defer cluster.DeletePod(ctx, pod)
if err := cluster.WaitForPodCompleted(ctx, pod); err != nil {
t.Fatalf("Failed to wait for pod to complete: %v", err)
}
rdr, err := cluster.GetLogReader(ctx, pod, v13.PodLogOptions{})
if err != nil {
t.Fatalf("GetLogReader on cluster %q pod %v: %v", cluster.GetName(), pod.GetName(), err)
}
out, err := io.ReadAll(rdr)
if err != nil {
t.Fatalf("failed to read from pod: %q: %v", pod.GetName(), err)
}
metrics, err := parseStandardOutput(string(out))
if err != nil {
t.Fatalf("parseStandardOutput: %v", err)
}
containerDuration, err := benchmetric.ParseTimedContainerOutput(string(out))
if err != nil {
t.Fatalf("Failed to get container duration: %v", err)
}
metrics = append(metrics, benchmetric.BenchmarkDuration(containerDuration))
recorder, err := benchmetric.GetRecorder(ctx)
if err != nil {
t.Fatalf("Failed to initialize benchmark recorder: %v", err)
}
if err := recorder.Record(ctx, params.BenchName(), metrics...); err != nil {
t.Fatalf("Failed to record benchmark data: %v", err)
}
}
func parseStandardOutput(output string) ([]benchmetric.MetricValue, error) {
gpuTimeMillis, err := parseGPUTime(output)
if err != nil {
return nil, fmt.Errorf("parseGPUTime: %v", err)
}
gpuPeakMemoryGB, err := parseGPUPeakMemoryGB(output)
if err != nil {
return nil, fmt.Errorf("parseGPUPeakMemory: %v", err)
}
cpuPeakMemoryGB, err := parseCPUPeakMemoryGB(output)
if err != nil {
return nil, fmt.Errorf("parseCPUPeakMemory: %v", err)
}
return []benchmetric.MetricValue{
benchmetric.SpecificDuration(time.Duration(gpuTimeMillis)*time.Millisecond, "gpu-runtime"),
benchmetric.SpecificBytes(gpuPeakMemoryGB*1024*1024*1024, "gpu-peak-memory"),
benchmetric.SpecificBytes(cpuPeakMemoryGB*1024*1024*1024, "cpu-peak-memory"),
}, nil
}
var gpuTimeRegex = regexp.MustCompile(`GPU\sTime:\s*(\d+\.\d+)\smilliseconds`)
func parseGPUTime(output string) (float64, error) {
match := gpuTimeRegex.FindStringSubmatch(output)
if len(match) < 2 {
return 0, fmt.Errorf("failed to find GPU Time: %s", output)
}
return strconv.ParseFloat(match[1], 64)
}
var gpuPeakMemoryRegex = regexp.MustCompile(`GPU\s0\sPeak\sMemory:\s*(\d+\.\d+)\sGB`)
func parseGPUPeakMemoryGB(output string) (float64, error) {
match := gpuPeakMemoryRegex.FindStringSubmatch(output)
if len(match) < 2 {
return 0, fmt.Errorf("failed to find GPU Peak Memory: %s", output)
}
return strconv.ParseFloat(match[1], 64)
}
var cpuPeakMemoryRegex = regexp.MustCompile(`CPU\sPeak\sMemory:\s*(\d+\.\d+)\sGB`)
func parseCPUPeakMemoryGB(output string) (float64, error) {
match := cpuPeakMemoryRegex.FindStringSubmatch(output)
if len(match) < 2 {
return 0, fmt.Errorf("failed to find CPU Peak Memory: %s", output)
}
return strconv.ParseFloat(match[1], 64)
}
func TestMain(m *testing.M) {
k8sctx.TestMain(m, map[string]k8sctx.TestFunc{
"TestFastNLPBert": TestFastNLPBert,
"TestBigBird": TestBigBird,
"TestSpeechTransformer": TestSpeechTransformer,
"TestLearningToPaint": TestLearningToPaint,
"TestMobileNetV2": TestMobileNetV2,
"TestBackgroundMatting": TestBackgroundMatting,
})
}
+474
View File
@@ -0,0 +1,474 @@
// Copyright 2024 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 redis_test holds the redis test where the runtime under test runs a redis server and the
// native runtime runs a client making requests against it.
package redis_test
import (
"context"
"fmt"
"io"
"math"
"regexp"
"strconv"
"strings"
"testing"
"time"
"gvisor.dev/gvisor/test/kubernetes/benchmarks/profiling"
"gvisor.dev/gvisor/test/kubernetes/benchmetric"
"gvisor.dev/gvisor/test/kubernetes/k8sctx"
"gvisor.dev/gvisor/test/kubernetes/testcluster"
v13 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/intstr"
)
const (
redisPort = 6379
defaultRequestsPerConnection = 50000
redisImageAMD = "gcr.io/gvisor-presubmit/benchmarks/redis_x86_64:latest"
redisImageARM = "gcr.io/gvisor-presubmit/benchmarks/redis_aarch64:latest"
redisServerLabelKey = "app.kubernetes.io/name"
redisServerLabelValue = "redis-server"
redisVolumeName = "redis-data"
redisDataDirectory = "/redis-data"
)
var (
numConnections = []int{1, 2, 4, 8, 16, 32}
latencyPercentiles = []int{50, 95, 99}
operations = []string{"SET", "GET", "MSET", "LPUSH", "LRANGE_500"}
)
// TestRedis benchmarks redis servers on k8s clusters.
func TestRedis(t *testing.T) {
ctx := context.Background()
k8sCtx, err := k8sctx.Context(ctx)
if err != nil {
t.Fatalf("Failed to get kubernetes context: %v", err)
}
k8sCtx.ForEachCluster(ctx, t, func(cluster *testcluster.TestCluster) {
t.Run("Redis", func(t *testing.T) {
t.Parallel()
doRedisTest(ctx, t, k8sCtx, cluster)
})
})
}
// doRedisTest runs redis test.
func doRedisTest(ctx context.Context, t *testing.T, k8sCtx k8sctx.KubernetesContext, cluster *testcluster.TestCluster) {
benchmarkNS := cluster.Namespace(testcluster.NamespaceBenchmark)
if err := benchmarkNS.Reset(ctx); err != nil {
t.Fatalf("cannot reset namespace: %v", err)
}
defer benchmarkNS.Cleanup(ctx)
persistentVol := benchmarkNS.GetPersistentVolume(redisVolumeName, "30Gi")
persistentVol, err := cluster.CreatePersistentVolume(ctx, persistentVol)
if err != nil {
t.Fatalf("Failed to create persistent volume: %v", err)
}
defer cluster.DeletePersistentVolume(ctx, persistentVol)
image := redisImageAMD
if cluster.RuntimeTestNodepoolIsARM() {
image = redisImageARM
}
if image, err = k8sCtx.ResolveImage(ctx, image); err != nil {
t.Fatalf("Failed to resolve image: %v", err)
}
for _, test := range []struct {
// Benchmark name
name string
// Suffix for the redis server, must be short to fit in pod name.
suffix string
// redis-server command-line.
serverCommand []string
// Volume to use for persistence, if any.
volume *v13.PersistentVolumeClaim
}{
{
name: "Persistence",
suffix: "persist",
serverCommand: []string{
"redis-server",
"--dir", redisDataDirectory,
// Default save settings per
// https://redis.io/docs/management/config-file/
"--save", "3600 1 300 100 60 10000",
},
volume: persistentVol,
},
{
name: "NoPersistence",
suffix: "nopersist",
serverCommand: []string{
"redis-server",
"--appendonly", "no",
"--save", "",
},
volume: nil,
},
} {
t.Run(test.name, func(t *testing.T) {
endProfiling, err := profiling.MaybeSetup(ctx, t, cluster, benchmarkNS)
if err != nil {
t.Fatalf("Failed to setup profiling: %v", err)
}
defer endProfiling()
// Create a server on the runtime under test nodepool.
server := newRedisPodWithPort(benchmarkNS, fmt.Sprintf("redis-%s", test.suffix), image, test.serverCommand, redisPort, test.volume)
if server.ObjectMeta.Labels == nil {
server.ObjectMeta.Labels = make(map[string]string)
}
server.ObjectMeta.Labels[redisServerLabelKey] = redisServerLabelValue
server, err = cluster.ConfigurePodForRuntimeTestNodepool(server)
if err != nil {
t.Fatalf("ConfigurePodForRuntimeTestNodepool on cluster %q: %v", cluster.GetName(), err)
}
server, err = testcluster.MaybeSetContainerResources(server, server.Spec.Containers[0].Name, testcluster.ContainerResourcesRequest{})
if err != nil {
t.Fatalf("MaybeSetContainerResources on cluster %q: %v", cluster.GetName(), err)
}
server, err = cluster.CreatePod(ctx, server)
if err != nil {
t.Fatalf("CreatePod on cluster %q: %v", cluster.GetName(), err)
}
defer cluster.DeletePod(ctx, server)
if err := cluster.WaitForPodRunning(ctx, server); err != nil {
t.Fatalf("WaitForPodRunning on cluster %q pod: %q: %v", cluster.GetName(), server.GetName(), err)
}
// create a service to service traffic to the pod.
service := newRedisService(benchmarkNS, server.GetName())
service, err = cluster.CreateService(ctx, service)
if err != nil {
t.Fatalf("CreateService on cluster %q: %v", cluster.GetName(), err)
}
defer cluster.DeleteService(ctx, service)
if err := cluster.WaitForServiceReady(ctx, service); err != nil {
t.Fatalf("WaitForServiceReady on cluster %q: %v", cluster.GetName(), err)
}
ip := testcluster.GetIPFromService(service)
if ip == "" {
t.Fatalf("did not get valid ip: %s", ip)
}
// run the 'redis-cli' command to ping the server and make sure it is up. The "ping" request comes
// back with a "PONG" response. We repeat -r=5 times with a -i=1 second interval. If we
// get one PONG back then the server is considered up.
pingCmd := []string{"redis-cli", "-h", ip, "-r", "5", "-i", "1", "ping"}
ensureUp := func() error {
pinger := newRedisPod(benchmarkNS, fmt.Sprintf("rpinger-%s", test.suffix), image, pingCmd)
pinger, err = cluster.ConfigurePodForClientNodepool(pinger)
if err != nil {
return fmt.Errorf("ConfigurePodForClientNodepool on cluster %q: pod: %q: %v", cluster.GetName(), pinger.GetName(), err)
}
pinger, err = cluster.CreatePod(ctx, pinger)
if err != nil {
return fmt.Errorf("CreatePod %q on cluster %q: %v", pinger.GetName(), cluster.GetName(), err)
}
defer cluster.DeletePod(ctx, pinger)
waitCtx, waitCancel := context.WithTimeout(ctx, 30*time.Second)
var podWaitSuffix string
if err := cluster.WaitForPodCompleted(waitCtx, pinger); err != nil {
podWaitSuffix = fmt.Sprintf(" (pod wait error: %v)", err)
}
waitCancel()
rdr, err := cluster.GetLogReader(ctx, pinger, v13.PodLogOptions{})
if err != nil {
return fmt.Errorf("GetLogReader on cluster %q: %v%s", cluster.GetName(), err, podWaitSuffix)
}
out, err := io.ReadAll(rdr)
if err != nil {
return fmt.Errorf("failed to read from pod: %q: %v%s", pinger.GetName(), err, podWaitSuffix)
}
if !strings.Contains(string(out), "PONG") {
return fmt.Errorf("mismatched output: wanted: PONG got: %q%s", string(out), podWaitSuffix)
}
return nil
}
var isUpErr error
serverUpCtx, serverUpCancel := context.WithTimeout(ctx, 100*time.Second)
defer serverUpCancel()
for serverUpCtx.Err() == nil {
if isUpErr = ensureUp(); isUpErr == nil {
break
}
}
if isUpErr != nil {
t.Fatalf("%s at IP %s did not come up: %v", server.GetName(), ip, isUpErr)
}
for _, connections := range numConnections {
t.Run(fmt.Sprintf("Connections_%d", connections), func(t *testing.T) {
for _, operation := range operations {
t.Run(operation, func(t *testing.T) {
// Create a client for this client run w/ the specified number of connections.
// Sadly the --csv mode only reports QPS, not latency. In order to report both,
// we need to parse the human-readable version of the output.
clientCmd := []string{
"redis-benchmark",
"-t", operation, // RPC to benchmark
"-h", ip, // Redis server IP
"-n", fmt.Sprintf("%d", defaultRequestsPerConnection*connections), // Number of total requests to do
"-c", fmt.Sprintf("%d", connections), // Number of threads to spread them over.
"-r", "1000", // Key space size (larger = more memory faults)
"--precision", "4", // Floating-point precision for reporting latency (in ms)
}
client := newRedisPod(benchmarkNS, "client", image, clientCmd)
client, err = cluster.ConfigurePodForClientNodepool(client)
if err != nil {
t.Fatalf("ConfigurePodForClientNodepool on cluster %q: pod: %q: %v", cluster.GetName(), client.GetName(), err)
}
client, err = cluster.CreatePod(ctx, client)
if err != nil {
t.Fatalf("CreatePod %q on cluster %q: %v", client.GetName(), cluster.GetName(), err)
}
defer cluster.DeletePod(ctx, client)
if err := cluster.WaitForPodCompleted(ctx, client); err != nil {
t.Fatalf("WaitForPodCompleted on cluster %q pod: %q: %v", cluster.GetName(), client.GetName(), err)
}
// get and parse the logs from the client to get the results
rdr, err := cluster.GetLogReader(ctx, client, v13.PodLogOptions{})
if err != nil {
t.Fatalf("GetLogReader on cluster %q: %v", cluster.GetName(), err)
}
out, err := io.ReadAll(rdr)
if err != nil {
t.Fatalf("failed to read from pod: %q: %v", client.GetName(), err)
}
recorder, err := benchmetric.GetRecorder(ctx)
if err != nil {
t.Fatalf("Failed to initialize benchmark recorder: %v", err)
}
redisBenchmarkName := fmt.Sprintf("Redis/%s/%dClients/%s", test.name, connections, operation)
metrics, err := getMeasurements(string(out), operation)
if err != nil {
// Redis uses '\r' to update its status by overwriting the current line.
// If printed directly, this messes up the output.
// To make that clear, we replace '\r' with a literal
// backslash + 'r', and add a newline.
humanReadableOut := strings.ReplaceAll(string(out), "\r", "\\r\n")
t.Fatalf("failed to get metric for op %q: out:\n\n%s\n\nerr: %v", operation, humanReadableOut, err)
}
// We don't multiply `defaultRequestsPerConnection` by `connections` here
// because the number of "samples" we're testing is the number of times we
// can call an RPC from *that many connections* (which is part of the
// benchmark name).
// Adding 5x the number of connections does not make the sample size of this
// benchmark go 5x higher.
if err := recorder.RecordIters(ctx, redisBenchmarkName, defaultRequestsPerConnection, metrics...); err != nil {
t.Fatalf("Failed to record benchmark data for op %q: %v", operation, err)
}
})
if t.Failed() {
break
}
}
})
if t.Failed() {
break
}
}
})
if t.Failed() {
break
}
}
}
// newRedisService gets a service to serve traffic to the redis server.
func newRedisService(namespace *testcluster.Namespace, containerName string) *v13.Service {
name := fmt.Sprintf("redis-service-%d", time.Now().UnixNano())
return namespace.GetService(name, v13.ServiceSpec{
Selector: map[string]string{redisServerLabelKey: redisServerLabelValue},
Ports: []v13.ServicePort{
{
Name: name,
Protocol: v13.ProtocolTCP,
Port: redisPort,
TargetPort: intstr.FromString(containerName),
},
},
})
}
var (
latencyPercentileRegex = regexp.MustCompile("^([-,.\\d]+)% <=? ([-,.\\d]+) milliseconds(?: \\(cumulative count .*\\))?$")
latencyStartHeader = "Latency by percentile distribution:"
queriesPerSecondRegex = regexp.MustCompile("^throughput summary: ([-,.\\d]+) requests per second$")
)
func stringToFloat64(s string) float64 {
f, err := strconv.ParseFloat(strings.ReplaceAll(s, ",", ""), 64)
if err != nil {
panic(fmt.Sprintf("cannot convert float %q: %v", s, err))
}
return f
}
// getMeasurements parses the output of redis-benchmark to get the stats.
func getMeasurements(out, operation string) ([]benchmetric.MetricValue, error) {
var currentOperation string
var returned []benchmetric.MetricValue
inLatencyBlock := false
foundPercentiles := make(map[int]bool, len(latencyPercentiles))
foundQPS := false
lastPercentile := -1.0
lastPercentileLatencyMs := math.NaN()
for _, line := range strings.Split(out, "\n") {
line = strings.TrimSpace(line)
// The human-readable output contains a bunch of data like
// "OPERATION: number\r" which are used to update the result
// interactively. Strip them out here.
if strings.Contains(line, "\r") {
line = line[strings.LastIndex(line, "\r")+1:]
}
if strings.HasPrefix(line, "====== ") {
currentOperation = strings.SplitN(strings.Trim(line, "= "), " ", 2)[0]
}
if currentOperation != operation {
continue
}
if line == latencyStartHeader {
inLatencyBlock = true
continue
}
if inLatencyBlock {
latencyMatch := latencyPercentileRegex.FindStringSubmatch(line)
if latencyMatch != nil {
percentile := stringToFloat64(latencyMatch[1])
if percentile < lastPercentile {
continue
}
latencyMs := stringToFloat64(latencyMatch[2])
if percentile == 0 {
lastPercentile = 0
lastPercentileLatencyMs = latencyMs
continue
}
// Look for all percentiles in `wantPercentiles` that are in the range [lastPercentile,
// percentile].
var recordPercentiles []int
for _, wantPercentile := range latencyPercentiles {
if float64(wantPercentile) < lastPercentile {
continue
}
if float64(wantPercentile) > percentile {
continue
}
if foundPercentiles[wantPercentile] {
continue
}
recordPercentiles = append(recordPercentiles, wantPercentile)
}
for _, recordPercentile := range recordPercentiles {
// Linear interpolation of the latency value from within the latency range in the two
// percentile values that we got.
// For example, given p50=1.0ms and p70=2.0ms, we infer that p60=1.5ms.
// This isn't bulletproof but it is better than rounding to either end of the bucket.
rangeFraction := (float64(recordPercentile) - lastPercentile) / (percentile - lastPercentile)
pctileLatency := rangeFraction*(latencyMs-lastPercentileLatencyMs) + lastPercentileLatencyMs
returned = append(returned, benchmetric.SpecificDuration(time.Duration(pctileLatency*float64(time.Millisecond)), fmt.Sprintf("p%d", recordPercentile)))
foundPercentiles[recordPercentile] = true
}
// Update values for next round.
lastPercentile = percentile
lastPercentileLatencyMs = latencyMs
} else {
inLatencyBlock = false
}
continue
}
qpsMatch := queriesPerSecondRegex.FindStringSubmatch(line)
if qpsMatch != nil {
if foundQPS {
return nil, fmt.Errorf("found QPS value multiple times: %q", line)
}
foundQPS = true
returned = append(returned, benchmetric.RequestsPerSecond(stringToFloat64(qpsMatch[1])))
}
}
if !foundQPS || len(foundPercentiles) != len(latencyPercentiles) {
return nil, fmt.Errorf("did not find the data we wanted: foundQPS=%v foundPercentiles=%v", foundQPS, foundPercentiles)
}
return returned, nil
}
// newRedisPodWithPort returns a redis pod template.
func newRedisPodWithPort(namespace *testcluster.Namespace, name, image string, cmd []string, port int32, pvc *v13.PersistentVolumeClaim) *v13.Pod {
container := newRedisContainer(name, image, cmd)
container.Ports = append(container.Ports, v13.ContainerPort{Name: name, ContainerPort: port})
if pvc != nil {
container.VolumeMounts = append(container.VolumeMounts, v13.VolumeMount{
Name: redisVolumeName,
MountPath: redisDataDirectory,
})
}
pod := namespace.NewPod(name)
pod.Spec.Containers = []v13.Container{container}
if pvc != nil {
pod.Spec.Volumes = append(pod.Spec.Volumes, v13.Volume{
Name: redisVolumeName,
VolumeSource: v13.VolumeSource{
PersistentVolumeClaim: &v13.PersistentVolumeClaimVolumeSource{
ClaimName: pvc.GetName(),
},
},
})
}
return pod
}
// newRedisPod returns a redis pod template.
func newRedisPod(namespace *testcluster.Namespace, name, image string, cmd []string) *v13.Pod {
pod := namespace.NewPod(name)
pod.Spec.Containers = []v13.Container{newRedisContainer(name, image, cmd)}
return pod
}
// newRedisContainer returns a new redis container.
func newRedisContainer(name, image string, cmd []string) v13.Container {
return v13.Container{
Name: name,
Image: image,
Command: cmd,
}
}
func TestMain(m *testing.M) {
k8sctx.TestMain(m, map[string]k8sctx.TestFunc{
"TestRedis": TestRedis,
})
}
@@ -0,0 +1,13 @@
package(
default_applicable_licenses = ["//:license"],
default_visibility = ["//test/kubernetes:__subpackages__"],
licenses = ["notice"],
)
filegroup(
name = "files",
srcs = glob(["*"]),
visibility = [
"//test/kubernetes/benchmarks:__subpackages__",
],
)
Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Some files were not shown because too many files have changed in this diff Show More