12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550 |
- /*
- Copyright 2016 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 eviction
- import (
- "fmt"
- "testing"
- "time"
- "k8s.io/api/core/v1"
- "k8s.io/apimachinery/pkg/api/resource"
- "k8s.io/apimachinery/pkg/types"
- "k8s.io/apimachinery/pkg/util/clock"
- 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"
- statsapi "k8s.io/kubernetes/pkg/kubelet/apis/stats/v1alpha1"
- evictionapi "k8s.io/kubernetes/pkg/kubelet/eviction/api"
- "k8s.io/kubernetes/pkg/kubelet/lifecycle"
- kubelettypes "k8s.io/kubernetes/pkg/kubelet/types"
- )
- const (
- lowPriority = -1
- defaultPriority = 0
- highPriority = 1
- )
- // mockPodKiller is used to testing which pod is killed
- type mockPodKiller struct {
- pod *v1.Pod
- status v1.PodStatus
- gracePeriodOverride *int64
- }
- // killPodNow records the pod that was killed
- func (m *mockPodKiller) killPodNow(pod *v1.Pod, status v1.PodStatus, gracePeriodOverride *int64) error {
- m.pod = pod
- m.status = status
- m.gracePeriodOverride = gracePeriodOverride
- return nil
- }
- // mockDiskInfoProvider is used to simulate testing.
- type mockDiskInfoProvider struct {
- dedicatedImageFs bool
- }
- // HasDedicatedImageFs returns the mocked value
- func (m *mockDiskInfoProvider) HasDedicatedImageFs() (bool, error) {
- return m.dedicatedImageFs, nil
- }
- // mockDiskGC is used to simulate invoking image and container garbage collection.
- type mockDiskGC struct {
- err error
- imageGCInvoked bool
- containerGCInvoked bool
- fakeSummaryProvider *fakeSummaryProvider
- summaryAfterGC *statsapi.Summary
- }
- // DeleteUnusedImages returns the mocked values.
- func (m *mockDiskGC) DeleteUnusedImages() error {
- m.imageGCInvoked = true
- if m.summaryAfterGC != nil && m.fakeSummaryProvider != nil {
- m.fakeSummaryProvider.result = m.summaryAfterGC
- }
- return m.err
- }
- // DeleteAllUnusedContainers returns the mocked value
- func (m *mockDiskGC) DeleteAllUnusedContainers() error {
- m.containerGCInvoked = true
- if m.summaryAfterGC != nil && m.fakeSummaryProvider != nil {
- m.fakeSummaryProvider.result = m.summaryAfterGC
- }
- return m.err
- }
- func makePodWithMemoryStats(name string, priority int32, requests v1.ResourceList, limits v1.ResourceList, memoryWorkingSet string) (*v1.Pod, statsapi.PodStats) {
- pod := newPod(name, priority, []v1.Container{
- newContainer(name, requests, limits),
- }, nil)
- podStats := newPodMemoryStats(pod, resource.MustParse(memoryWorkingSet))
- return pod, podStats
- }
- func makePodWithDiskStats(name string, priority int32, requests v1.ResourceList, limits v1.ResourceList, rootFsUsed, logsUsed, perLocalVolumeUsed string) (*v1.Pod, statsapi.PodStats) {
- pod := newPod(name, priority, []v1.Container{
- newContainer(name, requests, limits),
- }, nil)
- podStats := newPodDiskStats(pod, parseQuantity(rootFsUsed), parseQuantity(logsUsed), parseQuantity(perLocalVolumeUsed))
- return pod, podStats
- }
- func makeMemoryStats(nodeAvailableBytes string, podStats map[*v1.Pod]statsapi.PodStats) *statsapi.Summary {
- val := resource.MustParse(nodeAvailableBytes)
- availableBytes := uint64(val.Value())
- WorkingSetBytes := uint64(val.Value())
- result := &statsapi.Summary{
- Node: statsapi.NodeStats{
- Memory: &statsapi.MemoryStats{
- AvailableBytes: &availableBytes,
- WorkingSetBytes: &WorkingSetBytes,
- },
- SystemContainers: []statsapi.ContainerStats{
- {
- Name: statsapi.SystemContainerPods,
- Memory: &statsapi.MemoryStats{
- AvailableBytes: &availableBytes,
- WorkingSetBytes: &WorkingSetBytes,
- },
- },
- },
- },
- Pods: []statsapi.PodStats{},
- }
- for _, podStat := range podStats {
- result.Pods = append(result.Pods, podStat)
- }
- return result
- }
- func makeDiskStats(rootFsAvailableBytes, imageFsAvailableBytes string, podStats map[*v1.Pod]statsapi.PodStats) *statsapi.Summary {
- rootFsVal := resource.MustParse(rootFsAvailableBytes)
- rootFsBytes := uint64(rootFsVal.Value())
- rootFsCapacityBytes := uint64(rootFsVal.Value() * 2)
- imageFsVal := resource.MustParse(imageFsAvailableBytes)
- imageFsBytes := uint64(imageFsVal.Value())
- imageFsCapacityBytes := uint64(imageFsVal.Value() * 2)
- result := &statsapi.Summary{
- Node: statsapi.NodeStats{
- Fs: &statsapi.FsStats{
- AvailableBytes: &rootFsBytes,
- CapacityBytes: &rootFsCapacityBytes,
- },
- Runtime: &statsapi.RuntimeStats{
- ImageFs: &statsapi.FsStats{
- AvailableBytes: &imageFsBytes,
- CapacityBytes: &imageFsCapacityBytes,
- },
- },
- },
- Pods: []statsapi.PodStats{},
- }
- for _, podStat := range podStats {
- result.Pods = append(result.Pods, podStat)
- }
- return result
- }
- type podToMake struct {
- name string
- priority int32
- requests v1.ResourceList
- limits v1.ResourceList
- memoryWorkingSet string
- rootFsUsed string
- logsFsUsed string
- logsFsInodesUsed string
- rootFsInodesUsed string
- perLocalVolumeUsed string
- perLocalVolumeInodesUsed string
- }
- // TestMemoryPressure
- func TestMemoryPressure(t *testing.T) {
- podMaker := makePodWithMemoryStats
- summaryStatsMaker := makeMemoryStats
- podsToMake := []podToMake{
- {name: "guaranteed-low-priority-high-usage", priority: lowPriority, requests: newResourceList("100m", "1Gi", ""), limits: newResourceList("100m", "1Gi", ""), memoryWorkingSet: "900Mi"},
- {name: "burstable-below-requests", priority: defaultPriority, requests: newResourceList("100m", "100Mi", ""), limits: newResourceList("200m", "1Gi", ""), memoryWorkingSet: "50Mi"},
- {name: "burstable-above-requests", priority: defaultPriority, requests: newResourceList("100m", "100Mi", ""), limits: newResourceList("200m", "1Gi", ""), memoryWorkingSet: "400Mi"},
- {name: "best-effort-high-priority-high-usage", priority: highPriority, requests: newResourceList("", "", ""), limits: newResourceList("", "", ""), memoryWorkingSet: "400Mi"},
- {name: "best-effort-low-priority-low-usage", priority: lowPriority, requests: newResourceList("", "", ""), limits: newResourceList("", "", ""), memoryWorkingSet: "100Mi"},
- }
- pods := []*v1.Pod{}
- podStats := map[*v1.Pod]statsapi.PodStats{}
- for _, podToMake := range podsToMake {
- pod, podStat := podMaker(podToMake.name, podToMake.priority, podToMake.requests, podToMake.limits, podToMake.memoryWorkingSet)
- pods = append(pods, pod)
- podStats[pod] = podStat
- }
- podToEvict := pods[4]
- activePodsFunc := func() []*v1.Pod {
- return pods
- }
- fakeClock := clock.NewFakeClock(time.Now())
- podKiller := &mockPodKiller{}
- diskInfoProvider := &mockDiskInfoProvider{dedicatedImageFs: false}
- diskGC := &mockDiskGC{err: nil}
- nodeRef := &v1.ObjectReference{Kind: "Node", Name: "test", UID: types.UID("test"), Namespace: ""}
- config := Config{
- MaxPodGracePeriodSeconds: 5,
- PressureTransitionPeriod: time.Minute * 5,
- Thresholds: []evictionapi.Threshold{
- {
- Signal: evictionapi.SignalMemoryAvailable,
- Operator: evictionapi.OpLessThan,
- Value: evictionapi.ThresholdValue{
- Quantity: quantityMustParse("1Gi"),
- },
- },
- {
- Signal: evictionapi.SignalMemoryAvailable,
- Operator: evictionapi.OpLessThan,
- Value: evictionapi.ThresholdValue{
- Quantity: quantityMustParse("2Gi"),
- },
- GracePeriod: time.Minute * 2,
- },
- },
- }
- summaryProvider := &fakeSummaryProvider{result: summaryStatsMaker("2Gi", podStats)}
- manager := &managerImpl{
- clock: fakeClock,
- killPodFunc: podKiller.killPodNow,
- imageGC: diskGC,
- containerGC: diskGC,
- config: config,
- recorder: &record.FakeRecorder{},
- summaryProvider: summaryProvider,
- nodeRef: nodeRef,
- nodeConditionsLastObservedAt: nodeConditionsObservedAt{},
- thresholdsFirstObservedAt: thresholdsObservedAt{},
- }
- // create a best effort pod to test admission
- bestEffortPodToAdmit, _ := podMaker("best-admit", defaultPriority, newResourceList("", "", ""), newResourceList("", "", ""), "0Gi")
- burstablePodToAdmit, _ := podMaker("burst-admit", defaultPriority, newResourceList("100m", "100Mi", ""), newResourceList("200m", "200Mi", ""), "0Gi")
- // synchronize
- manager.synchronize(diskInfoProvider, activePodsFunc)
- // we should not have memory pressure
- if manager.IsUnderMemoryPressure() {
- t.Errorf("Manager should not report memory pressure")
- }
- // try to admit our pods (they should succeed)
- expected := []bool{true, true}
- for i, pod := range []*v1.Pod{bestEffortPodToAdmit, burstablePodToAdmit} {
- if result := manager.Admit(&lifecycle.PodAdmitAttributes{Pod: pod}); expected[i] != result.Admit {
- t.Errorf("Admit pod: %v, expected: %v, actual: %v", pod, expected[i], result.Admit)
- }
- }
- // induce soft threshold
- fakeClock.Step(1 * time.Minute)
- summaryProvider.result = summaryStatsMaker("1500Mi", podStats)
- manager.synchronize(diskInfoProvider, activePodsFunc)
- // we should have memory pressure
- if !manager.IsUnderMemoryPressure() {
- t.Errorf("Manager should report memory pressure since soft threshold was met")
- }
- // verify no pod was yet killed because there has not yet been enough time passed.
- if podKiller.pod != nil {
- t.Errorf("Manager should not have killed a pod yet, but killed: %v", podKiller.pod.Name)
- }
- // step forward in time pass the grace period
- fakeClock.Step(3 * time.Minute)
- summaryProvider.result = summaryStatsMaker("1500Mi", podStats)
- manager.synchronize(diskInfoProvider, activePodsFunc)
- // we should have memory pressure
- if !manager.IsUnderMemoryPressure() {
- t.Errorf("Manager should report memory pressure since soft threshold was met")
- }
- // verify the right pod was killed with the right grace period.
- if podKiller.pod != podToEvict {
- t.Errorf("Manager chose to kill pod: %v, but should have chosen %v", podKiller.pod.Name, podToEvict.Name)
- }
- if podKiller.gracePeriodOverride == nil {
- t.Errorf("Manager chose to kill pod but should have had a grace period override.")
- }
- observedGracePeriod := *podKiller.gracePeriodOverride
- if observedGracePeriod != manager.config.MaxPodGracePeriodSeconds {
- t.Errorf("Manager chose to kill pod with incorrect grace period. Expected: %d, actual: %d", manager.config.MaxPodGracePeriodSeconds, observedGracePeriod)
- }
- // reset state
- podKiller.pod = nil
- podKiller.gracePeriodOverride = nil
- // remove memory pressure
- fakeClock.Step(20 * time.Minute)
- summaryProvider.result = summaryStatsMaker("3Gi", podStats)
- manager.synchronize(diskInfoProvider, activePodsFunc)
- // we should not have memory pressure
- if manager.IsUnderMemoryPressure() {
- t.Errorf("Manager should not report memory pressure")
- }
- // induce memory pressure!
- fakeClock.Step(1 * time.Minute)
- summaryProvider.result = summaryStatsMaker("500Mi", podStats)
- manager.synchronize(diskInfoProvider, activePodsFunc)
- // we should have memory pressure
- if !manager.IsUnderMemoryPressure() {
- t.Errorf("Manager should report memory pressure")
- }
- // check the right pod was killed
- if podKiller.pod != podToEvict {
- t.Errorf("Manager chose to kill pod: %v, but should have chosen %v", podKiller.pod.Name, podToEvict.Name)
- }
- observedGracePeriod = *podKiller.gracePeriodOverride
- if observedGracePeriod != int64(0) {
- t.Errorf("Manager chose to kill pod with incorrect grace period. Expected: %d, actual: %d", 0, observedGracePeriod)
- }
- // the best-effort pod should not admit, burstable should
- expected = []bool{false, true}
- for i, pod := range []*v1.Pod{bestEffortPodToAdmit, burstablePodToAdmit} {
- if result := manager.Admit(&lifecycle.PodAdmitAttributes{Pod: pod}); expected[i] != result.Admit {
- t.Errorf("Admit pod: %v, expected: %v, actual: %v", pod, expected[i], result.Admit)
- }
- }
- // reduce memory pressure
- fakeClock.Step(1 * time.Minute)
- summaryProvider.result = summaryStatsMaker("2Gi", podStats)
- podKiller.pod = nil // reset state
- manager.synchronize(diskInfoProvider, activePodsFunc)
- // we should have memory pressure (because transition period not yet met)
- if !manager.IsUnderMemoryPressure() {
- t.Errorf("Manager should report memory pressure")
- }
- // no pod should have been killed
- if podKiller.pod != nil {
- t.Errorf("Manager chose to kill pod: %v when no pod should have been killed", podKiller.pod.Name)
- }
- // the best-effort pod should not admit, burstable should
- expected = []bool{false, true}
- for i, pod := range []*v1.Pod{bestEffortPodToAdmit, burstablePodToAdmit} {
- if result := manager.Admit(&lifecycle.PodAdmitAttributes{Pod: pod}); expected[i] != result.Admit {
- t.Errorf("Admit pod: %v, expected: %v, actual: %v", pod, expected[i], result.Admit)
- }
- }
- // move the clock past transition period to ensure that we stop reporting pressure
- fakeClock.Step(5 * time.Minute)
- summaryProvider.result = summaryStatsMaker("2Gi", podStats)
- podKiller.pod = nil // reset state
- manager.synchronize(diskInfoProvider, activePodsFunc)
- // we should not have memory pressure (because transition period met)
- if manager.IsUnderMemoryPressure() {
- t.Errorf("Manager should not report memory pressure")
- }
- // no pod should have been killed
- if podKiller.pod != nil {
- t.Errorf("Manager chose to kill pod: %v when no pod should have been killed", podKiller.pod.Name)
- }
- // all pods should admit now
- expected = []bool{true, true}
- for i, pod := range []*v1.Pod{bestEffortPodToAdmit, burstablePodToAdmit} {
- if result := manager.Admit(&lifecycle.PodAdmitAttributes{Pod: pod}); expected[i] != result.Admit {
- t.Errorf("Admit pod: %v, expected: %v, actual: %v", pod, expected[i], result.Admit)
- }
- }
- }
- func makeContainersByQOS(class v1.PodQOSClass) []v1.Container {
- resource := newResourceList("100m", "1Gi", "")
- switch class {
- case v1.PodQOSGuaranteed:
- return []v1.Container{newContainer("guaranteed-container", resource, resource)}
- case v1.PodQOSBurstable:
- return []v1.Container{newContainer("burtable-container", resource, nil)}
- case v1.PodQOSBestEffort:
- fallthrough
- default:
- return []v1.Container{newContainer("best-effort-container", nil, nil)}
- }
- }
- func TestAdmitUnderNodeConditions(t *testing.T) {
- manager := &managerImpl{}
- pods := []*v1.Pod{
- newPod("guaranteed-pod", scheduling.DefaultPriorityWhenNoDefaultClassExists, makeContainersByQOS(v1.PodQOSGuaranteed), nil),
- newPod("burstable-pod", scheduling.DefaultPriorityWhenNoDefaultClassExists, makeContainersByQOS(v1.PodQOSBurstable), nil),
- newPod("best-effort-pod", scheduling.DefaultPriorityWhenNoDefaultClassExists, makeContainersByQOS(v1.PodQOSBestEffort), nil),
- }
- expected := []bool{true, true, true}
- for i, pod := range pods {
- if result := manager.Admit(&lifecycle.PodAdmitAttributes{Pod: pod}); expected[i] != result.Admit {
- t.Errorf("Admit pod: %v, expected: %v, actual: %v", pod, expected[i], result.Admit)
- }
- }
- manager.nodeConditions = []v1.NodeConditionType{v1.NodeMemoryPressure}
- expected = []bool{true, true, false}
- for i, pod := range pods {
- if result := manager.Admit(&lifecycle.PodAdmitAttributes{Pod: pod}); expected[i] != result.Admit {
- t.Errorf("Admit pod: %v, expected: %v, actual: %v", pod, expected[i], result.Admit)
- }
- }
- manager.nodeConditions = []v1.NodeConditionType{v1.NodeMemoryPressure, v1.NodeDiskPressure}
- expected = []bool{false, false, false}
- for i, pod := range pods {
- if result := manager.Admit(&lifecycle.PodAdmitAttributes{Pod: pod}); expected[i] != result.Admit {
- t.Errorf("Admit pod: %v, expected: %v, actual: %v", pod, expected[i], result.Admit)
- }
- }
- }
- // parseQuantity parses the specified value (if provided) otherwise returns 0 value
- func parseQuantity(value string) resource.Quantity {
- if len(value) == 0 {
- return resource.MustParse("0")
- }
- return resource.MustParse(value)
- }
- func TestDiskPressureNodeFs(t *testing.T) {
- defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.LocalStorageCapacityIsolation, true)()
- podMaker := makePodWithDiskStats
- summaryStatsMaker := makeDiskStats
- podsToMake := []podToMake{
- {name: "low-priority-high-usage", priority: lowPriority, requests: newResourceList("100m", "1Gi", ""), limits: newResourceList("100m", "1Gi", ""), rootFsUsed: "900Mi"},
- {name: "below-requests", priority: defaultPriority, requests: newResourceList("100m", "100Mi", ""), limits: newResourceList("200m", "1Gi", ""), logsFsUsed: "50Mi"},
- {name: "above-requests", priority: defaultPriority, requests: newResourceList("100m", "100Mi", ""), limits: newResourceList("200m", "1Gi", ""), rootFsUsed: "400Mi"},
- {name: "high-priority-high-usage", priority: highPriority, requests: newResourceList("", "", ""), limits: newResourceList("", "", ""), perLocalVolumeUsed: "400Mi"},
- {name: "low-priority-low-usage", priority: lowPriority, requests: newResourceList("", "", ""), limits: newResourceList("", "", ""), rootFsUsed: "100Mi"},
- }
- pods := []*v1.Pod{}
- podStats := map[*v1.Pod]statsapi.PodStats{}
- for _, podToMake := range podsToMake {
- pod, podStat := podMaker(podToMake.name, podToMake.priority, podToMake.requests, podToMake.limits, podToMake.rootFsUsed, podToMake.logsFsUsed, podToMake.perLocalVolumeUsed)
- pods = append(pods, pod)
- podStats[pod] = podStat
- }
- podToEvict := pods[0]
- activePodsFunc := func() []*v1.Pod {
- return pods
- }
- fakeClock := clock.NewFakeClock(time.Now())
- podKiller := &mockPodKiller{}
- diskInfoProvider := &mockDiskInfoProvider{dedicatedImageFs: false}
- diskGC := &mockDiskGC{err: nil}
- nodeRef := &v1.ObjectReference{Kind: "Node", Name: "test", UID: types.UID("test"), Namespace: ""}
- config := Config{
- MaxPodGracePeriodSeconds: 5,
- PressureTransitionPeriod: time.Minute * 5,
- Thresholds: []evictionapi.Threshold{
- {
- Signal: evictionapi.SignalNodeFsAvailable,
- Operator: evictionapi.OpLessThan,
- Value: evictionapi.ThresholdValue{
- Quantity: quantityMustParse("1Gi"),
- },
- },
- {
- Signal: evictionapi.SignalNodeFsAvailable,
- Operator: evictionapi.OpLessThan,
- Value: evictionapi.ThresholdValue{
- Quantity: quantityMustParse("2Gi"),
- },
- GracePeriod: time.Minute * 2,
- },
- },
- }
- summaryProvider := &fakeSummaryProvider{result: summaryStatsMaker("16Gi", "200Gi", podStats)}
- manager := &managerImpl{
- clock: fakeClock,
- killPodFunc: podKiller.killPodNow,
- imageGC: diskGC,
- containerGC: diskGC,
- config: config,
- recorder: &record.FakeRecorder{},
- summaryProvider: summaryProvider,
- nodeRef: nodeRef,
- nodeConditionsLastObservedAt: nodeConditionsObservedAt{},
- thresholdsFirstObservedAt: thresholdsObservedAt{},
- }
- // create a best effort pod to test admission
- podToAdmit, _ := podMaker("pod-to-admit", defaultPriority, newResourceList("", "", ""), newResourceList("", "", ""), "0Gi", "0Gi", "0Gi")
- // synchronize
- manager.synchronize(diskInfoProvider, activePodsFunc)
- // we should not have disk pressure
- if manager.IsUnderDiskPressure() {
- t.Errorf("Manager should not report disk pressure")
- }
- // try to admit our pod (should succeed)
- if result := manager.Admit(&lifecycle.PodAdmitAttributes{Pod: podToAdmit}); !result.Admit {
- t.Errorf("Admit pod: %v, expected: %v, actual: %v", podToAdmit, true, result.Admit)
- }
- // induce soft threshold
- fakeClock.Step(1 * time.Minute)
- summaryProvider.result = summaryStatsMaker("1.5Gi", "200Gi", podStats)
- manager.synchronize(diskInfoProvider, activePodsFunc)
- // we should have disk pressure
- if !manager.IsUnderDiskPressure() {
- t.Errorf("Manager should report disk pressure since soft threshold was met")
- }
- // verify no pod was yet killed because there has not yet been enough time passed.
- if podKiller.pod != nil {
- t.Errorf("Manager should not have killed a pod yet, but killed: %v", podKiller.pod.Name)
- }
- // step forward in time pass the grace period
- fakeClock.Step(3 * time.Minute)
- summaryProvider.result = summaryStatsMaker("1.5Gi", "200Gi", podStats)
- manager.synchronize(diskInfoProvider, activePodsFunc)
- // we should have disk pressure
- if !manager.IsUnderDiskPressure() {
- t.Errorf("Manager should report disk pressure since soft threshold was met")
- }
- // verify the right pod was killed with the right grace period.
- if podKiller.pod != podToEvict {
- t.Errorf("Manager chose to kill pod: %v, but should have chosen %v", podKiller.pod.Name, podToEvict.Name)
- }
- if podKiller.gracePeriodOverride == nil {
- t.Errorf("Manager chose to kill pod but should have had a grace period override.")
- }
- observedGracePeriod := *podKiller.gracePeriodOverride
- if observedGracePeriod != manager.config.MaxPodGracePeriodSeconds {
- t.Errorf("Manager chose to kill pod with incorrect grace period. Expected: %d, actual: %d", manager.config.MaxPodGracePeriodSeconds, observedGracePeriod)
- }
- // reset state
- podKiller.pod = nil
- podKiller.gracePeriodOverride = nil
- // remove disk pressure
- fakeClock.Step(20 * time.Minute)
- summaryProvider.result = summaryStatsMaker("16Gi", "200Gi", podStats)
- manager.synchronize(diskInfoProvider, activePodsFunc)
- // we should not have disk pressure
- if manager.IsUnderDiskPressure() {
- t.Errorf("Manager should not report disk pressure")
- }
- // induce disk pressure!
- fakeClock.Step(1 * time.Minute)
- summaryProvider.result = summaryStatsMaker("500Mi", "200Gi", podStats)
- manager.synchronize(diskInfoProvider, activePodsFunc)
- // we should have disk pressure
- if !manager.IsUnderDiskPressure() {
- t.Errorf("Manager should report disk pressure")
- }
- // check the right pod was killed
- if podKiller.pod != podToEvict {
- t.Errorf("Manager chose to kill pod: %v, but should have chosen %v", podKiller.pod.Name, podToEvict.Name)
- }
- observedGracePeriod = *podKiller.gracePeriodOverride
- if observedGracePeriod != int64(0) {
- t.Errorf("Manager chose to kill pod with incorrect grace period. Expected: %d, actual: %d", 0, observedGracePeriod)
- }
- // try to admit our pod (should fail)
- if result := manager.Admit(&lifecycle.PodAdmitAttributes{Pod: podToAdmit}); result.Admit {
- t.Errorf("Admit pod: %v, expected: %v, actual: %v", podToAdmit, false, result.Admit)
- }
- // reduce disk pressure
- fakeClock.Step(1 * time.Minute)
- summaryProvider.result = summaryStatsMaker("16Gi", "200Gi", podStats)
- podKiller.pod = nil // reset state
- manager.synchronize(diskInfoProvider, activePodsFunc)
- // we should have disk pressure (because transition period not yet met)
- if !manager.IsUnderDiskPressure() {
- t.Errorf("Manager should report disk pressure")
- }
- // no pod should have been killed
- if podKiller.pod != nil {
- t.Errorf("Manager chose to kill pod: %v when no pod should have been killed", podKiller.pod.Name)
- }
- // try to admit our pod (should fail)
- if result := manager.Admit(&lifecycle.PodAdmitAttributes{Pod: podToAdmit}); result.Admit {
- t.Errorf("Admit pod: %v, expected: %v, actual: %v", podToAdmit, false, result.Admit)
- }
- // move the clock past transition period to ensure that we stop reporting pressure
- fakeClock.Step(5 * time.Minute)
- summaryProvider.result = summaryStatsMaker("16Gi", "200Gi", podStats)
- podKiller.pod = nil // reset state
- manager.synchronize(diskInfoProvider, activePodsFunc)
- // we should not have disk pressure (because transition period met)
- if manager.IsUnderDiskPressure() {
- t.Errorf("Manager should not report disk pressure")
- }
- // no pod should have been killed
- if podKiller.pod != nil {
- t.Errorf("Manager chose to kill pod: %v when no pod should have been killed", podKiller.pod.Name)
- }
- // try to admit our pod (should succeed)
- if result := manager.Admit(&lifecycle.PodAdmitAttributes{Pod: podToAdmit}); !result.Admit {
- t.Errorf("Admit pod: %v, expected: %v, actual: %v", podToAdmit, true, result.Admit)
- }
- }
- // TestMinReclaim verifies that min-reclaim works as desired.
- func TestMinReclaim(t *testing.T) {
- podMaker := makePodWithMemoryStats
- summaryStatsMaker := makeMemoryStats
- podsToMake := []podToMake{
- {name: "guaranteed-low-priority-high-usage", priority: lowPriority, requests: newResourceList("100m", "1Gi", ""), limits: newResourceList("100m", "1Gi", ""), memoryWorkingSet: "900Mi"},
- {name: "burstable-below-requests", priority: defaultPriority, requests: newResourceList("100m", "100Mi", ""), limits: newResourceList("200m", "1Gi", ""), memoryWorkingSet: "50Mi"},
- {name: "burstable-above-requests", priority: defaultPriority, requests: newResourceList("100m", "100Mi", ""), limits: newResourceList("200m", "1Gi", ""), memoryWorkingSet: "400Mi"},
- {name: "best-effort-high-priority-high-usage", priority: highPriority, requests: newResourceList("", "", ""), limits: newResourceList("", "", ""), memoryWorkingSet: "400Mi"},
- {name: "best-effort-low-priority-low-usage", priority: lowPriority, requests: newResourceList("", "", ""), limits: newResourceList("", "", ""), memoryWorkingSet: "100Mi"},
- }
- pods := []*v1.Pod{}
- podStats := map[*v1.Pod]statsapi.PodStats{}
- for _, podToMake := range podsToMake {
- pod, podStat := podMaker(podToMake.name, podToMake.priority, podToMake.requests, podToMake.limits, podToMake.memoryWorkingSet)
- pods = append(pods, pod)
- podStats[pod] = podStat
- }
- podToEvict := pods[4]
- activePodsFunc := func() []*v1.Pod {
- return pods
- }
- fakeClock := clock.NewFakeClock(time.Now())
- podKiller := &mockPodKiller{}
- diskInfoProvider := &mockDiskInfoProvider{dedicatedImageFs: false}
- diskGC := &mockDiskGC{err: nil}
- nodeRef := &v1.ObjectReference{Kind: "Node", Name: "test", UID: types.UID("test"), Namespace: ""}
- config := Config{
- MaxPodGracePeriodSeconds: 5,
- PressureTransitionPeriod: time.Minute * 5,
- Thresholds: []evictionapi.Threshold{
- {
- Signal: evictionapi.SignalMemoryAvailable,
- Operator: evictionapi.OpLessThan,
- Value: evictionapi.ThresholdValue{
- Quantity: quantityMustParse("1Gi"),
- },
- MinReclaim: &evictionapi.ThresholdValue{
- Quantity: quantityMustParse("500Mi"),
- },
- },
- },
- }
- summaryProvider := &fakeSummaryProvider{result: summaryStatsMaker("2Gi", podStats)}
- manager := &managerImpl{
- clock: fakeClock,
- killPodFunc: podKiller.killPodNow,
- imageGC: diskGC,
- containerGC: diskGC,
- config: config,
- recorder: &record.FakeRecorder{},
- summaryProvider: summaryProvider,
- nodeRef: nodeRef,
- nodeConditionsLastObservedAt: nodeConditionsObservedAt{},
- thresholdsFirstObservedAt: thresholdsObservedAt{},
- }
- // synchronize
- manager.synchronize(diskInfoProvider, activePodsFunc)
- // we should not have memory pressure
- if manager.IsUnderMemoryPressure() {
- t.Errorf("Manager should not report memory pressure")
- }
- // induce memory pressure!
- fakeClock.Step(1 * time.Minute)
- summaryProvider.result = summaryStatsMaker("500Mi", podStats)
- manager.synchronize(diskInfoProvider, activePodsFunc)
- // we should have memory pressure
- if !manager.IsUnderMemoryPressure() {
- t.Errorf("Manager should report memory pressure")
- }
- // check the right pod was killed
- if podKiller.pod != podToEvict {
- t.Errorf("Manager chose to kill pod: %v, but should have chosen %v", podKiller.pod.Name, podToEvict.Name)
- }
- observedGracePeriod := *podKiller.gracePeriodOverride
- if observedGracePeriod != int64(0) {
- t.Errorf("Manager chose to kill pod with incorrect grace period. Expected: %d, actual: %d", 0, observedGracePeriod)
- }
- // reduce memory pressure, but not below the min-reclaim amount
- fakeClock.Step(1 * time.Minute)
- summaryProvider.result = summaryStatsMaker("1.2Gi", podStats)
- podKiller.pod = nil // reset state
- manager.synchronize(diskInfoProvider, activePodsFunc)
- // we should have memory pressure (because transition period not yet met)
- if !manager.IsUnderMemoryPressure() {
- t.Errorf("Manager should report memory pressure")
- }
- // check the right pod was killed
- if podKiller.pod != podToEvict {
- t.Errorf("Manager chose to kill pod: %v, but should have chosen %v", podKiller.pod.Name, podToEvict.Name)
- }
- observedGracePeriod = *podKiller.gracePeriodOverride
- if observedGracePeriod != int64(0) {
- t.Errorf("Manager chose to kill pod with incorrect grace period. Expected: %d, actual: %d", 0, observedGracePeriod)
- }
- // reduce memory pressure and ensure the min-reclaim amount
- fakeClock.Step(1 * time.Minute)
- summaryProvider.result = summaryStatsMaker("2Gi", podStats)
- podKiller.pod = nil // reset state
- manager.synchronize(diskInfoProvider, activePodsFunc)
- // we should have memory pressure (because transition period not yet met)
- if !manager.IsUnderMemoryPressure() {
- t.Errorf("Manager should report memory pressure")
- }
- // no pod should have been killed
- if podKiller.pod != nil {
- t.Errorf("Manager chose to kill pod: %v when no pod should have been killed", podKiller.pod.Name)
- }
- // move the clock past transition period to ensure that we stop reporting pressure
- fakeClock.Step(5 * time.Minute)
- summaryProvider.result = summaryStatsMaker("2Gi", podStats)
- podKiller.pod = nil // reset state
- manager.synchronize(diskInfoProvider, activePodsFunc)
- // we should not have memory pressure (because transition period met)
- if manager.IsUnderMemoryPressure() {
- t.Errorf("Manager should not report memory pressure")
- }
- // no pod should have been killed
- if podKiller.pod != nil {
- t.Errorf("Manager chose to kill pod: %v when no pod should have been killed", podKiller.pod.Name)
- }
- }
- func TestNodeReclaimFuncs(t *testing.T) {
- defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.LocalStorageCapacityIsolation, true)()
- podMaker := makePodWithDiskStats
- summaryStatsMaker := makeDiskStats
- podsToMake := []podToMake{
- {name: "low-priority-high-usage", priority: lowPriority, requests: newResourceList("100m", "1Gi", ""), limits: newResourceList("100m", "1Gi", ""), rootFsUsed: "900Mi"},
- {name: "below-requests", priority: defaultPriority, requests: newResourceList("100m", "100Mi", ""), limits: newResourceList("200m", "1Gi", ""), logsFsUsed: "50Mi"},
- {name: "above-requests", priority: defaultPriority, requests: newResourceList("100m", "100Mi", ""), limits: newResourceList("200m", "1Gi", ""), rootFsUsed: "400Mi"},
- {name: "high-priority-high-usage", priority: highPriority, requests: newResourceList("", "", ""), limits: newResourceList("", "", ""), perLocalVolumeUsed: "400Mi"},
- {name: "low-priority-low-usage", priority: lowPriority, requests: newResourceList("", "", ""), limits: newResourceList("", "", ""), rootFsUsed: "100Mi"},
- }
- pods := []*v1.Pod{}
- podStats := map[*v1.Pod]statsapi.PodStats{}
- for _, podToMake := range podsToMake {
- pod, podStat := podMaker(podToMake.name, podToMake.priority, podToMake.requests, podToMake.limits, podToMake.rootFsUsed, podToMake.logsFsUsed, podToMake.perLocalVolumeUsed)
- pods = append(pods, pod)
- podStats[pod] = podStat
- }
- podToEvict := pods[0]
- activePodsFunc := func() []*v1.Pod {
- return pods
- }
- fakeClock := clock.NewFakeClock(time.Now())
- podKiller := &mockPodKiller{}
- diskInfoProvider := &mockDiskInfoProvider{dedicatedImageFs: false}
- nodeRef := &v1.ObjectReference{Kind: "Node", Name: "test", UID: types.UID("test"), Namespace: ""}
- config := Config{
- MaxPodGracePeriodSeconds: 5,
- PressureTransitionPeriod: time.Minute * 5,
- Thresholds: []evictionapi.Threshold{
- {
- Signal: evictionapi.SignalNodeFsAvailable,
- Operator: evictionapi.OpLessThan,
- Value: evictionapi.ThresholdValue{
- Quantity: quantityMustParse("1Gi"),
- },
- MinReclaim: &evictionapi.ThresholdValue{
- Quantity: quantityMustParse("500Mi"),
- },
- },
- },
- }
- summaryProvider := &fakeSummaryProvider{result: summaryStatsMaker("16Gi", "200Gi", podStats)}
- diskGC := &mockDiskGC{fakeSummaryProvider: summaryProvider, err: nil}
- manager := &managerImpl{
- clock: fakeClock,
- killPodFunc: podKiller.killPodNow,
- imageGC: diskGC,
- containerGC: diskGC,
- config: config,
- recorder: &record.FakeRecorder{},
- summaryProvider: summaryProvider,
- nodeRef: nodeRef,
- nodeConditionsLastObservedAt: nodeConditionsObservedAt{},
- thresholdsFirstObservedAt: thresholdsObservedAt{},
- }
- // synchronize
- manager.synchronize(diskInfoProvider, activePodsFunc)
- // we should not have disk pressure
- if manager.IsUnderDiskPressure() {
- t.Errorf("Manager should not report disk pressure")
- }
- // induce hard threshold
- fakeClock.Step(1 * time.Minute)
- summaryProvider.result = summaryStatsMaker(".9Gi", "200Gi", podStats)
- // make GC successfully return disk usage to previous levels
- diskGC.summaryAfterGC = summaryStatsMaker("16Gi", "200Gi", podStats)
- manager.synchronize(diskInfoProvider, activePodsFunc)
- // we should have disk pressure
- if !manager.IsUnderDiskPressure() {
- t.Errorf("Manager should report disk pressure since soft threshold was met")
- }
- // verify image gc was invoked
- if !diskGC.imageGCInvoked || !diskGC.containerGCInvoked {
- t.Errorf("Manager should have invoked image gc")
- }
- // verify no pod was killed because image gc was sufficient
- if podKiller.pod != nil {
- t.Errorf("Manager should not have killed a pod, but killed: %v", podKiller.pod.Name)
- }
- // reset state
- diskGC.imageGCInvoked = false
- diskGC.containerGCInvoked = false
- // remove disk pressure
- fakeClock.Step(20 * time.Minute)
- summaryProvider.result = summaryStatsMaker("16Gi", "200Gi", podStats)
- manager.synchronize(diskInfoProvider, activePodsFunc)
- // we should not have disk pressure
- if manager.IsUnderDiskPressure() {
- t.Errorf("Manager should not report disk pressure")
- }
- // induce disk pressure!
- fakeClock.Step(1 * time.Minute)
- summaryProvider.result = summaryStatsMaker("400Mi", "200Gi", podStats)
- // Dont reclaim any disk
- diskGC.summaryAfterGC = summaryStatsMaker("400Mi", "200Gi", podStats)
- manager.synchronize(diskInfoProvider, activePodsFunc)
- // we should have disk pressure
- if !manager.IsUnderDiskPressure() {
- t.Errorf("Manager should report disk pressure")
- }
- // ensure disk gc was invoked
- if !diskGC.imageGCInvoked || !diskGC.containerGCInvoked {
- t.Errorf("Manager should have invoked image gc")
- }
- // check the right pod was killed
- if podKiller.pod != podToEvict {
- t.Errorf("Manager chose to kill pod: %v, but should have chosen %v", podKiller.pod.Name, podToEvict.Name)
- }
- observedGracePeriod := *podKiller.gracePeriodOverride
- if observedGracePeriod != int64(0) {
- t.Errorf("Manager chose to kill pod with incorrect grace period. Expected: %d, actual: %d", 0, observedGracePeriod)
- }
- // reduce disk pressure
- fakeClock.Step(1 * time.Minute)
- summaryProvider.result = summaryStatsMaker("16Gi", "200Gi", podStats)
- diskGC.imageGCInvoked = false // reset state
- diskGC.containerGCInvoked = false // reset state
- podKiller.pod = nil // reset state
- manager.synchronize(diskInfoProvider, activePodsFunc)
- // we should have disk pressure (because transition period not yet met)
- if !manager.IsUnderDiskPressure() {
- t.Errorf("Manager should report disk pressure")
- }
- // no image gc should have occurred
- if diskGC.imageGCInvoked || diskGC.containerGCInvoked {
- t.Errorf("Manager chose to perform image gc when it was not needed")
- }
- // no pod should have been killed
- if podKiller.pod != nil {
- t.Errorf("Manager chose to kill pod: %v when no pod should have been killed", podKiller.pod.Name)
- }
- // move the clock past transition period to ensure that we stop reporting pressure
- fakeClock.Step(5 * time.Minute)
- summaryProvider.result = summaryStatsMaker("16Gi", "200Gi", podStats)
- diskGC.imageGCInvoked = false // reset state
- diskGC.containerGCInvoked = false // reset state
- podKiller.pod = nil // reset state
- manager.synchronize(diskInfoProvider, activePodsFunc)
- // we should not have disk pressure (because transition period met)
- if manager.IsUnderDiskPressure() {
- t.Errorf("Manager should not report disk pressure")
- }
- // no image gc should have occurred
- if diskGC.imageGCInvoked || diskGC.containerGCInvoked {
- t.Errorf("Manager chose to perform image gc when it was not needed")
- }
- // no pod should have been killed
- if podKiller.pod != nil {
- t.Errorf("Manager chose to kill pod: %v when no pod should have been killed", podKiller.pod.Name)
- }
- }
- func TestInodePressureNodeFsInodes(t *testing.T) {
- podMaker := func(name string, priority int32, requests v1.ResourceList, limits v1.ResourceList, rootInodes, logInodes, volumeInodes string) (*v1.Pod, statsapi.PodStats) {
- pod := newPod(name, priority, []v1.Container{
- newContainer(name, requests, limits),
- }, nil)
- podStats := newPodInodeStats(pod, parseQuantity(rootInodes), parseQuantity(logInodes), parseQuantity(volumeInodes))
- return pod, podStats
- }
- summaryStatsMaker := func(rootFsInodesFree, rootFsInodes string, podStats map[*v1.Pod]statsapi.PodStats) *statsapi.Summary {
- rootFsInodesFreeVal := resource.MustParse(rootFsInodesFree)
- internalRootFsInodesFree := uint64(rootFsInodesFreeVal.Value())
- rootFsInodesVal := resource.MustParse(rootFsInodes)
- internalRootFsInodes := uint64(rootFsInodesVal.Value())
- result := &statsapi.Summary{
- Node: statsapi.NodeStats{
- Fs: &statsapi.FsStats{
- InodesFree: &internalRootFsInodesFree,
- Inodes: &internalRootFsInodes,
- },
- },
- Pods: []statsapi.PodStats{},
- }
- for _, podStat := range podStats {
- result.Pods = append(result.Pods, podStat)
- }
- return result
- }
- podsToMake := []podToMake{
- {name: "low-priority-high-usage", priority: lowPriority, requests: newResourceList("100m", "1Gi", ""), limits: newResourceList("100m", "1Gi", ""), rootFsInodesUsed: "900Mi"},
- {name: "below-requests", priority: defaultPriority, requests: newResourceList("100m", "100Mi", ""), limits: newResourceList("200m", "1Gi", ""), rootFsInodesUsed: "50Mi"},
- {name: "above-requests", priority: defaultPriority, requests: newResourceList("100m", "100Mi", ""), limits: newResourceList("200m", "1Gi", ""), rootFsInodesUsed: "400Mi"},
- {name: "high-priority-high-usage", priority: highPriority, requests: newResourceList("", "", ""), limits: newResourceList("", "", ""), rootFsInodesUsed: "400Mi"},
- {name: "low-priority-low-usage", priority: lowPriority, requests: newResourceList("", "", ""), limits: newResourceList("", "", ""), rootFsInodesUsed: "100Mi"},
- }
- pods := []*v1.Pod{}
- podStats := map[*v1.Pod]statsapi.PodStats{}
- for _, podToMake := range podsToMake {
- pod, podStat := podMaker(podToMake.name, podToMake.priority, podToMake.requests, podToMake.limits, podToMake.rootFsInodesUsed, podToMake.logsFsInodesUsed, podToMake.perLocalVolumeInodesUsed)
- pods = append(pods, pod)
- podStats[pod] = podStat
- }
- podToEvict := pods[0]
- activePodsFunc := func() []*v1.Pod {
- return pods
- }
- fakeClock := clock.NewFakeClock(time.Now())
- podKiller := &mockPodKiller{}
- diskInfoProvider := &mockDiskInfoProvider{dedicatedImageFs: false}
- diskGC := &mockDiskGC{err: nil}
- nodeRef := &v1.ObjectReference{Kind: "Node", Name: "test", UID: types.UID("test"), Namespace: ""}
- config := Config{
- MaxPodGracePeriodSeconds: 5,
- PressureTransitionPeriod: time.Minute * 5,
- Thresholds: []evictionapi.Threshold{
- {
- Signal: evictionapi.SignalNodeFsInodesFree,
- Operator: evictionapi.OpLessThan,
- Value: evictionapi.ThresholdValue{
- Quantity: quantityMustParse("1Mi"),
- },
- },
- {
- Signal: evictionapi.SignalNodeFsInodesFree,
- Operator: evictionapi.OpLessThan,
- Value: evictionapi.ThresholdValue{
- Quantity: quantityMustParse("2Mi"),
- },
- GracePeriod: time.Minute * 2,
- },
- },
- }
- summaryProvider := &fakeSummaryProvider{result: summaryStatsMaker("3Mi", "4Mi", podStats)}
- manager := &managerImpl{
- clock: fakeClock,
- killPodFunc: podKiller.killPodNow,
- imageGC: diskGC,
- containerGC: diskGC,
- config: config,
- recorder: &record.FakeRecorder{},
- summaryProvider: summaryProvider,
- nodeRef: nodeRef,
- nodeConditionsLastObservedAt: nodeConditionsObservedAt{},
- thresholdsFirstObservedAt: thresholdsObservedAt{},
- }
- // create a best effort pod to test admission
- podToAdmit, _ := podMaker("pod-to-admit", defaultPriority, newResourceList("", "", ""), newResourceList("", "", ""), "0", "0", "0")
- // synchronize
- manager.synchronize(diskInfoProvider, activePodsFunc)
- // we should not have disk pressure
- if manager.IsUnderDiskPressure() {
- t.Errorf("Manager should not report inode pressure")
- }
- // try to admit our pod (should succeed)
- if result := manager.Admit(&lifecycle.PodAdmitAttributes{Pod: podToAdmit}); !result.Admit {
- t.Errorf("Admit pod: %v, expected: %v, actual: %v", podToAdmit, true, result.Admit)
- }
- // induce soft threshold
- fakeClock.Step(1 * time.Minute)
- summaryProvider.result = summaryStatsMaker("1.5Mi", "4Mi", podStats)
- manager.synchronize(diskInfoProvider, activePodsFunc)
- // we should have disk pressure
- if !manager.IsUnderDiskPressure() {
- t.Errorf("Manager should report inode pressure since soft threshold was met")
- }
- // verify no pod was yet killed because there has not yet been enough time passed.
- if podKiller.pod != nil {
- t.Errorf("Manager should not have killed a pod yet, but killed: %v", podKiller.pod.Name)
- }
- // step forward in time pass the grace period
- fakeClock.Step(3 * time.Minute)
- summaryProvider.result = summaryStatsMaker("1.5Mi", "4Mi", podStats)
- manager.synchronize(diskInfoProvider, activePodsFunc)
- // we should have disk pressure
- if !manager.IsUnderDiskPressure() {
- t.Errorf("Manager should report inode pressure since soft threshold was met")
- }
- // verify the right pod was killed with the right grace period.
- if podKiller.pod != podToEvict {
- t.Errorf("Manager chose to kill pod: %v, but should have chosen %v", podKiller.pod.Name, podToEvict.Name)
- }
- if podKiller.gracePeriodOverride == nil {
- t.Errorf("Manager chose to kill pod but should have had a grace period override.")
- }
- observedGracePeriod := *podKiller.gracePeriodOverride
- if observedGracePeriod != manager.config.MaxPodGracePeriodSeconds {
- t.Errorf("Manager chose to kill pod with incorrect grace period. Expected: %d, actual: %d", manager.config.MaxPodGracePeriodSeconds, observedGracePeriod)
- }
- // reset state
- podKiller.pod = nil
- podKiller.gracePeriodOverride = nil
- // remove inode pressure
- fakeClock.Step(20 * time.Minute)
- summaryProvider.result = summaryStatsMaker("3Mi", "4Mi", podStats)
- manager.synchronize(diskInfoProvider, activePodsFunc)
- // we should not have disk pressure
- if manager.IsUnderDiskPressure() {
- t.Errorf("Manager should not report inode pressure")
- }
- // induce inode pressure!
- fakeClock.Step(1 * time.Minute)
- summaryProvider.result = summaryStatsMaker("0.5Mi", "4Mi", podStats)
- manager.synchronize(diskInfoProvider, activePodsFunc)
- // we should have disk pressure
- if !manager.IsUnderDiskPressure() {
- t.Errorf("Manager should report inode pressure")
- }
- // check the right pod was killed
- if podKiller.pod != podToEvict {
- t.Errorf("Manager chose to kill pod: %v, but should have chosen %v", podKiller.pod.Name, podToEvict.Name)
- }
- observedGracePeriod = *podKiller.gracePeriodOverride
- if observedGracePeriod != int64(0) {
- t.Errorf("Manager chose to kill pod with incorrect grace period. Expected: %d, actual: %d", 0, observedGracePeriod)
- }
- // try to admit our pod (should fail)
- if result := manager.Admit(&lifecycle.PodAdmitAttributes{Pod: podToAdmit}); result.Admit {
- t.Errorf("Admit pod: %v, expected: %v, actual: %v", podToAdmit, false, result.Admit)
- }
- // reduce inode pressure
- fakeClock.Step(1 * time.Minute)
- summaryProvider.result = summaryStatsMaker("3Mi", "4Mi", podStats)
- podKiller.pod = nil // reset state
- manager.synchronize(diskInfoProvider, activePodsFunc)
- // we should have disk pressure (because transition period not yet met)
- if !manager.IsUnderDiskPressure() {
- t.Errorf("Manager should report inode pressure")
- }
- // no pod should have been killed
- if podKiller.pod != nil {
- t.Errorf("Manager chose to kill pod: %v when no pod should have been killed", podKiller.pod.Name)
- }
- // try to admit our pod (should fail)
- if result := manager.Admit(&lifecycle.PodAdmitAttributes{Pod: podToAdmit}); result.Admit {
- t.Errorf("Admit pod: %v, expected: %v, actual: %v", podToAdmit, false, result.Admit)
- }
- // move the clock past transition period to ensure that we stop reporting pressure
- fakeClock.Step(5 * time.Minute)
- summaryProvider.result = summaryStatsMaker("3Mi", "4Mi", podStats)
- podKiller.pod = nil // reset state
- manager.synchronize(diskInfoProvider, activePodsFunc)
- // we should not have disk pressure (because transition period met)
- if manager.IsUnderDiskPressure() {
- t.Errorf("Manager should not report inode pressure")
- }
- // no pod should have been killed
- if podKiller.pod != nil {
- t.Errorf("Manager chose to kill pod: %v when no pod should have been killed", podKiller.pod.Name)
- }
- // try to admit our pod (should succeed)
- if result := manager.Admit(&lifecycle.PodAdmitAttributes{Pod: podToAdmit}); !result.Admit {
- t.Errorf("Admit pod: %v, expected: %v, actual: %v", podToAdmit, true, result.Admit)
- }
- }
- // TestStaticCriticalPodsAreNotEvicted
- func TestStaticCriticalPodsAreNotEvicted(t *testing.T) {
- podMaker := makePodWithMemoryStats
- summaryStatsMaker := makeMemoryStats
- podsToMake := []podToMake{
- {name: "critical", priority: scheduling.SystemCriticalPriority, requests: newResourceList("100m", "1Gi", ""), limits: newResourceList("100m", "1Gi", ""), memoryWorkingSet: "800Mi"},
- }
- pods := []*v1.Pod{}
- podStats := map[*v1.Pod]statsapi.PodStats{}
- for _, podToMake := range podsToMake {
- pod, podStat := podMaker(podToMake.name, podToMake.priority, podToMake.requests, podToMake.limits, podToMake.memoryWorkingSet)
- pods = append(pods, pod)
- podStats[pod] = podStat
- }
- pods[0].Annotations = map[string]string{
- kubelettypes.ConfigSourceAnnotationKey: kubelettypes.FileSource,
- }
- // Mark the pod as critical
- podPriority := scheduling.SystemCriticalPriority
- pods[0].Spec.Priority = &podPriority
- pods[0].Namespace = kubeapi.NamespaceSystem
- podToEvict := pods[0]
- activePodsFunc := func() []*v1.Pod {
- return pods
- }
- mirrorPodFunc := func(staticPod *v1.Pod) (*v1.Pod, bool) {
- mirrorPod := staticPod.DeepCopy()
- mirrorPod.Annotations[kubelettypes.ConfigSourceAnnotationKey] = kubelettypes.ApiserverSource
- return mirrorPod, true
- }
- fakeClock := clock.NewFakeClock(time.Now())
- podKiller := &mockPodKiller{}
- diskInfoProvider := &mockDiskInfoProvider{dedicatedImageFs: false}
- diskGC := &mockDiskGC{err: nil}
- nodeRef := &v1.ObjectReference{
- Kind: "Node", Name: "test", UID: types.UID("test"), Namespace: "",
- }
- config := Config{
- MaxPodGracePeriodSeconds: 5,
- PressureTransitionPeriod: time.Minute * 5,
- Thresholds: []evictionapi.Threshold{
- {
- Signal: evictionapi.SignalMemoryAvailable,
- Operator: evictionapi.OpLessThan,
- Value: evictionapi.ThresholdValue{
- Quantity: quantityMustParse("1Gi"),
- },
- },
- {
- Signal: evictionapi.SignalMemoryAvailable,
- Operator: evictionapi.OpLessThan,
- Value: evictionapi.ThresholdValue{
- Quantity: quantityMustParse("2Gi"),
- },
- GracePeriod: time.Minute * 2,
- },
- },
- }
- summaryProvider := &fakeSummaryProvider{result: summaryStatsMaker("2Gi", podStats)}
- manager := &managerImpl{
- clock: fakeClock,
- killPodFunc: podKiller.killPodNow,
- mirrorPodFunc: mirrorPodFunc,
- imageGC: diskGC,
- containerGC: diskGC,
- config: config,
- recorder: &record.FakeRecorder{},
- summaryProvider: summaryProvider,
- nodeRef: nodeRef,
- nodeConditionsLastObservedAt: nodeConditionsObservedAt{},
- thresholdsFirstObservedAt: thresholdsObservedAt{},
- }
- fakeClock.Step(1 * time.Minute)
- summaryProvider.result = summaryStatsMaker("1500Mi", podStats)
- manager.synchronize(diskInfoProvider, activePodsFunc)
- // we should have memory pressure
- if !manager.IsUnderMemoryPressure() {
- t.Errorf("Manager should report memory pressure since soft threshold was met")
- }
- // verify no pod was yet killed because there has not yet been enough time passed.
- if podKiller.pod != nil {
- t.Errorf("Manager should not have killed a pod yet, but killed: %v", podKiller.pod.Name)
- }
- // step forward in time pass the grace period
- fakeClock.Step(3 * time.Minute)
- summaryProvider.result = summaryStatsMaker("1500Mi", podStats)
- manager.synchronize(diskInfoProvider, activePodsFunc)
- // we should have memory pressure
- if !manager.IsUnderMemoryPressure() {
- t.Errorf("Manager should report memory pressure since soft threshold was met")
- }
- // verify the right pod was killed with the right grace period.
- if podKiller.pod == podToEvict {
- t.Errorf("Manager chose to kill critical pod: %v, but should have ignored it", podKiller.pod.Name)
- }
- // reset state
- podKiller.pod = nil
- podKiller.gracePeriodOverride = nil
- // remove memory pressure
- fakeClock.Step(20 * time.Minute)
- summaryProvider.result = summaryStatsMaker("3Gi", podStats)
- manager.synchronize(diskInfoProvider, activePodsFunc)
- // we should not have memory pressure
- if manager.IsUnderMemoryPressure() {
- t.Errorf("Manager should not report memory pressure")
- }
- pods[0].Annotations = map[string]string{
- kubelettypes.ConfigSourceAnnotationKey: kubelettypes.FileSource,
- }
- pods[0].Spec.Priority = nil
- pods[0].Namespace = kubeapi.NamespaceSystem
- // induce memory pressure!
- fakeClock.Step(1 * time.Minute)
- summaryProvider.result = summaryStatsMaker("500Mi", podStats)
- manager.synchronize(diskInfoProvider, activePodsFunc)
- // we should have memory pressure
- if !manager.IsUnderMemoryPressure() {
- t.Errorf("Manager should report memory pressure")
- }
- }
- // TestAllocatableMemoryPressure
- func TestAllocatableMemoryPressure(t *testing.T) {
- podMaker := makePodWithMemoryStats
- summaryStatsMaker := makeMemoryStats
- podsToMake := []podToMake{
- {name: "guaranteed-low-priority-high-usage", priority: lowPriority, requests: newResourceList("100m", "1Gi", ""), limits: newResourceList("100m", "1Gi", ""), memoryWorkingSet: "900Mi"},
- {name: "burstable-below-requests", priority: defaultPriority, requests: newResourceList("100m", "100Mi", ""), limits: newResourceList("200m", "1Gi", ""), memoryWorkingSet: "50Mi"},
- {name: "burstable-above-requests", priority: defaultPriority, requests: newResourceList("100m", "100Mi", ""), limits: newResourceList("200m", "1Gi", ""), memoryWorkingSet: "400Mi"},
- {name: "best-effort-high-priority-high-usage", priority: highPriority, requests: newResourceList("", "", ""), limits: newResourceList("", "", ""), memoryWorkingSet: "400Mi"},
- {name: "best-effort-low-priority-low-usage", priority: lowPriority, requests: newResourceList("", "", ""), limits: newResourceList("", "", ""), memoryWorkingSet: "100Mi"},
- }
- pods := []*v1.Pod{}
- podStats := map[*v1.Pod]statsapi.PodStats{}
- for _, podToMake := range podsToMake {
- pod, podStat := podMaker(podToMake.name, podToMake.priority, podToMake.requests, podToMake.limits, podToMake.memoryWorkingSet)
- pods = append(pods, pod)
- podStats[pod] = podStat
- }
- podToEvict := pods[4]
- activePodsFunc := func() []*v1.Pod {
- return pods
- }
- fakeClock := clock.NewFakeClock(time.Now())
- podKiller := &mockPodKiller{}
- diskInfoProvider := &mockDiskInfoProvider{dedicatedImageFs: false}
- diskGC := &mockDiskGC{err: nil}
- nodeRef := &v1.ObjectReference{Kind: "Node", Name: "test", UID: types.UID("test"), Namespace: ""}
- config := Config{
- MaxPodGracePeriodSeconds: 5,
- PressureTransitionPeriod: time.Minute * 5,
- Thresholds: []evictionapi.Threshold{
- {
- Signal: evictionapi.SignalAllocatableMemoryAvailable,
- Operator: evictionapi.OpLessThan,
- Value: evictionapi.ThresholdValue{
- Quantity: quantityMustParse("1Gi"),
- },
- },
- },
- }
- summaryProvider := &fakeSummaryProvider{result: summaryStatsMaker("4Gi", podStats)}
- manager := &managerImpl{
- clock: fakeClock,
- killPodFunc: podKiller.killPodNow,
- imageGC: diskGC,
- containerGC: diskGC,
- config: config,
- recorder: &record.FakeRecorder{},
- summaryProvider: summaryProvider,
- nodeRef: nodeRef,
- nodeConditionsLastObservedAt: nodeConditionsObservedAt{},
- thresholdsFirstObservedAt: thresholdsObservedAt{},
- }
- // create a best effort pod to test admission
- bestEffortPodToAdmit, _ := podMaker("best-admit", defaultPriority, newResourceList("", "", ""), newResourceList("", "", ""), "0Gi")
- burstablePodToAdmit, _ := podMaker("burst-admit", defaultPriority, newResourceList("100m", "100Mi", ""), newResourceList("200m", "200Mi", ""), "0Gi")
- // synchronize
- manager.synchronize(diskInfoProvider, activePodsFunc)
- // we should not have memory pressure
- if manager.IsUnderMemoryPressure() {
- t.Errorf("Manager should not report memory pressure")
- }
- // try to admit our pods (they should succeed)
- expected := []bool{true, true}
- for i, pod := range []*v1.Pod{bestEffortPodToAdmit, burstablePodToAdmit} {
- if result := manager.Admit(&lifecycle.PodAdmitAttributes{Pod: pod}); expected[i] != result.Admit {
- t.Errorf("Admit pod: %v, expected: %v, actual: %v", pod, expected[i], result.Admit)
- }
- }
- // induce memory pressure!
- fakeClock.Step(1 * time.Minute)
- pod, podStat := podMaker("guaranteed-high-2", defaultPriority, newResourceList("100m", "1Gi", ""), newResourceList("100m", "1Gi", ""), "1Gi")
- podStats[pod] = podStat
- summaryProvider.result = summaryStatsMaker("500Mi", podStats)
- manager.synchronize(diskInfoProvider, activePodsFunc)
- // we should have memory pressure
- if !manager.IsUnderMemoryPressure() {
- t.Errorf("Manager should report memory pressure")
- }
- // check the right pod was killed
- if podKiller.pod != podToEvict {
- t.Errorf("Manager chose to kill pod: %v, but should have chosen %v", podKiller.pod.Name, podToEvict.Name)
- }
- observedGracePeriod := *podKiller.gracePeriodOverride
- if observedGracePeriod != int64(0) {
- t.Errorf("Manager chose to kill pod with incorrect grace period. Expected: %d, actual: %d", 0, observedGracePeriod)
- }
- // reset state
- podKiller.pod = nil
- podKiller.gracePeriodOverride = nil
- // the best-effort pod should not admit, burstable should
- expected = []bool{false, true}
- for i, pod := range []*v1.Pod{bestEffortPodToAdmit, burstablePodToAdmit} {
- if result := manager.Admit(&lifecycle.PodAdmitAttributes{Pod: pod}); expected[i] != result.Admit {
- t.Errorf("Admit pod: %v, expected: %v, actual: %v", pod, expected[i], result.Admit)
- }
- }
- // reduce memory pressure
- fakeClock.Step(1 * time.Minute)
- for pod := range podStats {
- if pod.Name == "guaranteed-high-2" {
- delete(podStats, pod)
- }
- }
- summaryProvider.result = summaryStatsMaker("2Gi", podStats)
- podKiller.pod = nil // reset state
- manager.synchronize(diskInfoProvider, activePodsFunc)
- // we should have memory pressure (because transition period not yet met)
- if !manager.IsUnderMemoryPressure() {
- t.Errorf("Manager should report memory pressure")
- }
- // no pod should have been killed
- if podKiller.pod != nil {
- t.Errorf("Manager chose to kill pod: %v when no pod should have been killed", podKiller.pod.Name)
- }
- // the best-effort pod should not admit, burstable should
- expected = []bool{false, true}
- for i, pod := range []*v1.Pod{bestEffortPodToAdmit, burstablePodToAdmit} {
- if result := manager.Admit(&lifecycle.PodAdmitAttributes{Pod: pod}); expected[i] != result.Admit {
- t.Errorf("Admit pod: %v, expected: %v, actual: %v", pod, expected[i], result.Admit)
- }
- }
- // move the clock past transition period to ensure that we stop reporting pressure
- fakeClock.Step(5 * time.Minute)
- summaryProvider.result = summaryStatsMaker("2Gi", podStats)
- podKiller.pod = nil // reset state
- manager.synchronize(diskInfoProvider, activePodsFunc)
- // we should not have memory pressure (because transition period met)
- if manager.IsUnderMemoryPressure() {
- t.Errorf("Manager should not report memory pressure")
- }
- // no pod should have been killed
- if podKiller.pod != nil {
- t.Errorf("Manager chose to kill pod: %v when no pod should have been killed", podKiller.pod.Name)
- }
- // all pods should admit now
- expected = []bool{true, true}
- for i, pod := range []*v1.Pod{bestEffortPodToAdmit, burstablePodToAdmit} {
- if result := manager.Admit(&lifecycle.PodAdmitAttributes{Pod: pod}); expected[i] != result.Admit {
- t.Errorf("Admit pod: %v, expected: %v, actual: %v", pod, expected[i], result.Admit)
- }
- }
- }
- func TestUpdateMemcgThreshold(t *testing.T) {
- activePodsFunc := func() []*v1.Pod {
- return []*v1.Pod{}
- }
- fakeClock := clock.NewFakeClock(time.Now())
- podKiller := &mockPodKiller{}
- diskInfoProvider := &mockDiskInfoProvider{dedicatedImageFs: false}
- diskGC := &mockDiskGC{err: nil}
- nodeRef := &v1.ObjectReference{Kind: "Node", Name: "test", UID: types.UID("test"), Namespace: ""}
- config := Config{
- MaxPodGracePeriodSeconds: 5,
- PressureTransitionPeriod: time.Minute * 5,
- Thresholds: []evictionapi.Threshold{
- {
- Signal: evictionapi.SignalMemoryAvailable,
- Operator: evictionapi.OpLessThan,
- Value: evictionapi.ThresholdValue{
- Quantity: quantityMustParse("1Gi"),
- },
- },
- },
- PodCgroupRoot: "kubepods",
- }
- summaryProvider := &fakeSummaryProvider{result: makeMemoryStats("2Gi", map[*v1.Pod]statsapi.PodStats{})}
- thresholdNotifier := &MockThresholdNotifier{}
- thresholdNotifier.On("UpdateThreshold", summaryProvider.result).Return(nil).Twice()
- manager := &managerImpl{
- clock: fakeClock,
- killPodFunc: podKiller.killPodNow,
- imageGC: diskGC,
- containerGC: diskGC,
- config: config,
- recorder: &record.FakeRecorder{},
- summaryProvider: summaryProvider,
- nodeRef: nodeRef,
- nodeConditionsLastObservedAt: nodeConditionsObservedAt{},
- thresholdsFirstObservedAt: thresholdsObservedAt{},
- thresholdNotifiers: []ThresholdNotifier{thresholdNotifier},
- }
- manager.synchronize(diskInfoProvider, activePodsFunc)
- // The UpdateThreshold method should have been called once, since this is the first run.
- thresholdNotifier.AssertNumberOfCalls(t, "UpdateThreshold", 1)
- manager.synchronize(diskInfoProvider, activePodsFunc)
- // The UpdateThreshold method should not have been called again, since not enough time has passed
- thresholdNotifier.AssertNumberOfCalls(t, "UpdateThreshold", 1)
- fakeClock.Step(2 * notifierRefreshInterval)
- manager.synchronize(diskInfoProvider, activePodsFunc)
- // The UpdateThreshold method should be called again since enough time has passed
- thresholdNotifier.AssertNumberOfCalls(t, "UpdateThreshold", 2)
- // new memory threshold notifier that returns an error
- thresholdNotifier = &MockThresholdNotifier{}
- thresholdNotifier.On("UpdateThreshold", summaryProvider.result).Return(fmt.Errorf("error updating threshold"))
- thresholdNotifier.On("Description").Return("mock thresholdNotifier").Once()
- manager.thresholdNotifiers = []ThresholdNotifier{thresholdNotifier}
- fakeClock.Step(2 * notifierRefreshInterval)
- manager.synchronize(diskInfoProvider, activePodsFunc)
- // The UpdateThreshold method should be called because at least notifierRefreshInterval time has passed.
- // The Description method should be called because UpdateThreshold returned an error
- thresholdNotifier.AssertNumberOfCalls(t, "UpdateThreshold", 1)
- thresholdNotifier.AssertNumberOfCalls(t, "Description", 1)
- }
|