diff --git a/hack/load-testing/README.md b/hack/load-testing/README.md index aaf4da2692..151f111c77 100644 --- a/hack/load-testing/README.md +++ b/hack/load-testing/README.md @@ -2,21 +2,8 @@ How to use: -1. Create a new vcluster +1. Create a new vcluster via the deployment options 2. Start testing via: ``` -go run hack/load-testing/main.go -amount 1000 secrets +go run hack/load-testing/main.go throughput ``` - -### Connect to SQLite - -Install SQLite: -``` -kubectl exec -it -n NAMESPACE NAME-0 -c syncer -- sh -c 'apk update && apk add sqlite' -``` - -Access Database: -``` -kubectl exec -it -n NAMESPACE NAME-0 -c syncer -- sh -c 'sqlite3 /data/server/db/state.db' -``` - diff --git a/hack/load-testing/deploy/k3s-mysql/mysql.yaml b/hack/load-testing/deploy/k3s-mysql/mysql.yaml new file mode 100644 index 0000000000..ce4bad95ea --- /dev/null +++ b/hack/load-testing/deploy/k3s-mysql/mysql.yaml @@ -0,0 +1,54 @@ +apiVersion: v1 +kind: Service +metadata: + name: mysql +spec: + ports: + - port: 3306 + selector: + app: mysql +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mysql +spec: + selector: + matchLabels: + app: mysql + strategy: + type: Recreate + template: + metadata: + labels: + app: mysql + spec: + containers: + - image: mysql:8 + name: mysql + args: + - "--default-authentication-plugin=mysql_native_password" + env: + # Use secret in real usage + - name: MYSQL_ROOT_PASSWORD + value: my-password + - name: MYSQL_DATABASE + value: k3s + volumeMounts: + - name: mysql-persistent-storage + mountPath: /var/lib/mysql + volumes: + - name: mysql-persistent-storage + persistentVolumeClaim: + claimName: mysql-pv-claim +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: mysql-pv-claim +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi \ No newline at end of file diff --git a/hack/load-testing/deploy/k3s-mysql/values.yaml b/hack/load-testing/deploy/k3s-mysql/values.yaml new file mode 100644 index 0000000000..36caa7285b --- /dev/null +++ b/hack/load-testing/deploy/k3s-mysql/values.yaml @@ -0,0 +1,20 @@ +syncer: + resources: + limits: + cpu: "0" + memory: "0" + requests: + cpu: "0" + memory: "0" + +vcluster: + env: + - name: K3S_DATASTORE_ENDPOINT + value: mysql://root:my-password@tcp(mysql:3306)/k3s + resources: + limits: + cpu: "0" + memory: "0" + requests: + cpu: "0" + memory: "0" \ No newline at end of file diff --git a/hack/load-testing/deploy/k3s/values.yaml b/hack/load-testing/deploy/k3s/values.yaml new file mode 100644 index 0000000000..7937af4b5f --- /dev/null +++ b/hack/load-testing/deploy/k3s/values.yaml @@ -0,0 +1,17 @@ +syncer: + resources: + limits: + cpu: "0" + memory: "0" + requests: + cpu: "0" + memory: "0" + +vcluster: + resources: + limits: + cpu: "0" + memory: "0" + requests: + cpu: "0" + memory: "0" \ No newline at end of file diff --git a/hack/load-testing/deploy/k8s/values.yaml b/hack/load-testing/deploy/k8s/values.yaml new file mode 100644 index 0000000000..b675fd4932 --- /dev/null +++ b/hack/load-testing/deploy/k8s/values.yaml @@ -0,0 +1,8 @@ +syncer: + resources: + limits: + cpu: "0" + memory: "0" + requests: + cpu: "0" + memory: "0" diff --git a/hack/load-testing/main.go b/hack/load-testing/main.go index e92d5124d6..e839887393 100644 --- a/hack/load-testing/main.go +++ b/hack/load-testing/main.go @@ -6,15 +6,14 @@ import ( "fmt" "os" - "github.com/loft-sh/vcluster/hack/load-testing/tests/events" - "github.com/loft-sh/vcluster/hack/load-testing/tests/secrets" + "github.com/loft-sh/vcluster/hack/load-testing/tests/throughput" "k8s.io/klog/v2" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" ) func printUsage() { - _, _ = fmt.Fprintln(os.Stderr, "usage: load-testing [-amount INT] [-namespace STRING]") + _, _ = fmt.Fprintln(os.Stderr, "usage: load-testing [-namespace STRING]") os.Exit(1) } @@ -25,8 +24,6 @@ func printError(err error) { func main() { ctx := context.Background() - amount := flag.Int64("amount", 100, "amount to create") - namespace := "" flag.StringVar(&namespace, "namespace", "load-testing", "namespace to use") flag.Parse() @@ -39,8 +36,8 @@ func main() { // We increase the limits here so that we don't get any problems restConfig := ctrl.GetConfigOrDie() - restConfig.QPS = 1000 - restConfig.Burst = 2000 + restConfig.QPS = 9999999 + restConfig.Burst = 9999999 restConfig.Timeout = 0 // build client @@ -51,13 +48,8 @@ func main() { } switch test { - case "secrets": - err = secrets.TestSecrets(ctx, kubeClient, *amount, namespace) - if err != nil { - printError(err) - } - case "events": - err = events.TestEvents(ctx, kubeClient, restConfig, *amount, namespace) + case "throughput": + err = throughput.TestThroughput(ctx, kubeClient, namespace) if err != nil { printError(err) } diff --git a/hack/load-testing/stopwatch/stopwatch.go b/hack/load-testing/stopwatch/stopwatch.go new file mode 100644 index 0000000000..3002e6bb0b --- /dev/null +++ b/hack/load-testing/stopwatch/stopwatch.go @@ -0,0 +1,30 @@ +package stopwatch + +import ( + "time" + + "github.com/go-logr/logr" +) + +func New(logger logr.Logger) *StopWatch { + return &StopWatch{ + logger: logger, + lastTime: time.Now(), + } +} + +type StopWatch struct { + logger logr.Logger + + lastTime time.Time +} + +func (s *StopWatch) Reset() { + s.lastTime = time.Now() +} + +func (s *StopWatch) Stop(msg string, keysAndValues ...interface{}) { + elapsed := time.Since(s.lastTime) + s.logger.Info(msg, append([]interface{}{"elapsed", elapsed}, keysAndValues...)...) + s.lastTime = time.Now() +} diff --git a/hack/load-testing/tests/events/events.go b/hack/load-testing/tests/events/events.go deleted file mode 100644 index 9238812145..0000000000 --- a/hack/load-testing/tests/events/events.go +++ /dev/null @@ -1,51 +0,0 @@ -package events - -import ( - "context" - "fmt" - "time" - - "github.com/loft-sh/vcluster/hack/load-testing/tests/framework" - "github.com/loft-sh/vcluster/pkg/util/random" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes" - clientcorev1 "k8s.io/client-go/kubernetes/typed/core/v1" - "k8s.io/client-go/rest" - "k8s.io/client-go/tools/record" - "k8s.io/klog/v2" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -func TestEvents(ctx context.Context, kubeClient client.Client, restConfig *rest.Config, amount int64, namespace string) error { - // create the event recorder - recorderClient, err := kubernetes.NewForConfig(restConfig) - if err != nil { - return fmt.Errorf("create kubernetes client: %w", err) - } - eventBroadcaster := record.NewBroadcaster() - eventBroadcaster.StartStructuredLogging(0) - eventBroadcaster.StartRecordingToSink(&clientcorev1.EventSinkImpl{Interface: recorderClient.CoreV1().Events("")}) - recorder := eventBroadcaster.NewRecorder(kubeClient.Scheme(), corev1.EventSource{Component: "loading-test"}) - err = framework.CreateNamespace(ctx, kubeClient, namespace) - if err != nil { - return err - } - - for i := int64(0); i < amount; i++ { - if i%int64(100) == 0 { - klog.FromContext(ctx).Info("Creating event", "n", i) - time.Sleep(time.Millisecond * 500) - } - - recorder.Event(&corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Name: fmt.Sprintf("pod-%v", i), - Namespace: namespace, - }, - }, "Warning", "Test", random.String(1024)) - } - - time.Sleep(time.Millisecond * 100) - return nil -} diff --git a/hack/load-testing/tests/secrets/secrets.go b/hack/load-testing/tests/secrets/secrets.go deleted file mode 100644 index e77a2f16f4..0000000000 --- a/hack/load-testing/tests/secrets/secrets.go +++ /dev/null @@ -1,46 +0,0 @@ -package secrets - -import ( - "context" - "fmt" - - "github.com/loft-sh/vcluster/hack/load-testing/tests/framework" - "github.com/loft-sh/vcluster/pkg/util/random" - corev1 "k8s.io/api/core/v1" - kerrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/klog/v2" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -func TestSecrets(ctx context.Context, kubeClient client.Client, amount int64, namespace string) error { - err := framework.CreateNamespace(ctx, kubeClient, namespace) - if err != nil { - return err - } - - for i := int64(0); i < amount; i++ { - if i%int64(100) == 0 { - klog.FromContext(ctx).Info("Creating secret", "n", i) - } - - err = kubeClient.Create(ctx, &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - GenerateName: fmt.Sprintf("secret-test-" + random.String(10) + "-"), - Namespace: namespace, - }, - Data: map[string][]byte{ - random.String(32): []byte(random.String(1024)), - }, - }) - if err != nil { - if kerrors.IsAlreadyExists(err) { - continue - } - - return fmt.Errorf("error creating secret: %w", err) - } - } - - return nil -} diff --git a/hack/load-testing/tests/throughput/throughput.go b/hack/load-testing/tests/throughput/throughput.go new file mode 100644 index 0000000000..71f05fb038 --- /dev/null +++ b/hack/load-testing/tests/throughput/throughput.go @@ -0,0 +1,112 @@ +package throughput + +import ( + "context" + "fmt" + "time" + + "github.com/loft-sh/vcluster/hack/load-testing/stopwatch" + "github.com/loft-sh/vcluster/hack/load-testing/tests/framework" + "github.com/loft-sh/vcluster/pkg/util/random" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/klog/v2" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func TestThroughput(ctx context.Context, kubeClient client.Client, namespace string) error { + err := framework.CreateNamespace(ctx, kubeClient, namespace) + if err != nil { + return err + } + + // create secrets + amount := 10000 + stopWatch := stopwatch.New(klog.FromContext(ctx)) + for i := 0; i < amount; i++ { + if i%2000 == 0 { + klog.FromContext(ctx).Info("Creating secret", "n", i) + } + + err = kubeClient.Create(ctx, &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: fmt.Sprintf("secret-test-" + random.String(10) + "-"), + Namespace: namespace, + }, + Data: map[string][]byte{ + random.String(32): []byte(random.String(1024)), + }, + }) + if err != nil { + return fmt.Errorf("error creating secret: %w", err) + } + } + stopWatch.Stop("Create secrets", "amount", amount) + + // list secrets + secretList := &corev1.SecretList{} + err = kubeClient.List(ctx, secretList, client.InNamespace(namespace)) + if err != nil { + return fmt.Errorf("error listing secrets: %w", err) + } + stopWatch.Stop("Listing secrets", "amount", amount) + + // update secrets + for i, secret := range secretList.Items { + if i%2000 == 0 { + klog.FromContext(ctx).Info("Updating secret", "n", i) + } + + secret.Data[random.String(32)] = []byte(random.String(1024)) + err = kubeClient.Update(ctx, &secret) + if err != nil { + return fmt.Errorf("error updating secret: %w", err) + } + } + stopWatch.Stop("Updating secrets", "amount", amount) + + // relist secrets + secretList = &corev1.SecretList{} + err = kubeClient.List(ctx, secretList, client.InNamespace(namespace)) + if err != nil { + return fmt.Errorf("error re-listing secrets: %w", err) + } + stopWatch.Stop("Re-Listing secrets", "amount", amount) + + // patch secrets + for i, secret := range secretList.Items { + if i%2000 == 0 { + klog.FromContext(ctx).Info("Patching secret", "n", i) + } + + oldSecret := secret.DeepCopy() + secret.Data[random.String(32)] = []byte(random.String(1024)) + err = kubeClient.Patch(ctx, &secret, client.MergeFrom(oldSecret)) + if err != nil { + return fmt.Errorf("error patching secret: %w", err) + } + } + stopWatch.Stop("Patching secrets", "amount", amount) + + // deleting secrets + err = kubeClient.Delete(ctx, &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: namespace, + }, + }) + if err != nil { + return fmt.Errorf("error deleting namespace: %w", err) + } + for { + err = kubeClient.Get(ctx, types.NamespacedName{Name: namespace}, &corev1.Namespace{}) + if err != nil { + break + } + + time.Sleep(time.Millisecond * 100) + } + stopWatch.Stop("Deleting secrets & namespace", "amount", amount) + + return nil +}