123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584 |
- /*
- Copyright 2017 The Kubernetes Authors.
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- */
- package preemption
- import (
- "fmt"
- "testing"
- "k8s.io/api/core/v1"
- "k8s.io/apimachinery/pkg/api/resource"
- metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
- utilfeature "k8s.io/apiserver/pkg/util/feature"
- "k8s.io/client-go/tools/record"
- featuregatetesting "k8s.io/component-base/featuregate/testing"
- kubeapi "k8s.io/kubernetes/pkg/apis/core"
- "k8s.io/kubernetes/pkg/apis/scheduling"
- "k8s.io/kubernetes/pkg/features"
- kubetypes "k8s.io/kubernetes/pkg/kubelet/types"
- )
- const (
- critical = "critical"
- clusterCritical = "cluster-critical"
- nodeCritical = "node-critical"
- bestEffort = "bestEffort"
- burstable = "burstable"
- highRequestBurstable = "high-request-burstable"
- guaranteed = "guaranteed"
- highRequestGuaranteed = "high-request-guaranteed"
- tinyBurstable = "tiny"
- maxPods = 110
- )
- type fakePodKiller struct {
- killedPods []*v1.Pod
- errDuringPodKilling bool
- }
- func newFakePodKiller(errPodKilling bool) *fakePodKiller {
- return &fakePodKiller{killedPods: []*v1.Pod{}, errDuringPodKilling: errPodKilling}
- }
- func (f *fakePodKiller) clear() {
- f.killedPods = []*v1.Pod{}
- }
- func (f *fakePodKiller) getKilledPods() []*v1.Pod {
- return f.killedPods
- }
- func (f *fakePodKiller) killPodNow(pod *v1.Pod, status v1.PodStatus, gracePeriodOverride *int64) error {
- if f.errDuringPodKilling {
- f.killedPods = []*v1.Pod{}
- return fmt.Errorf("problem killing pod %v", pod)
- }
- f.killedPods = append(f.killedPods, pod)
- return nil
- }
- type fakePodProvider struct {
- pods []*v1.Pod
- }
- func newFakePodProvider() *fakePodProvider {
- return &fakePodProvider{pods: []*v1.Pod{}}
- }
- func (f *fakePodProvider) setPods(pods []*v1.Pod) {
- f.pods = pods
- }
- func (f *fakePodProvider) getPods() []*v1.Pod {
- return f.pods
- }
- func getTestCriticalPodAdmissionHandler(podProvider *fakePodProvider, podKiller *fakePodKiller) *CriticalPodAdmissionHandler {
- return &CriticalPodAdmissionHandler{
- getPodsFunc: podProvider.getPods,
- killPodFunc: podKiller.killPodNow,
- recorder: &record.FakeRecorder{},
- }
- }
- func TestEvictPodsToFreeRequestsWithError(t *testing.T) {
- defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ExperimentalCriticalPodAnnotation, true)()
- type testRun struct {
- testName string
- inputPods []*v1.Pod
- insufficientResources admissionRequirementList
- expectErr bool
- expectedOutput []*v1.Pod
- }
- podProvider := newFakePodProvider()
- podKiller := newFakePodKiller(true)
- criticalPodAdmissionHandler := getTestCriticalPodAdmissionHandler(podProvider, podKiller)
- allPods := getTestPods()
- runs := []testRun{
- {
- testName: "multiple pods eviction error",
- inputPods: []*v1.Pod{
- allPods[critical], allPods[bestEffort], allPods[burstable], allPods[highRequestBurstable],
- allPods[guaranteed], allPods[highRequestGuaranteed]},
- insufficientResources: getAdmissionRequirementList(0, 550, 0),
- expectErr: false,
- expectedOutput: nil,
- },
- }
- for _, r := range runs {
- podProvider.setPods(r.inputPods)
- outErr := criticalPodAdmissionHandler.evictPodsToFreeRequests(allPods[critical], r.insufficientResources)
- outputPods := podKiller.getKilledPods()
- if !r.expectErr && outErr != nil {
- t.Errorf("evictPodsToFreeRequests returned an unexpected error during the %s test. Err: %v", r.testName, outErr)
- } else if r.expectErr && outErr == nil {
- t.Errorf("evictPodsToFreeRequests expected an error but returned a successful output=%v during the %s test.", outputPods, r.testName)
- } else if !podListEqual(r.expectedOutput, outputPods) {
- t.Errorf("evictPodsToFreeRequests expected %v but got %v during the %s test.", r.expectedOutput, outputPods, r.testName)
- }
- podKiller.clear()
- }
- }
- func TestEvictPodsToFreeRequests(t *testing.T) {
- defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ExperimentalCriticalPodAnnotation, true)()
- type testRun struct {
- testName string
- inputPods []*v1.Pod
- insufficientResources admissionRequirementList
- expectErr bool
- expectedOutput []*v1.Pod
- }
- podProvider := newFakePodProvider()
- podKiller := newFakePodKiller(false)
- criticalPodAdmissionHandler := getTestCriticalPodAdmissionHandler(podProvider, podKiller)
- allPods := getTestPods()
- runs := []testRun{
- {
- testName: "critical pods cannot be preempted",
- inputPods: []*v1.Pod{allPods[critical]},
- insufficientResources: getAdmissionRequirementList(0, 0, 1),
- expectErr: true,
- expectedOutput: nil,
- },
- {
- testName: "best effort pods are not preempted when attempting to free resources",
- inputPods: []*v1.Pod{allPods[bestEffort]},
- insufficientResources: getAdmissionRequirementList(0, 1, 0),
- expectErr: true,
- expectedOutput: nil,
- },
- {
- testName: "multiple pods evicted",
- inputPods: []*v1.Pod{
- allPods[critical], allPods[bestEffort], allPods[burstable], allPods[highRequestBurstable],
- allPods[guaranteed], allPods[highRequestGuaranteed]},
- insufficientResources: getAdmissionRequirementList(0, 550, 0),
- expectErr: false,
- expectedOutput: []*v1.Pod{allPods[highRequestBurstable], allPods[highRequestGuaranteed]},
- },
- }
- for _, r := range runs {
- podProvider.setPods(r.inputPods)
- outErr := criticalPodAdmissionHandler.evictPodsToFreeRequests(allPods[critical], r.insufficientResources)
- outputPods := podKiller.getKilledPods()
- if !r.expectErr && outErr != nil {
- t.Errorf("evictPodsToFreeRequests returned an unexpected error during the %s test. Err: %v", r.testName, outErr)
- } else if r.expectErr && outErr == nil {
- t.Errorf("evictPodsToFreeRequests expected an error but returned a successful output=%v during the %s test.", outputPods, r.testName)
- } else if !podListEqual(r.expectedOutput, outputPods) {
- t.Errorf("evictPodsToFreeRequests expected %v but got %v during the %s test.", r.expectedOutput, outputPods, r.testName)
- }
- podKiller.clear()
- }
- }
- func BenchmarkGetPodsToPreempt(t *testing.B) {
- allPods := getTestPods()
- inputPods := []*v1.Pod{}
- for i := 0; i < maxPods; i++ {
- inputPods = append(inputPods, allPods[tinyBurstable])
- }
- for n := 0; n < t.N; n++ {
- getPodsToPreempt(nil, inputPods, admissionRequirementList([]*admissionRequirement{
- {
- resourceName: v1.ResourceCPU,
- quantity: parseCPUToInt64("110m"),
- }}))
- }
- }
- func TestGetPodsToPreempt(t *testing.T) {
- defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ExperimentalCriticalPodAnnotation, true)()
- type testRun struct {
- testName string
- preemptor *v1.Pod
- inputPods []*v1.Pod
- insufficientResources admissionRequirementList
- expectErr bool
- expectedOutput []*v1.Pod
- }
- allPods := getTestPods()
- runs := []testRun{
- {
- testName: "no requirements",
- preemptor: allPods[critical],
- inputPods: []*v1.Pod{},
- insufficientResources: getAdmissionRequirementList(0, 0, 0),
- expectErr: false,
- expectedOutput: []*v1.Pod{},
- },
- {
- testName: "no pods",
- preemptor: allPods[critical],
- inputPods: []*v1.Pod{},
- insufficientResources: getAdmissionRequirementList(0, 0, 1),
- expectErr: true,
- expectedOutput: nil,
- },
- {
- testName: "equal pods and resources requirements",
- preemptor: allPods[critical],
- inputPods: []*v1.Pod{allPods[burstable]},
- insufficientResources: getAdmissionRequirementList(100, 100, 1),
- expectErr: false,
- expectedOutput: []*v1.Pod{allPods[burstable]},
- },
- {
- testName: "higher requirements than pod requests",
- preemptor: allPods[critical],
- inputPods: []*v1.Pod{allPods[burstable]},
- insufficientResources: getAdmissionRequirementList(200, 200, 2),
- expectErr: true,
- expectedOutput: nil,
- },
- {
- testName: "choose between bestEffort and burstable",
- preemptor: allPods[critical],
- inputPods: []*v1.Pod{allPods[burstable], allPods[bestEffort]},
- insufficientResources: getAdmissionRequirementList(0, 0, 1),
- expectErr: false,
- expectedOutput: []*v1.Pod{allPods[bestEffort]},
- },
- {
- testName: "choose between burstable and guaranteed",
- preemptor: allPods[critical],
- inputPods: []*v1.Pod{allPods[burstable], allPods[guaranteed]},
- insufficientResources: getAdmissionRequirementList(0, 0, 1),
- expectErr: false,
- expectedOutput: []*v1.Pod{allPods[burstable]},
- },
- {
- testName: "choose lower request burstable if it meets requirements",
- preemptor: allPods[critical],
- inputPods: []*v1.Pod{allPods[bestEffort], allPods[highRequestBurstable], allPods[burstable]},
- insufficientResources: getAdmissionRequirementList(100, 100, 0),
- expectErr: false,
- expectedOutput: []*v1.Pod{allPods[burstable]},
- },
- {
- testName: "choose higher request burstable if lower does not meet requirements",
- preemptor: allPods[critical],
- inputPods: []*v1.Pod{allPods[bestEffort], allPods[burstable], allPods[highRequestBurstable]},
- insufficientResources: getAdmissionRequirementList(150, 150, 0),
- expectErr: false,
- expectedOutput: []*v1.Pod{allPods[highRequestBurstable]},
- },
- {
- testName: "multiple pods required",
- preemptor: allPods[critical],
- inputPods: []*v1.Pod{allPods[bestEffort], allPods[burstable], allPods[highRequestBurstable], allPods[guaranteed], allPods[highRequestGuaranteed]},
- insufficientResources: getAdmissionRequirementList(350, 350, 0),
- expectErr: false,
- expectedOutput: []*v1.Pod{allPods[burstable], allPods[highRequestBurstable]},
- },
- {
- testName: "evict guaranteed when we have to, and dont evict the extra burstable",
- preemptor: allPods[critical],
- inputPods: []*v1.Pod{allPods[bestEffort], allPods[burstable], allPods[highRequestBurstable], allPods[guaranteed], allPods[highRequestGuaranteed]},
- insufficientResources: getAdmissionRequirementList(0, 550, 0),
- expectErr: false,
- expectedOutput: []*v1.Pod{allPods[highRequestBurstable], allPods[highRequestGuaranteed]},
- },
- {
- testName: "evict cluster critical pod for node critical pod",
- preemptor: allPods[nodeCritical],
- inputPods: []*v1.Pod{allPods[clusterCritical]},
- insufficientResources: getAdmissionRequirementList(100, 0, 0),
- expectErr: false,
- expectedOutput: []*v1.Pod{allPods[clusterCritical]},
- },
- {
- testName: "can not evict node critical pod for cluster critical pod",
- preemptor: allPods[clusterCritical],
- inputPods: []*v1.Pod{allPods[nodeCritical]},
- insufficientResources: getAdmissionRequirementList(100, 0, 0),
- expectErr: true,
- expectedOutput: nil,
- },
- }
- for _, r := range runs {
- outputPods, outErr := getPodsToPreempt(r.preemptor, r.inputPods, r.insufficientResources)
- if !r.expectErr && outErr != nil {
- t.Errorf("getPodsToPreempt returned an unexpected error during the %s test. Err: %v", r.testName, outErr)
- } else if r.expectErr && outErr == nil {
- t.Errorf("getPodsToPreempt expected an error but returned a successful output=%v during the %s test.", outputPods, r.testName)
- } else if !podListEqual(r.expectedOutput, outputPods) {
- t.Errorf("getPodsToPreempt expected %v but got %v during the %s test.", r.expectedOutput, outputPods, r.testName)
- }
- }
- }
- func TestAdmissionRequirementsDistance(t *testing.T) {
- type testRun struct {
- testName string
- requirements admissionRequirementList
- inputPod *v1.Pod
- expectedOutput float64
- }
- allPods := getTestPods()
- runs := []testRun{
- {
- testName: "no requirements",
- requirements: getAdmissionRequirementList(0, 0, 0),
- inputPod: allPods[burstable],
- expectedOutput: 0,
- },
- {
- testName: "no requests, some requirements",
- requirements: getAdmissionRequirementList(100, 100, 1),
- inputPod: allPods[bestEffort],
- expectedOutput: 2,
- },
- {
- testName: "equal requests and requirements",
- requirements: getAdmissionRequirementList(100, 100, 1),
- inputPod: allPods[burstable],
- expectedOutput: 0,
- },
- {
- testName: "higher requests than requirements",
- requirements: getAdmissionRequirementList(50, 50, 0),
- inputPod: allPods[burstable],
- expectedOutput: 0,
- },
- }
- for _, run := range runs {
- output := run.requirements.distance(run.inputPod)
- if output != run.expectedOutput {
- t.Errorf("expected: %f, got: %f for %s test", run.expectedOutput, output, run.testName)
- }
- }
- }
- func TestAdmissionRequirementsSubtract(t *testing.T) {
- type testRun struct {
- testName string
- initial admissionRequirementList
- inputPod *v1.Pod
- expectedOutput admissionRequirementList
- }
- allPods := getTestPods()
- runs := []testRun{
- {
- testName: "subtract a pod from no requirements",
- initial: getAdmissionRequirementList(0, 0, 0),
- inputPod: allPods[burstable],
- expectedOutput: getAdmissionRequirementList(0, 0, 0),
- },
- {
- testName: "subtract no requests from some requirements",
- initial: getAdmissionRequirementList(100, 100, 1),
- inputPod: allPods[bestEffort],
- expectedOutput: getAdmissionRequirementList(100, 100, 0),
- },
- {
- testName: "equal requests and requirements",
- initial: getAdmissionRequirementList(100, 100, 1),
- inputPod: allPods[burstable],
- expectedOutput: getAdmissionRequirementList(0, 0, 0),
- },
- {
- testName: "subtract higher requests than requirements",
- initial: getAdmissionRequirementList(50, 50, 0),
- inputPod: allPods[burstable],
- expectedOutput: getAdmissionRequirementList(0, 0, 0),
- },
- {
- testName: "subtract lower requests than requirements",
- initial: getAdmissionRequirementList(200, 200, 1),
- inputPod: allPods[burstable],
- expectedOutput: getAdmissionRequirementList(100, 100, 0),
- },
- }
- for _, run := range runs {
- output := run.initial.subtract(run.inputPod)
- if !admissionRequirementListEqual(output, run.expectedOutput) {
- t.Errorf("expected: %s, got: %s for %s test", run.expectedOutput.toString(), output.toString(), run.testName)
- }
- }
- }
- func getTestPods() map[string]*v1.Pod {
- allPods := map[string]*v1.Pod{
- tinyBurstable: getPodWithResources(tinyBurstable, v1.ResourceRequirements{
- Requests: v1.ResourceList{
- v1.ResourceCPU: resource.MustParse("1m"),
- v1.ResourceMemory: resource.MustParse("1Mi"),
- },
- }),
- bestEffort: getPodWithResources(bestEffort, v1.ResourceRequirements{}),
- critical: getPodWithResources(critical, v1.ResourceRequirements{
- Requests: v1.ResourceList{
- v1.ResourceCPU: resource.MustParse("100m"),
- v1.ResourceMemory: resource.MustParse("100Mi"),
- },
- }),
- clusterCritical: getPodWithResources(clusterCritical, v1.ResourceRequirements{
- Requests: v1.ResourceList{
- v1.ResourceCPU: resource.MustParse("100m"),
- v1.ResourceMemory: resource.MustParse("100Mi"),
- },
- }),
- nodeCritical: getPodWithResources(nodeCritical, v1.ResourceRequirements{
- Requests: v1.ResourceList{
- v1.ResourceCPU: resource.MustParse("100m"),
- v1.ResourceMemory: resource.MustParse("100Mi"),
- },
- }),
- burstable: getPodWithResources(burstable, v1.ResourceRequirements{
- Requests: v1.ResourceList{
- v1.ResourceCPU: resource.MustParse("100m"),
- v1.ResourceMemory: resource.MustParse("100Mi"),
- },
- }),
- guaranteed: getPodWithResources(guaranteed, v1.ResourceRequirements{
- Requests: v1.ResourceList{
- v1.ResourceCPU: resource.MustParse("100m"),
- v1.ResourceMemory: resource.MustParse("100Mi"),
- },
- Limits: v1.ResourceList{
- v1.ResourceCPU: resource.MustParse("100m"),
- v1.ResourceMemory: resource.MustParse("100Mi"),
- },
- }),
- highRequestBurstable: getPodWithResources(highRequestBurstable, v1.ResourceRequirements{
- Requests: v1.ResourceList{
- v1.ResourceCPU: resource.MustParse("300m"),
- v1.ResourceMemory: resource.MustParse("300Mi"),
- },
- }),
- highRequestGuaranteed: getPodWithResources(highRequestGuaranteed, v1.ResourceRequirements{
- Requests: v1.ResourceList{
- v1.ResourceCPU: resource.MustParse("300m"),
- v1.ResourceMemory: resource.MustParse("300Mi"),
- },
- Limits: v1.ResourceList{
- v1.ResourceCPU: resource.MustParse("300m"),
- v1.ResourceMemory: resource.MustParse("300Mi"),
- },
- }),
- }
- allPods[critical].Namespace = kubeapi.NamespaceSystem
- allPods[critical].Annotations[kubetypes.CriticalPodAnnotationKey] = ""
- allPods[clusterCritical].Namespace = kubeapi.NamespaceSystem
- allPods[clusterCritical].Spec.PriorityClassName = scheduling.SystemClusterCritical
- clusterPriority := scheduling.SystemCriticalPriority
- allPods[clusterCritical].Spec.Priority = &clusterPriority
- allPods[nodeCritical].Namespace = kubeapi.NamespaceSystem
- allPods[nodeCritical].Spec.PriorityClassName = scheduling.SystemNodeCritical
- nodePriority := scheduling.SystemCriticalPriority + 100
- allPods[nodeCritical].Spec.Priority = &nodePriority
- return allPods
- }
- func getPodWithResources(name string, requests v1.ResourceRequirements) *v1.Pod {
- return &v1.Pod{
- ObjectMeta: metav1.ObjectMeta{
- GenerateName: name,
- Annotations: map[string]string{},
- },
- Spec: v1.PodSpec{
- Containers: []v1.Container{
- {
- Name: fmt.Sprintf("%s-container", name),
- Resources: requests,
- },
- },
- },
- }
- }
- func parseCPUToInt64(res string) int64 {
- r := resource.MustParse(res)
- return (&r).MilliValue()
- }
- func parseNonCpuResourceToInt64(res string) int64 {
- r := resource.MustParse(res)
- return (&r).Value()
- }
- func getAdmissionRequirementList(cpu, memory, pods int) admissionRequirementList {
- reqs := []*admissionRequirement{}
- if cpu > 0 {
- reqs = append(reqs, &admissionRequirement{
- resourceName: v1.ResourceCPU,
- quantity: parseCPUToInt64(fmt.Sprintf("%dm", cpu)),
- })
- }
- if memory > 0 {
- reqs = append(reqs, &admissionRequirement{
- resourceName: v1.ResourceMemory,
- quantity: parseNonCpuResourceToInt64(fmt.Sprintf("%dMi", memory)),
- })
- }
- if pods > 0 {
- reqs = append(reqs, &admissionRequirement{
- resourceName: v1.ResourcePods,
- quantity: int64(pods),
- })
- }
- return admissionRequirementList(reqs)
- }
- // this checks if the lists contents contain all of the same elements.
- // this is not correct if there are duplicate pods in the list.
- // for example: podListEqual([a, a, b], [a, b, b]) will return true
- func admissionRequirementListEqual(list1 admissionRequirementList, list2 admissionRequirementList) bool {
- if len(list1) != len(list2) {
- return false
- }
- for _, a := range list1 {
- contains := false
- for _, b := range list2 {
- if a.resourceName == b.resourceName && a.quantity == b.quantity {
- contains = true
- }
- }
- if !contains {
- return false
- }
- }
- return true
- }
- // podListEqual checks if the lists contents contain all of the same elements.
- func podListEqual(list1 []*v1.Pod, list2 []*v1.Pod) bool {
- if len(list1) != len(list2) {
- return false
- }
- m := map[*v1.Pod]int{}
- for _, val := range list1 {
- m[val] = m[val] + 1
- }
- for _, val := range list2 {
- m[val] = m[val] - 1
- }
- for _, v := range m {
- if v != 0 {
- return false
- }
- }
- return true
- }
|