Skip to content

Commit 382c2cf

Browse files
authored
feat: check in performance test related code/utilities/scripts (2 of N) (kubefleet-dev#543)
1 parent 6936bb8 commit 382c2cf

9 files changed

Lines changed: 865 additions & 0 deletions

File tree

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# KubeFleet Performance/Scalability Test Utility: Creating 1K Placements Concurrently
2+
3+
This directory contains a utility program that creates 1K placements that run concurrently and polls
4+
until their full completion.
5+
6+
The program is added for the purpose of testing the performance and scalability of KubeFleet.
7+
8+
## Before you begin
9+
10+
* Set up a KubeFleet deployment.
11+
* Make sure that all member clusters are labelled with `placement-group=N`, where N ranges from 0 to 9.
12+
Placements created by this utility program will be each assigned an index X, ranging from 0 to 999,
13+
and a placement of index X will select all member clusters that are labelled with `placement-group=X%10`.
14+
* The program requires the following tools (aside from the Go runtime) to be installed:
15+
* `curl` (for retrieving pprof data)
16+
17+
> If you have followed the instructions in `../../README.md` and have used the given scripts and utility programs
18+
> to run the performance/scalability test, all the steps above should have been done for you already.
19+
20+
## Running the utility program
21+
22+
Run the commands below to run the utility program:
23+
24+
```bash
25+
RUN_NAME=example go run main.go
26+
```
27+
28+
With the default setup, it might take ~1 hour before all 1K placements are fully completed. The program
29+
reports the progress in the output.
30+
31+
After the program completes, run the commands below to clean up the created 1K placements:
32+
33+
```bash
34+
CLEANUP=set go run main.go
35+
```
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
apiVersion: placement.kubernetes-fleet.io/v1beta1
2+
kind: ClusterResourcePlacement
3+
metadata:
4+
name: crp-0
5+
spec:
6+
policy:
7+
placementType: PickAll
8+
affinity:
9+
clusterAffinity:
10+
requiredDuringSchedulingIgnoredDuringExecution:
11+
clusterSelectorTerms:
12+
- labelSelector:
13+
matchLabels:
14+
placement-group: "0"
15+
resourceSelectors:
16+
- group: ""
17+
kind: Namespace
18+
version: v1
19+
name: work-0
20+
strategy:
21+
type: RollingUpdate
22+
rollingUpdate:
23+
maxUnavailable: 100%
24+
unavailablePeriodSeconds: 1
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"time"
8+
9+
"k8s.io/apimachinery/pkg/util/wait"
10+
11+
"github.com/kubefleet-dev/kubefleet/hack/perftest/1000placements/utils"
12+
)
13+
14+
const (
15+
resourceSetupWorkerCount = 15
16+
longPollingWorkerCount = 15
17+
resourceCreationCoolDownPeriod = time.Second * 1
18+
longPollingCoolDownPeriod = time.Second * 2
19+
betweenStageCoolDownPeriod = time.Second * 30
20+
21+
maxCRPToCreateCount = 1000
22+
23+
configMapDataByteCount = 1024 // 1 KB.
24+
)
25+
26+
var (
27+
// Add trigger points for dumping pprof profiles when a specific # of placement
28+
// have been created.
29+
triggerPtsForMemProfileDumping = map[int]bool{}
30+
)
31+
32+
var (
33+
retryOpsBackoff = wait.Backoff{
34+
Steps: 4,
35+
Duration: 4 * time.Second,
36+
Factor: 2.0,
37+
Jitter: 0.1,
38+
}
39+
)
40+
41+
func main() {
42+
ctx := context.Background()
43+
44+
// Read the arguments.
45+
doCleanUp := false
46+
cleanUpFlag := os.Getenv("CLEANUP")
47+
if len(cleanUpFlag) != 0 {
48+
doCleanUp = true
49+
}
50+
51+
runName := os.Getenv("RUN_NAME")
52+
if len(runName) == 0 {
53+
panic("RUN_NAME environment variable is not set")
54+
}
55+
56+
retrievePprofData := false
57+
if s := os.Getenv("RETRIEVE_PPROF_DATA"); len(s) != 0 {
58+
retrievePprofData = true
59+
}
60+
var pProfEndpoint string
61+
if retrievePprofData {
62+
if pProfEndpoint = os.Getenv("PPROF_ENDPOINT"); len(pProfEndpoint) == 0 {
63+
panic("PPROF_ENDPOINT environment variable is not set")
64+
}
65+
}
66+
67+
runner := utils.New(
68+
runName,
69+
resourceSetupWorkerCount,
70+
longPollingWorkerCount,
71+
betweenStageCoolDownPeriod,
72+
resourceCreationCoolDownPeriod,
73+
longPollingCoolDownPeriod,
74+
configMapDataByteCount,
75+
maxCRPToCreateCount,
76+
triggerPtsForMemProfileDumping,
77+
pProfEndpoint,
78+
retryOpsBackoff,
79+
)
80+
81+
if doCleanUp {
82+
runner.CleanUp(ctx)
83+
return
84+
}
85+
86+
fmt.Println("Preparing the resources...")
87+
runner.CreateResources(ctx)
88+
89+
// Cool down.
90+
fmt.Println("Cooling down...")
91+
runner.CoolDown()
92+
93+
fmt.Println("Creating the placements...")
94+
runner.CreatePlacements(ctx)
95+
96+
// Cool down.
97+
fmt.Println("Cooling down...")
98+
runner.CoolDown()
99+
100+
fmt.Println("Long polling the placements...")
101+
runner.LongPollPlacements(ctx)
102+
103+
fmt.Println("Tracking latency")
104+
runner.TrackLatency(ctx)
105+
106+
if retrievePprofData {
107+
fmt.Println("retrieving final pprof data")
108+
runner.RetrievePProfProfile(maxCRPToCreateCount + 1)
109+
}
110+
111+
// Tally the latency quantiles.
112+
fmt.Println("Tallying latency quantiles...")
113+
runner.TallyLatencyQuantiles()
114+
115+
fmt.Println("All placements have been completed, exiting.")
116+
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
package utils
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"sync"
7+
"time"
8+
9+
corev1 "k8s.io/api/core/v1"
10+
"k8s.io/apimachinery/pkg/api/errors"
11+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
12+
"k8s.io/apimachinery/pkg/types"
13+
"k8s.io/client-go/util/retry"
14+
15+
placementv1beta1 "github.com/kubefleet-dev/kubefleet/apis/placement/v1beta1"
16+
)
17+
18+
func (r *Runner) CleanUp(ctx context.Context) {
19+
wg := sync.WaitGroup{}
20+
21+
// Run the producer.
22+
wg.Add(1)
23+
go func() {
24+
defer wg.Done()
25+
26+
for i := 0; i < r.maxCRPToCreateCount; i++ {
27+
select {
28+
case r.toDeleteChan <- i:
29+
case <-ctx.Done():
30+
close(r.toDeleteChan)
31+
return
32+
}
33+
}
34+
35+
close(r.toDeleteChan)
36+
}()
37+
38+
// Run the workers.
39+
for i := 0; i < r.resourceSetupWorkerCount; i++ {
40+
wg.Add(1)
41+
go func(workerIdx int) {
42+
defer wg.Done()
43+
44+
for {
45+
// Read from the channel.
46+
var resIdx int
47+
var readOk bool
48+
select {
49+
case resIdx, readOk = <-r.toDeleteChan:
50+
if !readOk {
51+
fmt.Printf("worker %d exits\n", workerIdx)
52+
return
53+
}
54+
case <-ctx.Done():
55+
return
56+
}
57+
58+
// Delete the CRPs.
59+
crp := placementv1beta1.ClusterResourcePlacement{
60+
ObjectMeta: metav1.ObjectMeta{
61+
Name: fmt.Sprintf(placementNameFmt, resIdx),
62+
},
63+
}
64+
errAfterRetries := retry.OnError(r.retryOpsBackoff, func(err error) bool {
65+
return err != nil && !errors.IsNotFound(err)
66+
}, func() error {
67+
return r.hubClient.Delete(ctx, &crp)
68+
})
69+
if errAfterRetries != nil && !errors.IsNotFound(errAfterRetries) {
70+
fmt.Printf("worker %d: failed to delete CRP %s after retries: %v\n", workerIdx, fmt.Sprintf(placementNameFmt, resIdx), errAfterRetries)
71+
continue
72+
}
73+
74+
// Wait until the CRP is deleted.
75+
errAfterRetries = retry.OnError(r.retryOpsBackoff, func(err error) bool {
76+
return err != nil && !errors.IsNotFound(err)
77+
}, func() error {
78+
crp := placementv1beta1.ClusterResourcePlacement{}
79+
err := r.hubClient.Get(ctx, types.NamespacedName{Name: fmt.Sprintf(placementNameFmt, resIdx)}, &crp)
80+
if err == nil {
81+
return fmt.Errorf("CRP %s still exists", fmt.Sprintf(placementNameFmt, resIdx))
82+
}
83+
return err
84+
})
85+
if !errors.IsNotFound(errAfterRetries) {
86+
fmt.Printf("worker %d: failed to wait for CRP %s to be deleted after retries: %v\n", workerIdx, fmt.Sprintf(placementNameFmt, resIdx), errAfterRetries)
87+
} else {
88+
fmt.Printf("worker %d: deleted CRP %s\n", workerIdx, fmt.Sprintf(placementNameFmt, resIdx))
89+
}
90+
91+
// Delete the namespace if it exists.
92+
namespace := corev1.Namespace{
93+
ObjectMeta: metav1.ObjectMeta{
94+
Name: fmt.Sprintf(nsNameFmt, resIdx),
95+
},
96+
}
97+
errAfterRetries = retry.OnError(r.retryOpsBackoff, func(err error) bool {
98+
return err != nil && !errors.IsNotFound(err)
99+
}, func() error {
100+
return r.hubClient.Delete(ctx, &namespace)
101+
})
102+
if errAfterRetries != nil && !errors.IsNotFound(errAfterRetries) {
103+
fmt.Printf("worker %d: failed to delete namespace %s after retries: %v\n", workerIdx, fmt.Sprintf(nsNameFmt, resIdx), errAfterRetries)
104+
continue
105+
}
106+
107+
// Wait until the namespace is deleted.
108+
errAfterRetries = retry.OnError(r.retryOpsBackoff, func(err error) bool {
109+
return err != nil && !errors.IsNotFound(err)
110+
}, func() error {
111+
namespace := corev1.Namespace{}
112+
err := r.hubClient.Get(ctx, types.NamespacedName{Name: fmt.Sprintf(nsNameFmt, resIdx)}, &namespace)
113+
if err == nil {
114+
return fmt.Errorf("namespace %s still exists", fmt.Sprintf(nsNameFmt, resIdx))
115+
}
116+
return err
117+
})
118+
if errAfterRetries == nil || !errors.IsNotFound(errAfterRetries) {
119+
fmt.Printf("worker %d: failed to wait for namespace %s to be deleted after retries: %v\n", workerIdx, fmt.Sprintf(nsNameFmt, resIdx), errAfterRetries)
120+
} else {
121+
fmt.Printf("worker %d: deleted namespace %s\n", workerIdx, fmt.Sprintf(nsNameFmt, resIdx))
122+
}
123+
}
124+
}(i)
125+
}
126+
wg.Wait()
127+
128+
// Cool down.
129+
time.Sleep(r.betweenStageCoolDownPeriod)
130+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package utils
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"math"
7+
"sort"
8+
"sync"
9+
)
10+
11+
func (r *Runner) TrackLatency(ctx context.Context) {
12+
wg := sync.WaitGroup{}
13+
14+
wg.Add(1)
15+
go func() {
16+
defer wg.Done()
17+
18+
for {
19+
var attempt latencyTrackAttempt
20+
var readOK bool
21+
select {
22+
case attempt, readOK = <-r.toTrackLatencyChan:
23+
if !readOK {
24+
return
25+
}
26+
fmt.Printf("latency tracker: CRP %s has latency %v\n", fmt.Sprintf(placementNameFmt, attempt.resIdx), attempt.latency)
27+
r.placementCompletionLatencyByName[fmt.Sprintf(placementNameFmt, attempt.resIdx)] = attempt.latency
28+
case <-ctx.Done():
29+
return
30+
}
31+
}
32+
}()
33+
wg.Wait()
34+
}
35+
36+
func (r *Runner) TallyLatencyQuantiles() {
37+
latencies := make([]float64, 0, len(r.placementCompletionLatencyByName))
38+
for _, latency := range r.placementCompletionLatencyByName {
39+
latencies = append(latencies, float64(latency.Seconds()))
40+
}
41+
sort.Slice(latencies, func(i, j int) bool {
42+
return latencies[i] < latencies[j]
43+
})
44+
q25 := int(math.Floor(float64(len(latencies)) * 0.25))
45+
q50 := int(math.Floor(float64(len(latencies)) * 0.50))
46+
q75 := int(math.Floor(float64(len(latencies)) * 0.75))
47+
q90 := int(math.Floor(float64(len(latencies)) * 0.90))
48+
q99 := int(math.Floor(float64(len(latencies)) * 0.99))
49+
fmt.Printf("latencies: 25th=%v, 50th=%v, 75th=%v, 90th=%v, 99th=%v\n",
50+
latencies[q25], latencies[q50], latencies[q75], latencies[q90], latencies[q99])
51+
}

0 commit comments

Comments
 (0)