This repository contains a collection of tools, pipelines and documentation for assessing Harvester performance and benchmark results.
hvperf is a CLI for running performance, capacity and benchmark test suites
against SUSE Harvester clusters.
- A kubeconfig with cluster-admin on the target Harvester cluster.
- Docker (or a compatible runtime) for running build and test targets.
- Go 1.26+ only if you build outside the container.
- The
rancher-monitoringaddon, only for the metrics cases; suites skip those cases when it is not enabled. - For
etcd-benchmark's metrics cases, the RKE2 server configetcd-expose-metricsmust be set totrue; etcd does not expose metrics otherwise. - For the
storage-kbenchsuite, the pod requires 4 CPUs to effectively run the benchmark. If the cluster has fewer than 4 CPUs available, the pod will not start.
Suites are driven from your workstation using an ordinary kubeconfig. Nothing is
installed on the Harvester nodes; suites that need on-node access (such as the
etcd benchmark) schedule a short-lived helper job in the test namespace, which
is deleted once the run is over unless --keep-alive is set.
These helper pods run privileged, since the on-node access they need can't
be had otherwise: e.g., the etcd benchmark's job pod mounts /var/lib/rancher to
reach the RKE2 etcd TLS certs, and node-capacity's daemonset pod uses hostPID
to run nsenter/lsblk against the node's disks.
Every command that talks to a cluster accepts the standard kubectl connection
flags (--kubeconfig, --context, --namespace, --server, --as, …), so
targeting a specific cluster works the same way it does with kubectl:
hvperf run node-capacity --kubeconfig ~/.kube/harvester.yaml --context prodSuites that create resources put them in the harvester-perf-system namespace,
creating the namespace if it does not exist. Pass --namespace to use a
different one.
# build ./dist/hvperf (runs the compiler inside a container, see Building)
make go/build
sudo ln -s `pwd`/dist/hvperf /usr/local/bin/hvperf # optional, for convenience
# install the benchmark tools (etcdctl, benchmark) into ~/.hvperf/tools
make tools # dev only: builds ./dist/hvperf-tools-<version>.tar.gz
hvperf tool install --tool-source ./dist/hvperf-tools-<version>.tar.gz
# what can be run
hvperf list
# client and cluster versions
hvperf version
# run a single suite
hvperf run node-capacity
# run several suites (one comma-separated argument, not a space-separated list)
hvperf run node-capacity,etcd-benchmark
# run exclusive suites one at a time because they stress the cluster and may
# temporarily degrade its performance
hvperf run storage-kbench
hvperf run etcd-benchmark| Command | Description |
|---|---|
hvperf list |
List registered suites. -o accepts table (default), name, json, yaml. |
hvperf run <suite>[,<suite>...] |
Run the named suites, given as a single comma-separated argument. An unknown name fails the run. An exclusive suite cannot be run together with other suites. -o accepts text (default), json, yaml. |
hvperf tool install -t <tar.gz> / list / uninstall [-y] |
Install, list or remove the benchmark tools in ~/.hvperf/tools. See Setting up the tools. |
hvperf version [--client-only] |
Print the client version and, unless --client-only is set, the cluster's server version. |
run also takes --keep-alive (default false), which decides what happens to
the test namespace once the run finishes. By default the namespace and
everything in it is deleted afterwards — but only when it is the default
harvester-perf-system. Pass --keep-alive=true to keep the namespace and its
resources around for inspection. A custom --namespace is never deleted and its
resources are not cleaned up; a warning is logged when cleanup is skipped.
Results are written to stdout; suite progress is logged to stderr, so
hvperf run ... -o json > results.json keeps the two streams separate. Pass
-q/--quiet to silence the progress logging, or -v <level> to raise it.
Results are printed even when a suite fails, so partial output is not lost.
Some suites (e.g. etcd-benchmark) need extra binaries such as etcdctl,
and benchmark. hvperf run reads them from ~/.hvperf/tools. Set
that directory up with hvperf tool:
# install from a local .tar.gz or an http(s) URL, e.g. the tarball attached to
# a release on the hvperf releases page
hvperf tool install -t https://github.com/<org>/harvester-perf/releases/download/<tag>/hvperf-tools-<version>.tar.gz
hvperf tool install --tool-source ./dist/hvperf-tools-<version>.tar.gz
# show what is installed
hvperf tool list
# remove ~/.hvperf/tools (prompts for confirmation; -y/--skip-confirm to skip)
hvperf tool uninstallThe tools tarball is published as a release asset on the hvperf releases
page. For development, make tools builds it locally into ./dist/ (inside a
container; honours ETCD_VERSION).
The archive must be a .tar.gz of regular files (the tool binaries); other
entry types are skipped. Files are extracted flat into the tools directory,
overwriting existing ones.
make tools uses the Dockerfile to build hvperf and the tools that the suites
need. The etcd tools are built from the etcd source at the version pinned in the
Dockerfile (ETCD_VERSION, currently v3.6.14). The runtime base is
registry.suse.com/bci/bci-base at BCI_TAG (16.0).
hvperf run reads ./hvperf.yaml by default. Copy and edit the file at the
repo root to tune suite parameters before running:
density:
batchWaitTimeout: 5m # per-batch wait timeout
vmi:
containerDisk: quay.io/kubevirt/cirros-container-disk-demo:latest
memory: 90Mi
cpu: 100mPass --config to point at a different file:
hvperf run density --config ~/my-cluster.yamlSuites are either exclusive or non-exclusive. Exclusive suites impose stress
on the cluster through stress loaders such as fio, which may temporarily
degrade the cluster's overall performance and stability. They are therefore not
suitable to run alongside other suites, to avoid worsening the cluster's
condition; hvperf run rejects an exclusive suite combined with any other
suite. The flag is reported by hvperf list.
| Suite | Exclusive | What it does |
|---|---|---|
node-capacity |
no | Assess node resource capacity: per-node OS info and disk info via a privileged hostPID daemonset. |
etcd-benchmark |
yes | Exercises the cluster's etcd from a privileged hostNetwork job pod. |
control-plane-footprint |
no | Measures the cluster's resource footprint: per-namespace CPU/memory usage and requests, host memory, and node allocatable, via Prometheus queries. |
density |
yes | Import a VM image, ramp up VMs concurrently until failure, and record VMI boot-latency percentiles (p50/p95/p99). |
storage-kbench |
yes | Run Longhorn's kbench job against a fresh PVC to benchmark bandwidth, IOPS and latency, then read the matching Longhorn metrics from Prometheus. |
A SuiteResult holds one CaseResult per check the suite ran. Each
CaseResult records what it did in whichever of these it produced —
a case can populate more than one:
| Type | Use it for |
|---|---|
CmdResult |
The result of executing a command in a test case — e.g. exec-ing into a helper pod. Stdout/Stderr hold the command's output streams. |
MetricResult |
The result of a Prometheus query run in a test case — e.g. reading etcd metrics after a benchmark. Samples holds the returned vector, Warnings any promclient warnings. |
PodLogResult |
The logs of a pod in a test case — e.g. the kbench job pod's output. Subject names the pod, Logs holds its output. |
K8sResourceResult |
The result of querying Kubernetes/node resources in a test case — e.g. node-capacity's per-node OS and disk info. Data is a flat map of the resource's spec/status attributes. |
Every result type carries its own Err, at a different scope:
| Field | Scope |
|---|---|
SuiteResult.Err |
The suite itself failed outside any single case — e.g. setup or cleanup shared by the whole suite. |
CaseResult.Err |
The case failed outside a specific command/query — e.g. a namespace or daemonset never became ready. Setting it marks the case errored immediately, before its CmdResults/MetricResults/K8sResourceResults are even considered. |
CmdResult.Err |
One exec'd command failed. |
MetricResult.Err |
One Prometheus query failed. |
PodLogResult.Err |
Fetching one pod's logs failed. |
K8sResourceResult.Err |
One resource query failed (this is the one error-typed field; the others are string). |
CaseResult.FinalizeState() rolls all of the above up into the case's
State: any non-empty Err, or any error in CmdResults, MetricResults,
PodLogResults, or K8sResourceResults, marks the case CaseResultStateErrored.
NewCaseResult and the With* builders call it for you.
The Makefile runs the Go toolchain inside the SUSE BCI golang image, so Docker is the only hard prerequisite.
make go/build # compile ./dist/hvperf
make go/test # go test -cover -race -shuffle=on ./...Useful overrides:
| Variable | Default | Purpose |
|---|---|---|
IMAGE_PLATFORMS |
linux/amd64 |
Target platform for go/build and tools |
HARVESTER_VERSION |
1.9 |
Version prefix; CLI_VERSION becomes <version>+<short-sha> |
BCI_GO_TAG |
1.26 |
Tag of registry.suse.com/bci/golang used to build and test |
ETCD_VERSION |
v3.6.14 |
etcd version the tools target builds etcdctl and benchmark from |
DOCKER |
docker |
Container runtime (e.g. DOCKER=podman) |
IMAGE_PLATFORMS=linux/arm64 make go/buildTo build natively instead, Go 1.26+ works directly: go build -o hvperf .
.
├── main.go # entry point; blank-imports internal/suites to register built-ins
├── cmd/ # cobra command tree (root, list, run, version) and k8s client setup
├── pkg/suites/ # public API: Suite interface, registry, options, results, marshalling
├── pkg/k8s/ # cluster helpers the suites share: namespaces, jobs, exec, logs, pod monitors
├── internal/suites/
│ ├── density/ # density suite
│ ├── etcd/ # etcd-benchmark suite
│ ├── nodes/ # node-capacity suite
│ ├── resourcefootprint/ # control-plane-footprint suite
│ ├── storage/ # storage-kbench suite
│ └── options/ # decodes the generic pkg/suites.Options into a suite's own options struct
├── METRICS.md # the metrics the suites collect and how to read them
├── Dockerfile # multi-stage: etcd tools + hvperf -> BCI base runtime
└── Makefile # containerised build, test and tools targets
-
Create a package under
internal/suites/<name>/. -
Implement
pkg/suites.Suite:type MySuite struct { pkgsuites.SuiteMarshaler *pkgsuites.Clients } func (s *MySuite) Name() string { return "my-suite" } func (s *MySuite) Description() string { return "what it measures" } func (s *MySuite) IsExclusive() bool { return false } func (s *MySuite) RunE(ctx context.Context, runID, namespace string, opts pkgsuites.Options) pkgsuites.SuiteResult func (s *MySuite) SetClients(c *pkgsuites.Clients)
Embedding
SuiteMarshalerand settings.Marshal = sat construction gives the suite itslisttable row and its JSON/YAML form for free.runIDis generated once perhvperf runinvocation and shared by every suite in it — use it to name and label anything the suite creates, and echo it back in theSuiteResult.namespaceis where those resources belong. Record each check as aCaseResult;Objectsis rendered as(Kind) namespace/namein the text output, and a case that could not be run for an environmental reason should setStatetoCaseResultStateSkippedinstead of erroring.NewCaseResult/NewCaseResultSkippedbuild aCaseResultand finalize itsStatefor you. -
Give the suite an options struct. Settings shared across suites — the default namespace, job pod image and timeouts, the monitoring addon coordinates — live in
pkgsuites.DefaultGlobalOptions(); decode them into your own struct withinternal/suites/options.FromOptions[*MyOptions]and layer the suite-specific defaults on top. Passing that struct throughpkgsuites.ToSuiteParamsintoSuiteResult.Paramsrecords the effective configuration in the results. -
Register it from the package's
init():func init() { pkgsuites.Register(NewMySuite()) }
-
Blank-import the package from
internal/suites/register.gosomainpulls it in.
Names are the registry key — two suites with the same Name() silently
overwrite each other. Suites should honour ctx cancellation and clean up
anything they create in the cluster.
Apache 2.0 — see LICENSE.