1
16
17 package testsuites
18
19 import (
20 "context"
21 "fmt"
22 "sync"
23 "time"
24
25 "github.com/onsi/ginkgo/v2"
26
27 v1 "k8s.io/api/core/v1"
28 storagev1 "k8s.io/api/storage/v1"
29 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
30 "k8s.io/apimachinery/pkg/runtime"
31 "k8s.io/apimachinery/pkg/util/dump"
32 "k8s.io/apimachinery/pkg/util/errors"
33 "k8s.io/apimachinery/pkg/watch"
34 clientset "k8s.io/client-go/kubernetes"
35 "k8s.io/client-go/tools/cache"
36 "k8s.io/kubernetes/test/e2e/framework"
37 e2epv "k8s.io/kubernetes/test/e2e/framework/pv"
38 e2eskipper "k8s.io/kubernetes/test/e2e/framework/skipper"
39 storageframework "k8s.io/kubernetes/test/e2e/storage/framework"
40 admissionapi "k8s.io/pod-security-admission/api"
41 )
42
43 type volumePerformanceTestSuite struct {
44 tsInfo storageframework.TestSuiteInfo
45 }
46
47 var _ storageframework.TestSuite = &volumePerformanceTestSuite{}
48
49 const testTimeout = 15 * time.Minute
50
51
52
53
54 type interval struct {
55 create time.Time
56 enterDesiredState time.Time
57 elapsed time.Duration
58 }
59
60
61 type performanceStats struct {
62 mutex *sync.Mutex
63 perObjectInterval map[string]*interval
64 operationMetrics *storageframework.Metrics
65 }
66
67
68
69
70
71
72
73 var waitForProvisionCh chan []*v1.PersistentVolumeClaim
74
75
76 func InitVolumePerformanceTestSuite() storageframework.TestSuite {
77 return &volumePerformanceTestSuite{
78 tsInfo: storageframework.TestSuiteInfo{
79 Name: "volume-lifecycle-performance",
80 TestPatterns: []storageframework.TestPattern{
81 storageframework.FsVolModeDynamicPV,
82 },
83 },
84 }
85 }
86
87 func (t *volumePerformanceTestSuite) GetTestSuiteInfo() storageframework.TestSuiteInfo {
88 return t.tsInfo
89 }
90
91 func (t *volumePerformanceTestSuite) SkipUnsupportedTests(driver storageframework.TestDriver, pattern storageframework.TestPattern) {
92 }
93
94 func (t *volumePerformanceTestSuite) DefineTests(driver storageframework.TestDriver, pattern storageframework.TestPattern) {
95 type local struct {
96 config *storageframework.PerTestConfig
97 cs clientset.Interface
98 ns *v1.Namespace
99 scName string
100 pvcs []*v1.PersistentVolumeClaim
101 options *storageframework.PerformanceTestOptions
102 stopCh chan struct{}
103 }
104 var (
105 dInfo *storageframework.DriverInfo
106 l *local
107 )
108 ginkgo.BeforeEach(func() {
109
110 dDriver := driver.(storageframework.DynamicPVTestDriver)
111 if dDriver == nil {
112 e2eskipper.Skipf("Test driver does not support dynamically created volumes")
113
114 }
115 dInfo = dDriver.GetDriverInfo()
116 if dInfo == nil {
117 e2eskipper.Skipf("Failed to get Driver info -- skipping")
118 }
119 if dInfo.PerformanceTestOptions == nil || dInfo.PerformanceTestOptions.ProvisioningOptions == nil {
120 e2eskipper.Skipf("Driver %s doesn't specify performance test options -- skipping", dInfo.Name)
121 }
122 })
123
124 frameworkOptions := framework.Options{
125 ClientQPS: 200,
126 ClientBurst: 400,
127 }
128 f := framework.NewFramework("volume-lifecycle-performance", frameworkOptions, nil)
129 f.NamespacePodSecurityLevel = admissionapi.LevelPrivileged
130
131 ginkgo.AfterEach(func(ctx context.Context) {
132 if l != nil {
133 if l.stopCh != nil {
134 ginkgo.By("Closing informer channel")
135 close(l.stopCh)
136 }
137
138 ginkgo.By("Deleting all PVCs")
139 for _, pvc := range l.pvcs {
140 err := e2epv.DeletePersistentVolumeClaim(ctx, l.cs, pvc.Name, pvc.Namespace)
141 framework.ExpectNoError(err)
142 err = e2epv.WaitForPersistentVolumeDeleted(ctx, l.cs, pvc.Spec.VolumeName, 1*time.Second, 5*time.Minute)
143 framework.ExpectNoError(err)
144 }
145 ginkgo.By(fmt.Sprintf("Deleting Storage Class %s", l.scName))
146 err := l.cs.StorageV1().StorageClasses().Delete(ctx, l.scName, metav1.DeleteOptions{})
147 framework.ExpectNoError(err)
148 } else {
149 ginkgo.By("Local l setup is nil")
150 }
151 })
152
153 f.It("should provision volumes at scale within performance constraints", f.WithSlow(), f.WithSerial(), func(ctx context.Context) {
154 l = &local{
155 cs: f.ClientSet,
156 ns: f.Namespace,
157 options: dInfo.PerformanceTestOptions,
158 }
159 l.config = driver.PrepareTest(ctx, f)
160
161
162
163 provisioningStats := &performanceStats{
164 mutex: &sync.Mutex{},
165 perObjectInterval: make(map[string]*interval),
166 operationMetrics: &storageframework.Metrics{},
167 }
168 sc := driver.(storageframework.DynamicPVTestDriver).GetDynamicProvisionStorageClass(ctx, l.config, pattern.FsType)
169 ginkgo.By(fmt.Sprintf("Creating Storage Class %v", sc))
170
171 if sc.VolumeBindingMode != nil && *sc.VolumeBindingMode == storagev1.VolumeBindingWaitForFirstConsumer {
172 e2eskipper.Skipf("WaitForFirstConsumer binding mode currently not supported for performance tests")
173 }
174 ginkgo.By(fmt.Sprintf("Creating Storage Class %s", sc.Name))
175 sc, err := l.cs.StorageV1().StorageClasses().Create(ctx, sc, metav1.CreateOptions{})
176 framework.ExpectNoError(err)
177 l.scName = sc.Name
178
179
180
181
182 controller := newPVCWatch(ctx, f, l.options.ProvisioningOptions.Count, provisioningStats)
183 l.stopCh = make(chan struct{})
184 go controller.Run(l.stopCh)
185 waitForProvisionCh = make(chan []*v1.PersistentVolumeClaim)
186
187 ginkgo.By(fmt.Sprintf("Creating %d PVCs of size %s", l.options.ProvisioningOptions.Count, l.options.ProvisioningOptions.VolumeSize))
188 for i := 0; i < l.options.ProvisioningOptions.Count; i++ {
189 pvc := e2epv.MakePersistentVolumeClaim(e2epv.PersistentVolumeClaimConfig{
190 ClaimSize: l.options.ProvisioningOptions.VolumeSize,
191 StorageClassName: &sc.Name,
192 }, l.ns.Name)
193 pvc, err = l.cs.CoreV1().PersistentVolumeClaims(l.ns.Name).Create(ctx, pvc, metav1.CreateOptions{})
194 framework.ExpectNoError(err)
195
196 provisioningStats.mutex.Lock()
197 provisioningStats.perObjectInterval[pvc.Name] = &interval{
198 create: pvc.CreationTimestamp.Time,
199 }
200 provisioningStats.mutex.Unlock()
201 }
202
203 ginkgo.By("Waiting for all PVCs to be Bound...")
204
205 select {
206 case l.pvcs = <-waitForProvisionCh:
207 framework.Logf("All PVCs in Bound state")
208 case <-time.After(testTimeout):
209 ginkgo.Fail(fmt.Sprintf("expected all PVCs to be in Bound state within %v minutes", testTimeout))
210 }
211
212 ginkgo.By("Calculating performance metrics for provisioning operations")
213 createPerformanceStats(provisioningStats, l.options.ProvisioningOptions.Count, l.pvcs)
214
215 ginkgo.By(fmt.Sprintf("Validating performance metrics for provisioning operations against baseline %v", dump.Pretty(l.options.ProvisioningOptions.ExpectedMetrics)))
216 errList := validatePerformanceStats(provisioningStats.operationMetrics, l.options.ProvisioningOptions.ExpectedMetrics)
217 framework.ExpectNoError(errors.NewAggregate(errList), "while validating performance metrics")
218 })
219
220 }
221
222
223
224 func createPerformanceStats(stats *performanceStats, provisionCount int, pvcs []*v1.PersistentVolumeClaim) {
225 var min, max, sum time.Duration
226 for _, pvc := range pvcs {
227 pvcMetric, ok := stats.perObjectInterval[pvc.Name]
228 if !ok {
229 framework.Failf("PVC %s not found in perObjectInterval", pvc.Name)
230 }
231
232 elapsedTime := pvcMetric.elapsed
233 sum += elapsedTime
234 if elapsedTime < min || min == 0 {
235 min = elapsedTime
236 }
237 if elapsedTime > max {
238 max = elapsedTime
239 }
240 }
241 stats.operationMetrics = &storageframework.Metrics{
242 AvgLatency: time.Duration(int64(sum) / int64(provisionCount)),
243 Throughput: float64(provisionCount) / max.Seconds(),
244 }
245 }
246
247
248 func validatePerformanceStats(operationMetrics *storageframework.Metrics, baselineMetrics *storageframework.Metrics) []error {
249 var errList []error
250 framework.Logf("Metrics to evaluate: %+v", dump.Pretty(operationMetrics))
251
252 if operationMetrics.AvgLatency > baselineMetrics.AvgLatency {
253 err := fmt.Errorf("expected latency to be less than %v but calculated latency %v", baselineMetrics.AvgLatency, operationMetrics.AvgLatency)
254 errList = append(errList, err)
255 }
256 if operationMetrics.Throughput < baselineMetrics.Throughput {
257 err := fmt.Errorf("expected throughput to be greater than %f but calculated throughput %f", baselineMetrics.Throughput, operationMetrics.Throughput)
258 errList = append(errList, err)
259 }
260 return errList
261 }
262
263
264
265
266 func newPVCWatch(ctx context.Context, f *framework.Framework, provisionCount int, pvcMetrics *performanceStats) cache.Controller {
267 defer ginkgo.GinkgoRecover()
268 count := 0
269 countLock := &sync.Mutex{}
270 ns := f.Namespace.Name
271 var pvcs []*v1.PersistentVolumeClaim
272 checkPVCBound := func(oldPVC *v1.PersistentVolumeClaim, newPVC *v1.PersistentVolumeClaim) {
273 now := time.Now()
274 pvcMetrics.mutex.Lock()
275 defer pvcMetrics.mutex.Unlock()
276 countLock.Lock()
277 defer countLock.Unlock()
278
279
280 if oldPVC.Status.Phase != v1.ClaimBound && newPVC.Status.Phase == v1.ClaimBound {
281 newPVCInterval, ok := pvcMetrics.perObjectInterval[newPVC.Name]
282 if !ok {
283 framework.Failf("PVC %s should exist in interval map already", newPVC.Name)
284 }
285 count++
286 newPVCInterval.enterDesiredState = now
287 newPVCInterval.elapsed = now.Sub(newPVCInterval.create)
288 pvcs = append(pvcs, newPVC)
289 }
290 if count == provisionCount {
291
292
293
294 waitForProvisionCh <- pvcs
295 }
296 }
297 _, controller := cache.NewInformer(
298 &cache.ListWatch{
299 ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
300 obj, err := f.ClientSet.CoreV1().PersistentVolumeClaims(ns).List(ctx, metav1.ListOptions{})
301 return runtime.Object(obj), err
302 },
303 WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
304 return f.ClientSet.CoreV1().PersistentVolumeClaims(ns).Watch(ctx, metav1.ListOptions{})
305 },
306 },
307 &v1.PersistentVolumeClaim{},
308 0,
309 cache.ResourceEventHandlerFuncs{
310 UpdateFunc: func(oldObj, newObj interface{}) {
311 oldPVC, ok := oldObj.(*v1.PersistentVolumeClaim)
312 if !ok {
313 framework.Failf("Expected a PVC, got instead an old object of type %T", oldObj)
314 }
315 newPVC, ok := newObj.(*v1.PersistentVolumeClaim)
316 if !ok {
317 framework.Failf("Expected a PVC, got instead a new object of type %T", newObj)
318 }
319
320 checkPVCBound(oldPVC, newPVC)
321 },
322 },
323 )
324 return controller
325 }
326
View as plain text