k8s.io/kubernetes@v1.29.3/test/e2e/windows/density.go (about) 1 /* 2 Copyright 2018 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package windows 18 19 import ( 20 "context" 21 "fmt" 22 "sort" 23 "sync" 24 "time" 25 26 v1 "k8s.io/api/core/v1" 27 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 "k8s.io/apimachinery/pkg/labels" 29 "k8s.io/apimachinery/pkg/runtime" 30 "k8s.io/apimachinery/pkg/util/uuid" 31 "k8s.io/apimachinery/pkg/watch" 32 "k8s.io/client-go/tools/cache" 33 "k8s.io/kubernetes/test/e2e/feature" 34 "k8s.io/kubernetes/test/e2e/framework" 35 e2emetrics "k8s.io/kubernetes/test/e2e/framework/metrics" 36 e2epod "k8s.io/kubernetes/test/e2e/framework/pod" 37 imageutils "k8s.io/kubernetes/test/utils/image" 38 admissionapi "k8s.io/pod-security-admission/api" 39 40 "github.com/onsi/ginkgo/v2" 41 "github.com/onsi/gomega" 42 ) 43 44 var _ = sigDescribe(feature.Windows, "Density", framework.WithSerial(), framework.WithSlow(), skipUnlessWindows(func() { 45 f := framework.NewDefaultFramework("density-test-windows") 46 f.NamespacePodSecurityLevel = admissionapi.LevelPrivileged 47 48 ginkgo.Context("create a batch of pods", func() { 49 // TODO(coufon): the values are generous, set more precise limits with benchmark data 50 // and add more tests 51 dTests := []densityTest{ 52 { 53 podsNr: 10, 54 interval: 0 * time.Millisecond, 55 // percentile limit of single pod startup latency 56 podStartupLimits: e2emetrics.LatencyMetric{ 57 Perc50: 30 * time.Second, 58 Perc90: 54 * time.Second, 59 Perc99: 59 * time.Second, 60 }, 61 // upbound of startup latency of a batch of pods 62 podBatchStartupLimit: 10 * time.Minute, 63 }, 64 } 65 66 for _, testArg := range dTests { 67 itArg := testArg 68 desc := fmt.Sprintf("latency/resource should be within limit when create %d pods with %v interval", itArg.podsNr, itArg.interval) 69 ginkgo.It(desc, func(ctx context.Context) { 70 itArg.createMethod = "batch" 71 runDensityBatchTest(ctx, f, itArg) 72 }) 73 } 74 }) 75 76 })) 77 78 type densityTest struct { 79 // number of pods 80 podsNr int 81 // interval between creating pod (rate control) 82 interval time.Duration 83 // create pods in 'batch' or 'sequence' 84 createMethod string 85 // API QPS limit 86 APIQPSLimit int 87 // performance limits 88 podStartupLimits e2emetrics.LatencyMetric 89 podBatchStartupLimit time.Duration 90 } 91 92 // runDensityBatchTest runs the density batch pod creation test 93 func runDensityBatchTest(ctx context.Context, f *framework.Framework, testArg densityTest) (time.Duration, []e2emetrics.PodLatencyData) { 94 const ( 95 podType = "density_test_pod" 96 ) 97 var ( 98 mutex = &sync.Mutex{} 99 watchTimes = make(map[string]metav1.Time) 100 stopCh = make(chan struct{}) 101 ) 102 103 // create test pod data structure 104 pods := newDensityTestPods(testArg.podsNr, false, imageutils.GetPauseImageName(), podType) 105 106 // the controller watches the change of pod status 107 controller := newInformerWatchPod(ctx, f, mutex, watchTimes, podType) 108 go controller.Run(stopCh) 109 defer close(stopCh) 110 111 ginkgo.By("Creating a batch of pods") 112 // It returns a map['pod name']'creation time' containing the creation timestamps 113 createTimes := createBatchPodWithRateControl(ctx, f, pods, testArg.interval) 114 115 ginkgo.By("Waiting for all Pods to be observed by the watch...") 116 117 gomega.Eventually(ctx, func() bool { 118 return len(watchTimes) == testArg.podsNr 119 }, 10*time.Minute, 10*time.Second).Should(gomega.BeTrue()) 120 121 if len(watchTimes) < testArg.podsNr { 122 framework.Failf("Timeout reached waiting for all Pods to be observed by the watch.") 123 } 124 125 // Analyze results 126 var ( 127 firstCreate metav1.Time 128 lastRunning metav1.Time 129 init = true 130 e2eLags = make([]e2emetrics.PodLatencyData, 0) 131 ) 132 133 for name, create := range createTimes { 134 watch, ok := watchTimes[name] 135 if !ok { 136 framework.Failf("pod %s failed to be observed by the watch", name) 137 } 138 139 e2eLags = append(e2eLags, 140 e2emetrics.PodLatencyData{Name: name, Latency: watch.Time.Sub(create.Time)}) 141 142 if !init { 143 if firstCreate.Time.After(create.Time) { 144 firstCreate = create 145 } 146 if lastRunning.Time.Before(watch.Time) { 147 lastRunning = watch 148 } 149 } else { 150 init = false 151 firstCreate, lastRunning = create, watch 152 } 153 } 154 155 sort.Sort(e2emetrics.LatencySlice(e2eLags)) 156 batchLag := lastRunning.Time.Sub(firstCreate.Time) 157 158 deletePodsSync(ctx, f, pods) 159 160 return batchLag, e2eLags 161 } 162 163 // createBatchPodWithRateControl creates a batch of pods concurrently, uses one goroutine for each creation. 164 // between creations there is an interval for throughput control 165 func createBatchPodWithRateControl(ctx context.Context, f *framework.Framework, pods []*v1.Pod, interval time.Duration) map[string]metav1.Time { 166 createTimes := make(map[string]metav1.Time) 167 for _, pod := range pods { 168 createTimes[pod.ObjectMeta.Name] = metav1.Now() 169 go e2epod.NewPodClient(f).Create(ctx, pod) 170 time.Sleep(interval) 171 } 172 return createTimes 173 } 174 175 // newInformerWatchPod creates an informer to check whether all pods are running. 176 func newInformerWatchPod(ctx context.Context, f *framework.Framework, mutex *sync.Mutex, watchTimes map[string]metav1.Time, podType string) cache.Controller { 177 ns := f.Namespace.Name 178 checkPodRunning := func(p *v1.Pod) { 179 mutex.Lock() 180 defer mutex.Unlock() 181 defer ginkgo.GinkgoRecover() 182 183 if p.Status.Phase == v1.PodRunning { 184 if _, found := watchTimes[p.Name]; !found { 185 watchTimes[p.Name] = metav1.Now() 186 } 187 } 188 } 189 190 _, controller := cache.NewInformer( 191 &cache.ListWatch{ 192 ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { 193 options.LabelSelector = labels.SelectorFromSet(labels.Set{"type": podType}).String() 194 obj, err := f.ClientSet.CoreV1().Pods(ns).List(ctx, options) 195 return runtime.Object(obj), err 196 }, 197 WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { 198 options.LabelSelector = labels.SelectorFromSet(labels.Set{"type": podType}).String() 199 return f.ClientSet.CoreV1().Pods(ns).Watch(ctx, options) 200 }, 201 }, 202 &v1.Pod{}, 203 0, 204 cache.ResourceEventHandlerFuncs{ 205 AddFunc: func(obj interface{}) { 206 p, ok := obj.(*v1.Pod) 207 if !ok { 208 framework.Failf("expected Pod, got %T", obj) 209 } 210 go checkPodRunning(p) 211 }, 212 UpdateFunc: func(oldObj, newObj interface{}) { 213 p, ok := newObj.(*v1.Pod) 214 if !ok { 215 framework.Failf("expected Pod, got %T", newObj) 216 } 217 go checkPodRunning(p) 218 }, 219 }, 220 ) 221 return controller 222 } 223 224 // newDensityTestPods creates a list of pods (specification) for test. 225 func newDensityTestPods(numPods int, volume bool, imageName, podType string) []*v1.Pod { 226 var pods []*v1.Pod 227 228 for i := 0; i < numPods; i++ { 229 230 podName := "test-" + string(uuid.NewUUID()) 231 pod := v1.Pod{ 232 ObjectMeta: metav1.ObjectMeta{ 233 Name: podName, 234 Labels: map[string]string{ 235 "type": podType, 236 "name": podName, 237 }, 238 }, 239 Spec: v1.PodSpec{ 240 // Restart policy is always (default). 241 Containers: []v1.Container{ 242 { 243 Image: imageName, 244 Name: podName, 245 }, 246 }, 247 NodeSelector: map[string]string{ 248 "kubernetes.io/os": "windows", 249 }, 250 }, 251 } 252 253 if volume { 254 pod.Spec.Containers[0].VolumeMounts = []v1.VolumeMount{ 255 {MountPath: "/test-volume-mnt", Name: podName + "-volume"}, 256 } 257 pod.Spec.Volumes = []v1.Volume{ 258 {Name: podName + "-volume", VolumeSource: v1.VolumeSource{EmptyDir: &v1.EmptyDirVolumeSource{}}}, 259 } 260 } 261 262 pods = append(pods, &pod) 263 } 264 265 return pods 266 } 267 268 // deletePodsSync deletes a list of pods and block until pods disappear. 269 func deletePodsSync(ctx context.Context, f *framework.Framework, pods []*v1.Pod) { 270 var wg sync.WaitGroup 271 for _, pod := range pods { 272 wg.Add(1) 273 go func(pod *v1.Pod) { 274 defer ginkgo.GinkgoRecover() 275 defer wg.Done() 276 277 err := e2epod.NewPodClient(f).Delete(ctx, pod.ObjectMeta.Name, *metav1.NewDeleteOptions(30)) 278 framework.ExpectNoError(err) 279 280 err = e2epod.WaitForPodNotFoundInNamespace(ctx, f.ClientSet, pod.ObjectMeta.Name, f.Namespace.Name, 10*time.Minute) 281 framework.ExpectNoError(err) 282 }(pod) 283 } 284 wg.Wait() 285 }