k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/pkg/kubelet/eviction/eviction_manager_test.go (about) 1 /* 2 Copyright 2016 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 eviction 18 19 import ( 20 "context" 21 "fmt" 22 "testing" 23 "time" 24 25 "github.com/google/go-cmp/cmp" 26 "github.com/google/go-cmp/cmp/cmpopts" 27 gomock "go.uber.org/mock/gomock" 28 v1 "k8s.io/api/core/v1" 29 "k8s.io/apimachinery/pkg/api/resource" 30 "k8s.io/apimachinery/pkg/types" 31 utilfeature "k8s.io/apiserver/pkg/util/feature" 32 "k8s.io/client-go/tools/record" 33 featuregatetesting "k8s.io/component-base/featuregate/testing" 34 statsapi "k8s.io/kubelet/pkg/apis/stats/v1alpha1" 35 kubeapi "k8s.io/kubernetes/pkg/apis/core" 36 "k8s.io/kubernetes/pkg/apis/scheduling" 37 "k8s.io/kubernetes/pkg/features" 38 evictionapi "k8s.io/kubernetes/pkg/kubelet/eviction/api" 39 "k8s.io/kubernetes/pkg/kubelet/lifecycle" 40 kubelettypes "k8s.io/kubernetes/pkg/kubelet/types" 41 testingclock "k8s.io/utils/clock/testing" 42 "k8s.io/utils/ptr" 43 ) 44 45 const ( 46 lowPriority = -1 47 defaultPriority = 0 48 highPriority = 1 49 ) 50 51 // mockPodKiller is used to testing which pod is killed 52 type mockPodKiller struct { 53 pod *v1.Pod 54 evict bool 55 statusFn func(*v1.PodStatus) 56 gracePeriodOverride *int64 57 } 58 59 // killPodNow records the pod that was killed 60 func (m *mockPodKiller) killPodNow(pod *v1.Pod, evict bool, gracePeriodOverride *int64, statusFn func(*v1.PodStatus)) error { 61 m.pod = pod 62 m.statusFn = statusFn 63 m.evict = evict 64 m.gracePeriodOverride = gracePeriodOverride 65 return nil 66 } 67 68 // mockDiskInfoProvider is used to simulate testing. 69 type mockDiskInfoProvider struct { 70 dedicatedImageFs *bool 71 } 72 73 // HasDedicatedImageFs returns the mocked value 74 func (m *mockDiskInfoProvider) HasDedicatedImageFs(_ context.Context) (bool, error) { 75 return ptr.Deref(m.dedicatedImageFs, false), nil 76 } 77 78 // mockDiskGC is used to simulate invoking image and container garbage collection. 79 type mockDiskGC struct { 80 err error 81 imageGCInvoked bool 82 containerGCInvoked bool 83 readAndWriteSeparate bool 84 fakeSummaryProvider *fakeSummaryProvider 85 summaryAfterGC *statsapi.Summary 86 } 87 88 // DeleteUnusedImages returns the mocked values. 89 func (m *mockDiskGC) DeleteUnusedImages(_ context.Context) error { 90 m.imageGCInvoked = true 91 if m.summaryAfterGC != nil && m.fakeSummaryProvider != nil { 92 m.fakeSummaryProvider.result = m.summaryAfterGC 93 } 94 return m.err 95 } 96 97 // DeleteAllUnusedContainers returns the mocked value 98 func (m *mockDiskGC) DeleteAllUnusedContainers(_ context.Context) error { 99 m.containerGCInvoked = true 100 if m.summaryAfterGC != nil && m.fakeSummaryProvider != nil { 101 m.fakeSummaryProvider.result = m.summaryAfterGC 102 } 103 return m.err 104 } 105 106 func (m *mockDiskGC) IsContainerFsSeparateFromImageFs(_ context.Context) bool { 107 return m.readAndWriteSeparate 108 } 109 110 func makePodWithMemoryStats(name string, priority int32, requests v1.ResourceList, limits v1.ResourceList, memoryWorkingSet string) (*v1.Pod, statsapi.PodStats) { 111 pod := newPod(name, priority, []v1.Container{ 112 newContainer(name, requests, limits), 113 }, nil) 114 podStats := newPodMemoryStats(pod, resource.MustParse(memoryWorkingSet)) 115 return pod, podStats 116 } 117 118 func makePodWithPIDStats(name string, priority int32, processCount uint64) (*v1.Pod, statsapi.PodStats) { 119 pod := newPod(name, priority, []v1.Container{ 120 newContainer(name, nil, nil), 121 }, nil) 122 podStats := newPodProcessStats(pod, processCount) 123 return pod, podStats 124 } 125 126 func makePodWithDiskStats(name string, priority int32, requests v1.ResourceList, limits v1.ResourceList, rootFsUsed, logsUsed, perLocalVolumeUsed string, volumes []v1.Volume) (*v1.Pod, statsapi.PodStats) { 127 pod := newPod(name, priority, []v1.Container{ 128 newContainer(name, requests, limits), 129 }, volumes) 130 podStats := newPodDiskStats(pod, parseQuantity(rootFsUsed), parseQuantity(logsUsed), parseQuantity(perLocalVolumeUsed)) 131 return pod, podStats 132 } 133 134 func makePodWithLocalStorageCapacityIsolationOpen(name string, priority int32, requests v1.ResourceList, limits v1.ResourceList, memoryWorkingSet string) (*v1.Pod, statsapi.PodStats) { 135 vol := newVolume("local-volume", v1.VolumeSource{ 136 EmptyDir: &v1.EmptyDirVolumeSource{ 137 SizeLimit: resource.NewQuantity(requests.Memory().Value(), resource.BinarySI), 138 }, 139 }) 140 var vols []v1.Volume 141 vols = append(vols, vol) 142 pod := newPod(name, priority, []v1.Container{ 143 newContainer(name, requests, limits), 144 }, vols) 145 146 var podStats statsapi.PodStats 147 switch name { 148 case "empty-dir": 149 podStats = newPodMemoryStats(pod, *resource.NewQuantity(requests.Memory().Value()*2, resource.BinarySI)) 150 case "container-ephemeral-storage-limit": 151 podStats = newPodMemoryStats(pod, *resource.NewQuantity(limits.StorageEphemeral().Value(), resource.BinarySI)) 152 case "pod-ephemeral-storage-limit": 153 podStats = newPodMemoryStats(pod, *resource.NewQuantity(limits.StorageEphemeral().Value()*2, resource.BinarySI)) 154 default: 155 podStats = newPodMemoryStats(pod, resource.MustParse(memoryWorkingSet)) 156 } 157 return pod, podStats 158 } 159 160 func makePIDStats(nodeAvailablePIDs string, numberOfRunningProcesses string, podStats map[*v1.Pod]statsapi.PodStats) *statsapi.Summary { 161 val := resource.MustParse(nodeAvailablePIDs) 162 availablePIDs := int64(val.Value()) 163 164 parsed := resource.MustParse(numberOfRunningProcesses) 165 NumberOfRunningProcesses := int64(parsed.Value()) 166 result := &statsapi.Summary{ 167 Node: statsapi.NodeStats{ 168 Rlimit: &statsapi.RlimitStats{ 169 MaxPID: &availablePIDs, 170 NumOfRunningProcesses: &NumberOfRunningProcesses, 171 }, 172 }, 173 Pods: []statsapi.PodStats{}, 174 } 175 for _, podStat := range podStats { 176 result.Pods = append(result.Pods, podStat) 177 } 178 return result 179 } 180 181 func makeMemoryStats(nodeAvailableBytes string, podStats map[*v1.Pod]statsapi.PodStats) *statsapi.Summary { 182 val := resource.MustParse(nodeAvailableBytes) 183 availableBytes := uint64(val.Value()) 184 WorkingSetBytes := uint64(val.Value()) 185 result := &statsapi.Summary{ 186 Node: statsapi.NodeStats{ 187 Memory: &statsapi.MemoryStats{ 188 AvailableBytes: &availableBytes, 189 WorkingSetBytes: &WorkingSetBytes, 190 }, 191 SystemContainers: []statsapi.ContainerStats{ 192 { 193 Name: statsapi.SystemContainerPods, 194 Memory: &statsapi.MemoryStats{ 195 AvailableBytes: &availableBytes, 196 WorkingSetBytes: &WorkingSetBytes, 197 }, 198 }, 199 }, 200 }, 201 Pods: []statsapi.PodStats{}, 202 } 203 for _, podStat := range podStats { 204 result.Pods = append(result.Pods, podStat) 205 } 206 return result 207 } 208 209 type diskStats struct { 210 rootFsAvailableBytes string 211 imageFsAvailableBytes string 212 // optional fs 213 // if not specified, than will assume imagefs=containerfs 214 containerFsAvailableBytes string 215 podStats map[*v1.Pod]statsapi.PodStats 216 } 217 218 func makeDiskStats(diskStats diskStats) *statsapi.Summary { 219 rootFsVal := resource.MustParse(diskStats.rootFsAvailableBytes) 220 rootFsBytes := uint64(rootFsVal.Value()) 221 rootFsCapacityBytes := uint64(rootFsVal.Value() * 2) 222 imageFsVal := resource.MustParse(diskStats.imageFsAvailableBytes) 223 imageFsBytes := uint64(imageFsVal.Value()) 224 imageFsCapacityBytes := uint64(imageFsVal.Value() * 2) 225 if diskStats.containerFsAvailableBytes == "" { 226 diskStats.containerFsAvailableBytes = diskStats.imageFsAvailableBytes 227 } 228 containerFsVal := resource.MustParse(diskStats.containerFsAvailableBytes) 229 containerFsBytes := uint64(containerFsVal.Value()) 230 containerFsCapacityBytes := uint64(containerFsVal.Value() * 2) 231 result := &statsapi.Summary{ 232 Node: statsapi.NodeStats{ 233 Fs: &statsapi.FsStats{ 234 AvailableBytes: &rootFsBytes, 235 CapacityBytes: &rootFsCapacityBytes, 236 }, 237 Runtime: &statsapi.RuntimeStats{ 238 ImageFs: &statsapi.FsStats{ 239 AvailableBytes: &imageFsBytes, 240 CapacityBytes: &imageFsCapacityBytes, 241 }, 242 ContainerFs: &statsapi.FsStats{ 243 AvailableBytes: &containerFsBytes, 244 CapacityBytes: &containerFsCapacityBytes, 245 }, 246 }, 247 }, 248 Pods: []statsapi.PodStats{}, 249 } 250 for _, podStat := range diskStats.podStats { 251 result.Pods = append(result.Pods, podStat) 252 } 253 return result 254 } 255 256 type podToMake struct { 257 name string 258 priority int32 259 requests v1.ResourceList 260 limits v1.ResourceList 261 memoryWorkingSet string 262 pidUsage uint64 263 rootFsUsed string 264 logsFsUsed string 265 logsFsInodesUsed string 266 rootFsInodesUsed string 267 perLocalVolumeUsed string 268 perLocalVolumeInodesUsed string 269 } 270 271 func TestMemoryPressure_VerifyPodStatus(t *testing.T) { 272 testCases := map[string]struct { 273 wantPodStatus v1.PodStatus 274 }{ 275 "eviction due to memory pressure; no image fs": { 276 wantPodStatus: v1.PodStatus{ 277 Phase: v1.PodFailed, 278 Reason: "Evicted", 279 Message: "The node was low on resource: memory. Threshold quantity: 2Gi, available: 1500Mi. ", 280 }, 281 }, 282 "eviction due to memory pressure; image fs": { 283 wantPodStatus: v1.PodStatus{ 284 Phase: v1.PodFailed, 285 Reason: "Evicted", 286 Message: "The node was low on resource: memory. Threshold quantity: 2Gi, available: 1500Mi. ", 287 }, 288 }, 289 } 290 for name, tc := range testCases { 291 for _, enablePodDisruptionConditions := range []bool{false, true} { 292 t.Run(fmt.Sprintf("%s;PodDisruptionConditions=%v", name, enablePodDisruptionConditions), func(t *testing.T) { 293 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.PodDisruptionConditions, enablePodDisruptionConditions) 294 295 podMaker := makePodWithMemoryStats 296 summaryStatsMaker := makeMemoryStats 297 podsToMake := []podToMake{ 298 {name: "below-requests", requests: newResourceList("", "1Gi", ""), limits: newResourceList("", "1Gi", ""), memoryWorkingSet: "900Mi"}, 299 {name: "above-requests", requests: newResourceList("", "100Mi", ""), limits: newResourceList("", "1Gi", ""), memoryWorkingSet: "700Mi"}, 300 } 301 pods := []*v1.Pod{} 302 podStats := map[*v1.Pod]statsapi.PodStats{} 303 for _, podToMake := range podsToMake { 304 pod, podStat := podMaker(podToMake.name, podToMake.priority, podToMake.requests, podToMake.limits, podToMake.memoryWorkingSet) 305 pods = append(pods, pod) 306 podStats[pod] = podStat 307 } 308 activePodsFunc := func() []*v1.Pod { 309 return pods 310 } 311 312 fakeClock := testingclock.NewFakeClock(time.Now()) 313 podKiller := &mockPodKiller{} 314 diskInfoProvider := &mockDiskInfoProvider{dedicatedImageFs: ptr.To(false)} 315 diskGC := &mockDiskGC{err: nil} 316 nodeRef := &v1.ObjectReference{Kind: "Node", Name: "test", UID: types.UID("test"), Namespace: ""} 317 318 config := Config{ 319 PressureTransitionPeriod: time.Minute * 5, 320 Thresholds: []evictionapi.Threshold{ 321 { 322 Signal: evictionapi.SignalMemoryAvailable, 323 Operator: evictionapi.OpLessThan, 324 Value: evictionapi.ThresholdValue{ 325 Quantity: quantityMustParse("2Gi"), 326 }, 327 }, 328 }, 329 } 330 summaryProvider := &fakeSummaryProvider{result: summaryStatsMaker("1500Mi", podStats)} 331 manager := &managerImpl{ 332 clock: fakeClock, 333 killPodFunc: podKiller.killPodNow, 334 imageGC: diskGC, 335 containerGC: diskGC, 336 config: config, 337 recorder: &record.FakeRecorder{}, 338 summaryProvider: summaryProvider, 339 nodeRef: nodeRef, 340 nodeConditionsLastObservedAt: nodeConditionsObservedAt{}, 341 thresholdsFirstObservedAt: thresholdsObservedAt{}, 342 } 343 344 // synchronize to detect the memory pressure 345 _, err := manager.synchronize(diskInfoProvider, activePodsFunc) 346 347 if err != nil { 348 t.Fatalf("Manager expects no error but got %v", err) 349 } 350 // verify memory pressure is detected 351 if !manager.IsUnderMemoryPressure() { 352 t.Fatalf("Manager should have detected memory pressure") 353 } 354 355 // verify a pod is selected for eviction 356 if podKiller.pod == nil { 357 t.Fatalf("Manager should have selected a pod for eviction") 358 } 359 360 wantPodStatus := tc.wantPodStatus.DeepCopy() 361 if enablePodDisruptionConditions { 362 wantPodStatus.Conditions = append(wantPodStatus.Conditions, v1.PodCondition{ 363 Type: "DisruptionTarget", 364 Status: "True", 365 Reason: "TerminationByKubelet", 366 Message: "The node was low on resource: memory. Threshold quantity: 2Gi, available: 1500Mi. ", 367 }) 368 } 369 370 // verify the pod status after applying the status update function 371 podKiller.statusFn(&podKiller.pod.Status) 372 if diff := cmp.Diff(*wantPodStatus, podKiller.pod.Status, cmpopts.IgnoreFields(v1.PodCondition{}, "LastProbeTime", "LastTransitionTime")); diff != "" { 373 t.Errorf("Unexpected pod status of the evicted pod (-want,+got):\n%s", diff) 374 } 375 }) 376 } 377 } 378 } 379 380 func TestPIDPressure_VerifyPodStatus(t *testing.T) { 381 testCases := map[string]struct { 382 wantPodStatus v1.PodStatus 383 }{ 384 "eviction due to pid pressure": { 385 wantPodStatus: v1.PodStatus{ 386 Phase: v1.PodFailed, 387 Reason: "Evicted", 388 Message: "The node was low on resource: pids. Threshold quantity: 1200, available: 500. ", 389 }, 390 }, 391 } 392 for name, tc := range testCases { 393 for _, enablePodDisruptionConditions := range []bool{true, false} { 394 t.Run(fmt.Sprintf("%s;PodDisruptionConditions=%v", name, enablePodDisruptionConditions), func(t *testing.T) { 395 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.PodDisruptionConditions, enablePodDisruptionConditions) 396 397 podMaker := makePodWithPIDStats 398 summaryStatsMaker := makePIDStats 399 podsToMake := []podToMake{ 400 {name: "pod1", priority: lowPriority, pidUsage: 500}, 401 {name: "pod2", priority: defaultPriority, pidUsage: 500}, 402 } 403 pods := []*v1.Pod{} 404 podStats := map[*v1.Pod]statsapi.PodStats{} 405 for _, podToMake := range podsToMake { 406 pod, podStat := podMaker(podToMake.name, podToMake.priority, 2) 407 pods = append(pods, pod) 408 podStats[pod] = podStat 409 } 410 activePodsFunc := func() []*v1.Pod { 411 return pods 412 } 413 414 fakeClock := testingclock.NewFakeClock(time.Now()) 415 podKiller := &mockPodKiller{} 416 diskInfoProvider := &mockDiskInfoProvider{dedicatedImageFs: ptr.To(false)} 417 diskGC := &mockDiskGC{err: nil} 418 nodeRef := &v1.ObjectReference{Kind: "Node", Name: "test", UID: types.UID("test"), Namespace: ""} 419 420 config := Config{ 421 PressureTransitionPeriod: time.Minute * 5, 422 Thresholds: []evictionapi.Threshold{ 423 { 424 Signal: evictionapi.SignalPIDAvailable, 425 Operator: evictionapi.OpLessThan, 426 Value: evictionapi.ThresholdValue{ 427 Quantity: quantityMustParse("1200"), 428 }, 429 }, 430 }, 431 } 432 summaryProvider := &fakeSummaryProvider{result: summaryStatsMaker("1500", "1000", podStats)} 433 manager := &managerImpl{ 434 clock: fakeClock, 435 killPodFunc: podKiller.killPodNow, 436 imageGC: diskGC, 437 containerGC: diskGC, 438 config: config, 439 recorder: &record.FakeRecorder{}, 440 summaryProvider: summaryProvider, 441 nodeRef: nodeRef, 442 nodeConditionsLastObservedAt: nodeConditionsObservedAt{}, 443 thresholdsFirstObservedAt: thresholdsObservedAt{}, 444 } 445 446 // synchronize to detect the PID pressure 447 _, err := manager.synchronize(diskInfoProvider, activePodsFunc) 448 449 if err != nil { 450 t.Fatalf("Manager expects no error but got %v", err) 451 } 452 453 // verify PID pressure is detected 454 if !manager.IsUnderPIDPressure() { 455 t.Fatalf("Manager should have detected PID pressure") 456 } 457 458 // verify a pod is selected for eviction 459 if podKiller.pod == nil { 460 t.Fatalf("Manager should have selected a pod for eviction") 461 } 462 463 wantPodStatus := tc.wantPodStatus.DeepCopy() 464 if enablePodDisruptionConditions { 465 wantPodStatus.Conditions = append(wantPodStatus.Conditions, v1.PodCondition{ 466 Type: "DisruptionTarget", 467 Status: "True", 468 Reason: "TerminationByKubelet", 469 Message: "The node was low on resource: pids. Threshold quantity: 1200, available: 500. ", 470 }) 471 } 472 473 // verify the pod status after applying the status update function 474 podKiller.statusFn(&podKiller.pod.Status) 475 if diff := cmp.Diff(*wantPodStatus, podKiller.pod.Status, cmpopts.IgnoreFields(v1.PodCondition{}, "LastProbeTime", "LastTransitionTime")); diff != "" { 476 t.Errorf("Unexpected pod status of the evicted pod (-want,+got):\n%s", diff) 477 } 478 }) 479 } 480 } 481 } 482 483 func TestDiskPressureNodeFs_VerifyPodStatus(t *testing.T) { 484 testCases := map[string]struct { 485 nodeFsStats string 486 imageFsStats string 487 containerFsStats string 488 evictionMessage string 489 kubeletSeparateDiskFeature bool 490 writeableSeparateFromReadOnly bool 491 thresholdToMonitor evictionapi.Threshold 492 podToMakes []podToMake 493 dedicatedImageFs *bool 494 expectErr string 495 }{ 496 "eviction due to disk pressure; no image fs": { 497 dedicatedImageFs: ptr.To(false), 498 nodeFsStats: "1.5Gi", 499 imageFsStats: "10Gi", 500 containerFsStats: "10Gi", 501 thresholdToMonitor: evictionapi.Threshold{ 502 Signal: evictionapi.SignalNodeFsAvailable, 503 Operator: evictionapi.OpLessThan, 504 Value: evictionapi.ThresholdValue{ 505 Quantity: quantityMustParse("2Gi"), 506 }, 507 }, 508 evictionMessage: "The node was low on resource: ephemeral-storage. Threshold quantity: 2Gi, available: 1536Mi. Container above-requests was using 700Mi, request is 100Mi, has larger consumption of ephemeral-storage. ", 509 podToMakes: []podToMake{ 510 {name: "below-requests", requests: newResourceList("", "", "1Gi"), limits: newResourceList("", "", "1Gi"), rootFsUsed: "900Mi"}, 511 {name: "above-requests", requests: newResourceList("", "", "100Mi"), limits: newResourceList("", "", "1Gi"), rootFsUsed: "700Mi"}, 512 }, 513 }, 514 "eviction due to image disk pressure; image fs": { 515 dedicatedImageFs: ptr.To(true), 516 nodeFsStats: "1Gi", 517 imageFsStats: "10Gi", 518 containerFsStats: "10Gi", 519 evictionMessage: "The node was low on resource: ephemeral-storage. Threshold quantity: 50Gi, available: 10Gi. Container above-requests was using 80Gi, request is 50Gi, has larger consumption of ephemeral-storage. ", 520 thresholdToMonitor: evictionapi.Threshold{ 521 Signal: evictionapi.SignalImageFsAvailable, 522 Operator: evictionapi.OpLessThan, 523 Value: evictionapi.ThresholdValue{ 524 Quantity: quantityMustParse("50Gi"), 525 }, 526 }, 527 podToMakes: []podToMake{ 528 {name: "below-requests", requests: newResourceList("", "", "1Gi"), limits: newResourceList("", "", "1Gi"), rootFsUsed: "900Mi"}, 529 {name: "above-requests", requests: newResourceList("", "", "50Gi"), limits: newResourceList("", "", "50Gi"), rootFsUsed: "80Gi"}, 530 }, 531 }, 532 "eviction due to container disk pressure; feature off; error; container fs": { 533 dedicatedImageFs: ptr.To(true), 534 kubeletSeparateDiskFeature: false, 535 writeableSeparateFromReadOnly: true, 536 expectErr: "KubeletSeparateDiskGC is turned off but we still have a split filesystem", 537 nodeFsStats: "1Gi", 538 imageFsStats: "100Gi", 539 containerFsStats: "10Gi", 540 evictionMessage: "The node was low on resource: ephemeral-storage. Threshold quantity: 50Gi, available: 10Gi.Container above-requests was using 80Gi, request is 50Gi, has larger consumption of ephemeral-storage. ", 541 thresholdToMonitor: evictionapi.Threshold{ 542 Signal: evictionapi.SignalContainerFsAvailable, 543 Operator: evictionapi.OpLessThan, 544 Value: evictionapi.ThresholdValue{ 545 Quantity: quantityMustParse("50Gi"), 546 }, 547 }, 548 podToMakes: []podToMake{ 549 {name: "below-requests", requests: newResourceList("", "", "1Gi"), limits: newResourceList("", "", "1Gi"), rootFsUsed: "900Mi"}, 550 {name: "above-requests", requests: newResourceList("", "", "50Gi"), limits: newResourceList("", "", "50Gi"), rootFsUsed: "80Gi"}, 551 }, 552 }, 553 "eviction due to container disk pressure; container fs": { 554 dedicatedImageFs: ptr.To(true), 555 kubeletSeparateDiskFeature: true, 556 writeableSeparateFromReadOnly: true, 557 nodeFsStats: "10Gi", 558 imageFsStats: "100Gi", 559 containerFsStats: "10Gi", 560 evictionMessage: "The node was low on resource: ephemeral-storage. Threshold quantity: 50Gi, available: 10Gi. Container above-requests was using 80Gi, request is 50Gi, has larger consumption of ephemeral-storage. ", 561 thresholdToMonitor: evictionapi.Threshold{ 562 Signal: evictionapi.SignalNodeFsAvailable, 563 Operator: evictionapi.OpLessThan, 564 Value: evictionapi.ThresholdValue{ 565 Quantity: quantityMustParse("50Gi"), 566 }, 567 }, 568 podToMakes: []podToMake{ 569 {name: "below-requests", requests: newResourceList("", "", "1Gi"), limits: newResourceList("", "", "1Gi"), rootFsUsed: "900Mi"}, 570 {name: "above-requests", requests: newResourceList("", "", "50Gi"), limits: newResourceList("", "", "50Gi"), rootFsUsed: "80Gi"}, 571 }, 572 }, 573 } 574 for name, tc := range testCases { 575 for _, enablePodDisruptionConditions := range []bool{false, true} { 576 t.Run(fmt.Sprintf("%s;PodDisruptionConditions=%v", name, enablePodDisruptionConditions), func(t *testing.T) { 577 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.KubeletSeparateDiskGC, tc.kubeletSeparateDiskFeature) 578 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.PodDisruptionConditions, enablePodDisruptionConditions) 579 580 podMaker := makePodWithDiskStats 581 summaryStatsMaker := makeDiskStats 582 podsToMake := tc.podToMakes 583 wantPodStatus := v1.PodStatus{ 584 Phase: v1.PodFailed, 585 Reason: "Evicted", 586 Message: tc.evictionMessage, 587 } 588 pods := []*v1.Pod{} 589 podStats := map[*v1.Pod]statsapi.PodStats{} 590 for _, podToMake := range podsToMake { 591 pod, podStat := podMaker(podToMake.name, podToMake.priority, podToMake.requests, podToMake.limits, podToMake.rootFsUsed, podToMake.logsFsUsed, podToMake.perLocalVolumeUsed, nil) 592 pods = append(pods, pod) 593 podStats[pod] = podStat 594 } 595 activePodsFunc := func() []*v1.Pod { 596 return pods 597 } 598 599 fakeClock := testingclock.NewFakeClock(time.Now()) 600 podKiller := &mockPodKiller{} 601 diskInfoProvider := &mockDiskInfoProvider{dedicatedImageFs: tc.dedicatedImageFs} 602 diskGC := &mockDiskGC{err: nil, readAndWriteSeparate: tc.writeableSeparateFromReadOnly} 603 nodeRef := &v1.ObjectReference{Kind: "Node", Name: "test", UID: types.UID("test"), Namespace: ""} 604 605 config := Config{ 606 PressureTransitionPeriod: time.Minute * 5, 607 Thresholds: []evictionapi.Threshold{tc.thresholdToMonitor}, 608 } 609 diskStat := diskStats{ 610 rootFsAvailableBytes: tc.nodeFsStats, 611 imageFsAvailableBytes: tc.imageFsStats, 612 containerFsAvailableBytes: tc.containerFsStats, 613 podStats: podStats, 614 } 615 summaryProvider := &fakeSummaryProvider{result: summaryStatsMaker(diskStat)} 616 manager := &managerImpl{ 617 clock: fakeClock, 618 killPodFunc: podKiller.killPodNow, 619 imageGC: diskGC, 620 containerGC: diskGC, 621 config: config, 622 recorder: &record.FakeRecorder{}, 623 summaryProvider: summaryProvider, 624 nodeRef: nodeRef, 625 nodeConditionsLastObservedAt: nodeConditionsObservedAt{}, 626 thresholdsFirstObservedAt: thresholdsObservedAt{}, 627 } 628 629 // synchronize 630 pods, synchErr := manager.synchronize(diskInfoProvider, activePodsFunc) 631 632 if synchErr == nil && tc.expectErr != "" { 633 t.Fatalf("Manager should report error but did not") 634 } else if tc.expectErr != "" && synchErr != nil { 635 if diff := cmp.Diff(tc.expectErr, synchErr.Error()); diff != "" { 636 t.Errorf("Unexpected error (-want,+got):\n%s", diff) 637 } 638 } else { 639 // verify manager detected disk pressure 640 if !manager.IsUnderDiskPressure() { 641 t.Fatalf("Manager should report disk pressure") 642 } 643 644 // verify a pod is selected for eviction 645 if podKiller.pod == nil { 646 t.Fatalf("Manager should have selected a pod for eviction") 647 } 648 649 if enablePodDisruptionConditions { 650 wantPodStatus.Conditions = append(wantPodStatus.Conditions, v1.PodCondition{ 651 Type: "DisruptionTarget", 652 Status: "True", 653 Reason: "TerminationByKubelet", 654 Message: tc.evictionMessage, 655 }) 656 } 657 658 // verify the pod status after applying the status update function 659 podKiller.statusFn(&podKiller.pod.Status) 660 if diff := cmp.Diff(wantPodStatus, podKiller.pod.Status, cmpopts.IgnoreFields(v1.PodCondition{}, "LastProbeTime", "LastTransitionTime")); diff != "" { 661 t.Errorf("Unexpected pod status of the evicted pod (-want,+got):\n%s", diff) 662 } 663 } 664 }) 665 } 666 } 667 } 668 669 // TestMemoryPressure 670 func TestMemoryPressure(t *testing.T) { 671 podMaker := makePodWithMemoryStats 672 summaryStatsMaker := makeMemoryStats 673 podsToMake := []podToMake{ 674 {name: "guaranteed-low-priority-high-usage", priority: lowPriority, requests: newResourceList("100m", "1Gi", ""), limits: newResourceList("100m", "1Gi", ""), memoryWorkingSet: "900Mi"}, 675 {name: "burstable-below-requests", priority: defaultPriority, requests: newResourceList("100m", "100Mi", ""), limits: newResourceList("200m", "1Gi", ""), memoryWorkingSet: "50Mi"}, 676 {name: "burstable-above-requests", priority: defaultPriority, requests: newResourceList("100m", "100Mi", ""), limits: newResourceList("200m", "1Gi", ""), memoryWorkingSet: "400Mi"}, 677 {name: "best-effort-high-priority-high-usage", priority: highPriority, requests: newResourceList("", "", ""), limits: newResourceList("", "", ""), memoryWorkingSet: "400Mi"}, 678 {name: "best-effort-low-priority-low-usage", priority: lowPriority, requests: newResourceList("", "", ""), limits: newResourceList("", "", ""), memoryWorkingSet: "100Mi"}, 679 } 680 pods := []*v1.Pod{} 681 podStats := map[*v1.Pod]statsapi.PodStats{} 682 for _, podToMake := range podsToMake { 683 pod, podStat := podMaker(podToMake.name, podToMake.priority, podToMake.requests, podToMake.limits, podToMake.memoryWorkingSet) 684 pods = append(pods, pod) 685 podStats[pod] = podStat 686 } 687 podToEvict := pods[4] 688 activePodsFunc := func() []*v1.Pod { 689 return pods 690 } 691 692 fakeClock := testingclock.NewFakeClock(time.Now()) 693 podKiller := &mockPodKiller{} 694 diskInfoProvider := &mockDiskInfoProvider{dedicatedImageFs: ptr.To(false)} 695 diskGC := &mockDiskGC{err: nil} 696 nodeRef := &v1.ObjectReference{Kind: "Node", Name: "test", UID: types.UID("test"), Namespace: ""} 697 698 config := Config{ 699 MaxPodGracePeriodSeconds: 5, 700 PressureTransitionPeriod: time.Minute * 5, 701 Thresholds: []evictionapi.Threshold{ 702 { 703 Signal: evictionapi.SignalMemoryAvailable, 704 Operator: evictionapi.OpLessThan, 705 Value: evictionapi.ThresholdValue{ 706 Quantity: quantityMustParse("1Gi"), 707 }, 708 }, 709 { 710 Signal: evictionapi.SignalMemoryAvailable, 711 Operator: evictionapi.OpLessThan, 712 Value: evictionapi.ThresholdValue{ 713 Quantity: quantityMustParse("2Gi"), 714 }, 715 GracePeriod: time.Minute * 2, 716 }, 717 }, 718 } 719 summaryProvider := &fakeSummaryProvider{result: summaryStatsMaker("2Gi", podStats)} 720 manager := &managerImpl{ 721 clock: fakeClock, 722 killPodFunc: podKiller.killPodNow, 723 imageGC: diskGC, 724 containerGC: diskGC, 725 config: config, 726 recorder: &record.FakeRecorder{}, 727 summaryProvider: summaryProvider, 728 nodeRef: nodeRef, 729 nodeConditionsLastObservedAt: nodeConditionsObservedAt{}, 730 thresholdsFirstObservedAt: thresholdsObservedAt{}, 731 } 732 733 // create a best effort pod to test admission 734 bestEffortPodToAdmit, _ := podMaker("best-admit", defaultPriority, newResourceList("", "", ""), newResourceList("", "", ""), "0Gi") 735 burstablePodToAdmit, _ := podMaker("burst-admit", defaultPriority, newResourceList("100m", "100Mi", ""), newResourceList("200m", "200Mi", ""), "0Gi") 736 737 // synchronize 738 _, err := manager.synchronize(diskInfoProvider, activePodsFunc) 739 740 if err != nil { 741 t.Fatalf("Manager expects no error but got %v", err) 742 } 743 744 // we should not have memory pressure 745 if manager.IsUnderMemoryPressure() { 746 t.Errorf("Manager should not report memory pressure") 747 } 748 749 // try to admit our pods (they should succeed) 750 expected := []bool{true, true} 751 for i, pod := range []*v1.Pod{bestEffortPodToAdmit, burstablePodToAdmit} { 752 if result := manager.Admit(&lifecycle.PodAdmitAttributes{Pod: pod}); expected[i] != result.Admit { 753 t.Errorf("Admit pod: %v, expected: %v, actual: %v", pod, expected[i], result.Admit) 754 } 755 } 756 757 // induce soft threshold 758 fakeClock.Step(1 * time.Minute) 759 summaryProvider.result = summaryStatsMaker("1500Mi", podStats) 760 _, err = manager.synchronize(diskInfoProvider, activePodsFunc) 761 762 if err != nil { 763 t.Fatalf("Manager expects no error but got %v", err) 764 } 765 766 // we should have memory pressure 767 if !manager.IsUnderMemoryPressure() { 768 t.Errorf("Manager should report memory pressure since soft threshold was met") 769 } 770 771 // verify no pod was yet killed because there has not yet been enough time passed. 772 if podKiller.pod != nil { 773 t.Errorf("Manager should not have killed a pod yet, but killed: %v", podKiller.pod.Name) 774 } 775 776 // step forward in time pass the grace period 777 fakeClock.Step(3 * time.Minute) 778 summaryProvider.result = summaryStatsMaker("1500Mi", podStats) 779 _, err = manager.synchronize(diskInfoProvider, activePodsFunc) 780 781 if err != nil { 782 t.Fatalf("Manager expects no error but got %v", err) 783 } 784 785 // we should have memory pressure 786 if !manager.IsUnderMemoryPressure() { 787 t.Errorf("Manager should report memory pressure since soft threshold was met") 788 } 789 790 // verify the right pod was killed with the right grace period. 791 if podKiller.pod != podToEvict { 792 t.Errorf("Manager chose to kill pod: %v, but should have chosen %v", podKiller.pod.Name, podToEvict.Name) 793 } 794 if podKiller.gracePeriodOverride == nil { 795 t.Errorf("Manager chose to kill pod but should have had a grace period override.") 796 } 797 observedGracePeriod := *podKiller.gracePeriodOverride 798 if observedGracePeriod != manager.config.MaxPodGracePeriodSeconds { 799 t.Errorf("Manager chose to kill pod with incorrect grace period. Expected: %d, actual: %d", manager.config.MaxPodGracePeriodSeconds, observedGracePeriod) 800 } 801 // reset state 802 podKiller.pod = nil 803 podKiller.gracePeriodOverride = nil 804 805 // remove memory pressure 806 fakeClock.Step(20 * time.Minute) 807 summaryProvider.result = summaryStatsMaker("3Gi", podStats) 808 _, err = manager.synchronize(diskInfoProvider, activePodsFunc) 809 810 if err != nil { 811 t.Fatalf("Manager expects no error but got %v", err) 812 } 813 814 // we should not have memory pressure 815 if manager.IsUnderMemoryPressure() { 816 t.Errorf("Manager should not report memory pressure") 817 } 818 819 // induce memory pressure! 820 fakeClock.Step(1 * time.Minute) 821 summaryProvider.result = summaryStatsMaker("500Mi", podStats) 822 _, err = manager.synchronize(diskInfoProvider, activePodsFunc) 823 824 if err != nil { 825 t.Fatalf("Manager expects no error but got %v", err) 826 } 827 828 // we should have memory pressure 829 if !manager.IsUnderMemoryPressure() { 830 t.Errorf("Manager should report memory pressure") 831 } 832 833 // check the right pod was killed 834 if podKiller.pod != podToEvict { 835 t.Errorf("Manager chose to kill pod: %v, but should have chosen %v", podKiller.pod.Name, podToEvict.Name) 836 } 837 observedGracePeriod = *podKiller.gracePeriodOverride 838 if observedGracePeriod != int64(1) { 839 t.Errorf("Manager chose to kill pod with incorrect grace period. Expected: %d, actual: %d", 1, observedGracePeriod) 840 } 841 842 // the best-effort pod should not admit, burstable should 843 expected = []bool{false, true} 844 for i, pod := range []*v1.Pod{bestEffortPodToAdmit, burstablePodToAdmit} { 845 if result := manager.Admit(&lifecycle.PodAdmitAttributes{Pod: pod}); expected[i] != result.Admit { 846 t.Errorf("Admit pod: %v, expected: %v, actual: %v", pod, expected[i], result.Admit) 847 } 848 } 849 850 // reduce memory pressure 851 fakeClock.Step(1 * time.Minute) 852 summaryProvider.result = summaryStatsMaker("2Gi", podStats) 853 podKiller.pod = nil // reset state 854 _, err = manager.synchronize(diskInfoProvider, activePodsFunc) 855 856 if err != nil { 857 t.Fatalf("Manager expects no error but got %v", err) 858 } 859 860 // we should have memory pressure (because transition period not yet met) 861 if !manager.IsUnderMemoryPressure() { 862 t.Errorf("Manager should report memory pressure") 863 } 864 865 // no pod should have been killed 866 if podKiller.pod != nil { 867 t.Errorf("Manager chose to kill pod: %v when no pod should have been killed", podKiller.pod.Name) 868 } 869 870 // the best-effort pod should not admit, burstable should 871 expected = []bool{false, true} 872 for i, pod := range []*v1.Pod{bestEffortPodToAdmit, burstablePodToAdmit} { 873 if result := manager.Admit(&lifecycle.PodAdmitAttributes{Pod: pod}); expected[i] != result.Admit { 874 t.Errorf("Admit pod: %v, expected: %v, actual: %v", pod, expected[i], result.Admit) 875 } 876 } 877 878 // move the clock past transition period to ensure that we stop reporting pressure 879 fakeClock.Step(5 * time.Minute) 880 summaryProvider.result = summaryStatsMaker("2Gi", podStats) 881 podKiller.pod = nil // reset state 882 _, err = manager.synchronize(diskInfoProvider, activePodsFunc) 883 884 if err != nil { 885 t.Fatalf("Manager expects no error but got %v", err) 886 } 887 888 // we should not have memory pressure (because transition period met) 889 if manager.IsUnderMemoryPressure() { 890 t.Errorf("Manager should not report memory pressure") 891 } 892 893 // no pod should have been killed 894 if podKiller.pod != nil { 895 t.Errorf("Manager chose to kill pod: %v when no pod should have been killed", podKiller.pod.Name) 896 } 897 898 // all pods should admit now 899 expected = []bool{true, true} 900 for i, pod := range []*v1.Pod{bestEffortPodToAdmit, burstablePodToAdmit} { 901 if result := manager.Admit(&lifecycle.PodAdmitAttributes{Pod: pod}); expected[i] != result.Admit { 902 t.Errorf("Admit pod: %v, expected: %v, actual: %v", pod, expected[i], result.Admit) 903 } 904 } 905 } 906 907 func makeContainersByQOS(class v1.PodQOSClass) []v1.Container { 908 resource := newResourceList("100m", "1Gi", "") 909 switch class { 910 case v1.PodQOSGuaranteed: 911 return []v1.Container{newContainer("guaranteed-container", resource, resource)} 912 case v1.PodQOSBurstable: 913 return []v1.Container{newContainer("burtable-container", resource, nil)} 914 case v1.PodQOSBestEffort: 915 fallthrough 916 default: 917 return []v1.Container{newContainer("best-effort-container", nil, nil)} 918 } 919 } 920 921 func TestPIDPressure(t *testing.T) { 922 testCases := []struct { 923 name string 924 podsToMake []podToMake 925 evictPodIndex int 926 noPressurePIDUsage string 927 pressurePIDUsageWithGracePeriod string 928 pressurePIDUsageWithoutGracePeriod string 929 totalPID string 930 }{ 931 { 932 name: "eviction due to pid pressure", 933 podsToMake: []podToMake{ 934 {name: "high-priority-high-usage", priority: highPriority, pidUsage: 900}, 935 {name: "default-priority-low-usage", priority: defaultPriority, pidUsage: 100}, 936 {name: "default-priority-medium-usage", priority: defaultPriority, pidUsage: 400}, 937 {name: "low-priority-high-usage", priority: lowPriority, pidUsage: 600}, 938 {name: "low-priority-low-usage", priority: lowPriority, pidUsage: 50}, 939 }, 940 evictPodIndex: 3, // we expect the low-priority-high-usage pod to be evicted 941 noPressurePIDUsage: "300", 942 pressurePIDUsageWithGracePeriod: "700", 943 pressurePIDUsageWithoutGracePeriod: "1200", 944 totalPID: "2000", 945 }, 946 } 947 948 for _, tc := range testCases { 949 t.Run(tc.name, func(t *testing.T) { 950 podMaker := makePodWithPIDStats 951 summaryStatsMaker := makePIDStats 952 pods := []*v1.Pod{} 953 podStats := map[*v1.Pod]statsapi.PodStats{} 954 for _, podToMake := range tc.podsToMake { 955 pod, podStat := podMaker(podToMake.name, podToMake.priority, podToMake.pidUsage) 956 pods = append(pods, pod) 957 podStats[pod] = podStat 958 } 959 podToEvict := pods[tc.evictPodIndex] 960 activePodsFunc := func() []*v1.Pod { return pods } 961 962 fakeClock := testingclock.NewFakeClock(time.Now()) 963 podKiller := &mockPodKiller{} 964 diskInfoProvider := &mockDiskInfoProvider{dedicatedImageFs: ptr.To(false)} 965 diskGC := &mockDiskGC{err: nil} 966 nodeRef := &v1.ObjectReference{Kind: "Node", Name: "test", UID: types.UID("test"), Namespace: ""} 967 968 config := Config{ 969 MaxPodGracePeriodSeconds: 5, 970 PressureTransitionPeriod: time.Minute * 5, 971 Thresholds: []evictionapi.Threshold{ 972 { 973 Signal: evictionapi.SignalPIDAvailable, 974 Operator: evictionapi.OpLessThan, 975 Value: evictionapi.ThresholdValue{ 976 Quantity: quantityMustParse("1200"), 977 }, 978 }, 979 { 980 Signal: evictionapi.SignalPIDAvailable, 981 Operator: evictionapi.OpLessThan, 982 Value: evictionapi.ThresholdValue{ 983 Quantity: quantityMustParse("1500"), 984 }, 985 GracePeriod: time.Minute * 2, 986 }, 987 }, 988 } 989 990 summaryProvider := &fakeSummaryProvider{result: summaryStatsMaker(tc.totalPID, tc.noPressurePIDUsage, podStats)} 991 manager := &managerImpl{ 992 clock: fakeClock, 993 killPodFunc: podKiller.killPodNow, 994 imageGC: diskGC, 995 containerGC: diskGC, 996 config: config, 997 recorder: &record.FakeRecorder{}, 998 summaryProvider: summaryProvider, 999 nodeRef: nodeRef, 1000 nodeConditionsLastObservedAt: nodeConditionsObservedAt{}, 1001 thresholdsFirstObservedAt: thresholdsObservedAt{}, 1002 } 1003 1004 // create a pod to test admission 1005 podToAdmit, _ := podMaker("pod-to-admit", defaultPriority, 50) 1006 1007 // synchronize 1008 _, err := manager.synchronize(diskInfoProvider, activePodsFunc) 1009 1010 if err != nil { 1011 t.Fatalf("Manager expects no error but got %v", err) 1012 } 1013 1014 // we should not have PID pressure 1015 if manager.IsUnderPIDPressure() { 1016 t.Fatalf("Manager should not report PID pressure") 1017 } 1018 1019 // try to admit our pod (should succeed) 1020 if result := manager.Admit(&lifecycle.PodAdmitAttributes{Pod: podToAdmit}); !result.Admit { 1021 t.Fatalf("Admit pod: %v, expected: %v, actual: %v", podToAdmit, true, result.Admit) 1022 } 1023 1024 // induce soft threshold for PID pressure 1025 fakeClock.Step(1 * time.Minute) 1026 summaryProvider.result = summaryStatsMaker(tc.totalPID, tc.pressurePIDUsageWithGracePeriod, podStats) 1027 _, err = manager.synchronize(diskInfoProvider, activePodsFunc) 1028 1029 if err != nil { 1030 t.Fatalf("Manager expects no error but got %v", err) 1031 } 1032 1033 // now, we should have PID pressure 1034 if !manager.IsUnderPIDPressure() { 1035 t.Errorf("Manager should report PID pressure since soft threshold was met") 1036 } 1037 1038 // verify no pod was yet killed because there has not yet been enough time passed 1039 if podKiller.pod != nil { 1040 t.Errorf("Manager should not have killed a pod yet, but killed: %v", podKiller.pod.Name) 1041 } 1042 1043 // step forward in time past the grace period 1044 fakeClock.Step(3 * time.Minute) 1045 // no change in PID stats to simulate continued pressure 1046 _, err = manager.synchronize(diskInfoProvider, activePodsFunc) 1047 1048 if err != nil { 1049 t.Fatalf("Manager expects no error but got %v", err) 1050 } 1051 1052 // verify PID pressure is still reported 1053 if !manager.IsUnderPIDPressure() { 1054 t.Errorf("Manager should still report PID pressure") 1055 } 1056 1057 // verify the right pod was killed with the right grace period. 1058 if podKiller.pod != podToEvict { 1059 t.Errorf("Manager chose to kill pod: %v, but should have chosen %v", podKiller.pod.Name, podToEvict.Name) 1060 } 1061 if podKiller.gracePeriodOverride == nil { 1062 t.Errorf("Manager chose to kill pod but should have had a grace period override.") 1063 } 1064 observedGracePeriod := *podKiller.gracePeriodOverride 1065 if observedGracePeriod != manager.config.MaxPodGracePeriodSeconds { 1066 t.Errorf("Manager chose to kill pod with incorrect grace period. Expected: %d, actual: %d", manager.config.MaxPodGracePeriodSeconds, observedGracePeriod) 1067 } 1068 1069 // reset state 1070 podKiller.pod = nil 1071 podKiller.gracePeriodOverride = nil 1072 1073 // remove PID pressure by simulating increased PID availability 1074 fakeClock.Step(20 * time.Minute) 1075 summaryProvider.result = summaryStatsMaker(tc.totalPID, tc.noPressurePIDUsage, podStats) // Simulate increased PID availability 1076 _, err = manager.synchronize(diskInfoProvider, activePodsFunc) 1077 1078 if err != nil { 1079 t.Fatalf("Manager expects no error but got %v", err) 1080 } 1081 1082 // verify PID pressure is resolved 1083 if manager.IsUnderPIDPressure() { 1084 t.Errorf("Manager should not report PID pressure") 1085 } 1086 1087 // re-induce PID pressure 1088 fakeClock.Step(1 * time.Minute) 1089 summaryProvider.result = summaryStatsMaker(tc.totalPID, tc.pressurePIDUsageWithoutGracePeriod, podStats) 1090 _, err = manager.synchronize(diskInfoProvider, activePodsFunc) 1091 1092 if err != nil { 1093 t.Fatalf("Manager expects no error but got %v", err) 1094 } 1095 1096 // verify PID pressure is reported again 1097 if !manager.IsUnderPIDPressure() { 1098 t.Errorf("Manager should report PID pressure") 1099 } 1100 1101 // verify the right pod was killed with the right grace period. 1102 if podKiller.pod != podToEvict { 1103 t.Errorf("Manager chose to kill pod: %v, but should have chosen %v", podKiller.pod.Name, podToEvict.Name) 1104 } 1105 if podKiller.gracePeriodOverride == nil { 1106 t.Errorf("Manager chose to kill pod but should have had a grace period override.") 1107 } 1108 observedGracePeriod = *podKiller.gracePeriodOverride 1109 if observedGracePeriod != int64(1) { 1110 t.Errorf("Manager chose to kill pod with incorrect grace period. Expected: %d, actual: %d", 1, observedGracePeriod) 1111 } 1112 1113 // try to admit our pod (should fail) 1114 if result := manager.Admit(&lifecycle.PodAdmitAttributes{Pod: podToAdmit}); result.Admit { 1115 t.Fatalf("Admit pod: %v, expected: %v, actual: %v", podToAdmit, false, result.Admit) 1116 } 1117 1118 // reduce PID pressure 1119 fakeClock.Step(1 * time.Minute) 1120 summaryProvider.result = summaryStatsMaker(tc.totalPID, tc.noPressurePIDUsage, podStats) 1121 podKiller.pod = nil // reset state 1122 _, err = manager.synchronize(diskInfoProvider, activePodsFunc) 1123 1124 if err != nil { 1125 t.Fatalf("Manager expects no error but got %v", err) 1126 } 1127 1128 // we should have PID pressure (because transition period not yet met) 1129 if !manager.IsUnderPIDPressure() { 1130 t.Errorf("Manager should report PID pressure") 1131 } 1132 1133 // no pod should have been killed 1134 if podKiller.pod != nil { 1135 t.Errorf("Manager chose to kill pod: %v when no pod should have been killed", podKiller.pod.Name) 1136 } 1137 1138 // try to admit our pod (should fail) 1139 if result := manager.Admit(&lifecycle.PodAdmitAttributes{Pod: podToAdmit}); result.Admit { 1140 t.Fatalf("Admit pod: %v, expected: %v, actual: %v", podToAdmit, false, result.Admit) 1141 } 1142 1143 // move the clock past the transition period 1144 fakeClock.Step(5 * time.Minute) 1145 summaryProvider.result = summaryStatsMaker(tc.totalPID, tc.noPressurePIDUsage, podStats) 1146 _, err = manager.synchronize(diskInfoProvider, activePodsFunc) 1147 1148 if err != nil { 1149 t.Fatalf("Manager expects no error but got %v", err) 1150 } 1151 1152 // we should not have PID pressure (because transition period met) 1153 if manager.IsUnderPIDPressure() { 1154 t.Errorf("Manager should not report PID pressure") 1155 } 1156 1157 // no pod should have been killed 1158 if podKiller.pod != nil { 1159 t.Errorf("Manager chose to kill pod: %v when no pod should have been killed", podKiller.pod.Name) 1160 } 1161 1162 // try to admit our pod (should succeed) 1163 if result := manager.Admit(&lifecycle.PodAdmitAttributes{Pod: podToAdmit}); !result.Admit { 1164 t.Fatalf("Admit pod: %v, expected: %v, actual: %v", podToAdmit, true, result.Admit) 1165 } 1166 }) 1167 } 1168 } 1169 1170 func TestAdmitUnderNodeConditions(t *testing.T) { 1171 manager := &managerImpl{} 1172 pods := []*v1.Pod{ 1173 newPod("guaranteed-pod", scheduling.DefaultPriorityWhenNoDefaultClassExists, makeContainersByQOS(v1.PodQOSGuaranteed), nil), 1174 newPod("burstable-pod", scheduling.DefaultPriorityWhenNoDefaultClassExists, makeContainersByQOS(v1.PodQOSBurstable), nil), 1175 newPod("best-effort-pod", scheduling.DefaultPriorityWhenNoDefaultClassExists, makeContainersByQOS(v1.PodQOSBestEffort), nil), 1176 } 1177 1178 expected := []bool{true, true, true} 1179 for i, pod := range pods { 1180 if result := manager.Admit(&lifecycle.PodAdmitAttributes{Pod: pod}); expected[i] != result.Admit { 1181 t.Errorf("Admit pod: %v, expected: %v, actual: %v", pod, expected[i], result.Admit) 1182 } 1183 } 1184 1185 manager.nodeConditions = []v1.NodeConditionType{v1.NodeMemoryPressure} 1186 expected = []bool{true, true, false} 1187 for i, pod := range pods { 1188 if result := manager.Admit(&lifecycle.PodAdmitAttributes{Pod: pod}); expected[i] != result.Admit { 1189 t.Errorf("Admit pod: %v, expected: %v, actual: %v", pod, expected[i], result.Admit) 1190 } 1191 } 1192 1193 manager.nodeConditions = []v1.NodeConditionType{v1.NodeMemoryPressure, v1.NodeDiskPressure} 1194 expected = []bool{false, false, false} 1195 for i, pod := range pods { 1196 if result := manager.Admit(&lifecycle.PodAdmitAttributes{Pod: pod}); expected[i] != result.Admit { 1197 t.Errorf("Admit pod: %v, expected: %v, actual: %v", pod, expected[i], result.Admit) 1198 } 1199 } 1200 } 1201 1202 // parseQuantity parses the specified value (if provided) otherwise returns 0 value 1203 func parseQuantity(value string) resource.Quantity { 1204 if len(value) == 0 { 1205 return resource.MustParse("0") 1206 } 1207 return resource.MustParse(value) 1208 } 1209 1210 func TestDiskPressureNodeFs(t *testing.T) { 1211 1212 testCases := map[string]struct { 1213 nodeFsStats string 1214 imageFsStats string 1215 containerFsStats string 1216 kubeletSeparateDiskFeature bool 1217 writeableSeparateFromReadOnly bool 1218 thresholdToMonitor []evictionapi.Threshold 1219 podToMakes []podToMake 1220 dedicatedImageFs *bool 1221 expectErr string 1222 inducePressureOnWhichFs string 1223 softDiskPressure string 1224 hardDiskPressure string 1225 }{ 1226 "eviction due to disk pressure; no image fs": { 1227 dedicatedImageFs: ptr.To(false), 1228 nodeFsStats: "16Gi", 1229 imageFsStats: "16Gi", 1230 containerFsStats: "16Gi", 1231 inducePressureOnWhichFs: "nodefs", 1232 softDiskPressure: "1.5Gi", 1233 hardDiskPressure: "750Mi", 1234 thresholdToMonitor: []evictionapi.Threshold{ 1235 { 1236 Signal: evictionapi.SignalNodeFsAvailable, 1237 Operator: evictionapi.OpLessThan, 1238 Value: evictionapi.ThresholdValue{ 1239 Quantity: quantityMustParse("1Gi"), 1240 }, 1241 }, 1242 { 1243 Signal: evictionapi.SignalNodeFsAvailable, 1244 Operator: evictionapi.OpLessThan, 1245 Value: evictionapi.ThresholdValue{ 1246 Quantity: quantityMustParse("2Gi"), 1247 }, 1248 GracePeriod: time.Minute * 2, 1249 }, 1250 }, 1251 podToMakes: []podToMake{ 1252 {name: "low-priority-high-usage", priority: lowPriority, requests: newResourceList("100m", "1Gi", ""), limits: newResourceList("100m", "1Gi", ""), rootFsUsed: "900Mi"}, 1253 {name: "below-requests", priority: defaultPriority, requests: newResourceList("100m", "100Mi", ""), limits: newResourceList("200m", "1Gi", ""), logsFsUsed: "50Mi"}, 1254 {name: "above-requests", priority: defaultPriority, requests: newResourceList("100m", "100Mi", ""), limits: newResourceList("200m", "1Gi", ""), rootFsUsed: "400Mi"}, 1255 {name: "high-priority-high-usage", priority: highPriority, requests: newResourceList("", "", ""), limits: newResourceList("", "", ""), perLocalVolumeUsed: "400Mi"}, 1256 {name: "low-priority-low-usage", priority: lowPriority, requests: newResourceList("", "", ""), limits: newResourceList("", "", ""), rootFsUsed: "100Mi"}, 1257 }, 1258 }, 1259 "eviction due to image disk pressure; image fs": { 1260 dedicatedImageFs: ptr.To(true), 1261 nodeFsStats: "16Gi", 1262 imageFsStats: "16Gi", 1263 containerFsStats: "16Gi", 1264 softDiskPressure: "1.5Gi", 1265 hardDiskPressure: "750Mi", 1266 inducePressureOnWhichFs: "imagefs", 1267 thresholdToMonitor: []evictionapi.Threshold{ 1268 { 1269 Signal: evictionapi.SignalImageFsAvailable, 1270 Operator: evictionapi.OpLessThan, 1271 Value: evictionapi.ThresholdValue{ 1272 Quantity: quantityMustParse("1Gi"), 1273 }, 1274 }, 1275 { 1276 Signal: evictionapi.SignalImageFsAvailable, 1277 Operator: evictionapi.OpLessThan, 1278 Value: evictionapi.ThresholdValue{ 1279 Quantity: quantityMustParse("2Gi"), 1280 }, 1281 GracePeriod: time.Minute * 2, 1282 }, 1283 }, 1284 podToMakes: []podToMake{ 1285 {name: "low-priority-high-usage", priority: lowPriority, requests: newResourceList("100m", "1Gi", ""), limits: newResourceList("100m", "1Gi", ""), rootFsUsed: "900Mi"}, 1286 {name: "below-requests", priority: defaultPriority, requests: newResourceList("100m", "100Mi", ""), limits: newResourceList("200m", "1Gi", ""), logsFsUsed: "50Mi"}, 1287 {name: "above-requests", priority: defaultPriority, requests: newResourceList("100m", "100Mi", ""), limits: newResourceList("200m", "1Gi", ""), rootFsUsed: "400Mi"}, 1288 {name: "high-priority-high-usage", priority: highPriority, requests: newResourceList("", "", ""), limits: newResourceList("", "", ""), perLocalVolumeUsed: "400Mi"}, 1289 {name: "low-priority-low-usage", priority: lowPriority, requests: newResourceList("", "", ""), limits: newResourceList("", "", ""), rootFsUsed: "100Mi"}, 1290 }, 1291 }, 1292 "eviction due to container disk pressure; container fs": { 1293 dedicatedImageFs: ptr.To(true), 1294 kubeletSeparateDiskFeature: true, 1295 writeableSeparateFromReadOnly: true, 1296 nodeFsStats: "16Gi", 1297 imageFsStats: "16Gi", 1298 containerFsStats: "16Gi", 1299 softDiskPressure: "1.5Gi", 1300 hardDiskPressure: "750Mi", 1301 inducePressureOnWhichFs: "containerfs", 1302 thresholdToMonitor: []evictionapi.Threshold{ 1303 { 1304 Signal: evictionapi.SignalNodeFsAvailable, 1305 Operator: evictionapi.OpLessThan, 1306 Value: evictionapi.ThresholdValue{ 1307 Quantity: quantityMustParse("1Gi"), 1308 }, 1309 }, 1310 { 1311 Signal: evictionapi.SignalNodeFsAvailable, 1312 Operator: evictionapi.OpLessThan, 1313 Value: evictionapi.ThresholdValue{ 1314 Quantity: quantityMustParse("2Gi"), 1315 }, 1316 GracePeriod: time.Minute * 2, 1317 }, 1318 }, 1319 podToMakes: []podToMake{ 1320 {name: "low-priority-high-usage", priority: lowPriority, requests: newResourceList("100m", "1Gi", ""), limits: newResourceList("100m", "1Gi", ""), rootFsUsed: "900Mi"}, 1321 {name: "below-requests", priority: defaultPriority, requests: newResourceList("100m", "100Mi", ""), limits: newResourceList("200m", "1Gi", ""), logsFsUsed: "50Mi"}, 1322 {name: "above-requests", priority: defaultPriority, requests: newResourceList("100m", "100Mi", ""), limits: newResourceList("200m", "1Gi", ""), rootFsUsed: "400Mi"}, 1323 {name: "high-priority-high-usage", priority: highPriority, requests: newResourceList("", "", ""), limits: newResourceList("", "", ""), perLocalVolumeUsed: "400Mi"}, 1324 {name: "low-priority-low-usage", priority: lowPriority, requests: newResourceList("", "", ""), limits: newResourceList("", "", ""), rootFsUsed: "100Mi"}, 1325 }, 1326 }, 1327 } 1328 1329 for name, tc := range testCases { 1330 t.Run(name, func(t *testing.T) { 1331 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.KubeletSeparateDiskGC, tc.kubeletSeparateDiskFeature) 1332 1333 podMaker := makePodWithDiskStats 1334 summaryStatsMaker := makeDiskStats 1335 podsToMake := tc.podToMakes 1336 pods := []*v1.Pod{} 1337 podStats := map[*v1.Pod]statsapi.PodStats{} 1338 for _, podToMake := range podsToMake { 1339 pod, podStat := podMaker(podToMake.name, podToMake.priority, podToMake.requests, podToMake.limits, podToMake.rootFsUsed, podToMake.logsFsUsed, podToMake.perLocalVolumeUsed, nil) 1340 pods = append(pods, pod) 1341 podStats[pod] = podStat 1342 } 1343 podToEvict := pods[0] 1344 activePodsFunc := func() []*v1.Pod { 1345 return pods 1346 } 1347 1348 fakeClock := testingclock.NewFakeClock(time.Now()) 1349 podKiller := &mockPodKiller{} 1350 diskInfoProvider := &mockDiskInfoProvider{dedicatedImageFs: tc.dedicatedImageFs} 1351 diskGC := &mockDiskGC{err: nil, readAndWriteSeparate: tc.writeableSeparateFromReadOnly} 1352 nodeRef := &v1.ObjectReference{Kind: "Node", Name: "test", UID: types.UID("test"), Namespace: ""} 1353 1354 config := Config{ 1355 MaxPodGracePeriodSeconds: 5, 1356 PressureTransitionPeriod: time.Minute * 5, 1357 Thresholds: tc.thresholdToMonitor, 1358 } 1359 1360 diskStatStart := diskStats{ 1361 rootFsAvailableBytes: tc.nodeFsStats, 1362 imageFsAvailableBytes: tc.imageFsStats, 1363 containerFsAvailableBytes: tc.containerFsStats, 1364 podStats: podStats, 1365 } 1366 diskStatConst := diskStatStart 1367 summaryProvider := &fakeSummaryProvider{result: summaryStatsMaker(diskStatStart)} 1368 manager := &managerImpl{ 1369 clock: fakeClock, 1370 killPodFunc: podKiller.killPodNow, 1371 imageGC: diskGC, 1372 containerGC: diskGC, 1373 config: config, 1374 recorder: &record.FakeRecorder{}, 1375 summaryProvider: summaryProvider, 1376 nodeRef: nodeRef, 1377 nodeConditionsLastObservedAt: nodeConditionsObservedAt{}, 1378 thresholdsFirstObservedAt: thresholdsObservedAt{}, 1379 } 1380 1381 // create a best effort pod to test admission 1382 podToAdmit, _ := podMaker("pod-to-admit", defaultPriority, newResourceList("", "", ""), newResourceList("", "", ""), "0Gi", "0Gi", "0Gi", nil) 1383 1384 // synchronize 1385 _, err := manager.synchronize(diskInfoProvider, activePodsFunc) 1386 1387 if err != nil { 1388 t.Fatalf("Manager expects no error but got %v", err) 1389 } 1390 1391 // we should not have disk pressure 1392 if manager.IsUnderDiskPressure() { 1393 t.Fatalf("Manager should not report disk pressure") 1394 } 1395 1396 // try to admit our pod (should succeed) 1397 if result := manager.Admit(&lifecycle.PodAdmitAttributes{Pod: podToAdmit}); !result.Admit { 1398 t.Fatalf("Admit pod: %v, expected: %v, actual: %v", podToAdmit, true, result.Admit) 1399 } 1400 1401 // induce soft threshold 1402 fakeClock.Step(1 * time.Minute) 1403 1404 if tc.inducePressureOnWhichFs == "nodefs" { 1405 diskStatStart.rootFsAvailableBytes = tc.softDiskPressure 1406 } else if tc.inducePressureOnWhichFs == "imagefs" { 1407 diskStatStart.imageFsAvailableBytes = tc.softDiskPressure 1408 } else if tc.inducePressureOnWhichFs == "containerfs" { 1409 diskStatStart.containerFsAvailableBytes = tc.softDiskPressure 1410 } 1411 summaryProvider.result = summaryStatsMaker(diskStatStart) 1412 _, err = manager.synchronize(diskInfoProvider, activePodsFunc) 1413 1414 if err != nil { 1415 t.Fatalf("Manager expects no error but got %v", err) 1416 } 1417 1418 // we should have disk pressure 1419 if !manager.IsUnderDiskPressure() { 1420 t.Fatalf("Manager should report disk pressure since soft threshold was met") 1421 } 1422 1423 // verify no pod was yet killed because there has not yet been enough time passed. 1424 if podKiller.pod != nil { 1425 t.Fatalf("Manager should not have killed a pod yet, but killed: %v", podKiller.pod.Name) 1426 } 1427 1428 // step forward in time pass the grace period 1429 fakeClock.Step(3 * time.Minute) 1430 summaryProvider.result = summaryStatsMaker(diskStatStart) 1431 _, err = manager.synchronize(diskInfoProvider, activePodsFunc) 1432 1433 if err != nil { 1434 t.Fatalf("Manager expects no error but got %v", err) 1435 } 1436 1437 // we should have disk pressure 1438 if !manager.IsUnderDiskPressure() { 1439 t.Fatalf("Manager should report disk pressure since soft threshold was met") 1440 } 1441 1442 // verify the right pod was killed with the right grace period. 1443 if podKiller.pod != podToEvict { 1444 t.Fatalf("Manager chose to kill pod: %v, but should have chosen %v", podKiller.pod.Name, podToEvict.Name) 1445 } 1446 if podKiller.gracePeriodOverride == nil { 1447 t.Fatalf("Manager chose to kill pod but should have had a grace period override.") 1448 } 1449 observedGracePeriod := *podKiller.gracePeriodOverride 1450 if observedGracePeriod != manager.config.MaxPodGracePeriodSeconds { 1451 t.Fatalf("Manager chose to kill pod with incorrect grace period. Expected: %d, actual: %d", manager.config.MaxPodGracePeriodSeconds, observedGracePeriod) 1452 } 1453 // reset state 1454 podKiller.pod = nil 1455 podKiller.gracePeriodOverride = nil 1456 1457 // remove disk pressure 1458 fakeClock.Step(20 * time.Minute) 1459 summaryProvider.result = summaryStatsMaker(diskStatConst) 1460 _, err = manager.synchronize(diskInfoProvider, activePodsFunc) 1461 1462 if err != nil { 1463 t.Fatalf("Manager expects no error but got %v", err) 1464 } 1465 1466 // we should not have disk pressure 1467 if manager.IsUnderDiskPressure() { 1468 t.Fatalf("Manager should not report disk pressure") 1469 } 1470 1471 // induce disk pressure! 1472 fakeClock.Step(1 * time.Minute) 1473 if tc.inducePressureOnWhichFs == "nodefs" { 1474 diskStatStart.rootFsAvailableBytes = tc.hardDiskPressure 1475 } else if tc.inducePressureOnWhichFs == "imagefs" { 1476 diskStatStart.imageFsAvailableBytes = tc.hardDiskPressure 1477 } else if tc.inducePressureOnWhichFs == "containerfs" { 1478 diskStatStart.containerFsAvailableBytes = tc.hardDiskPressure 1479 } 1480 summaryProvider.result = summaryStatsMaker(diskStatStart) 1481 _, err = manager.synchronize(diskInfoProvider, activePodsFunc) 1482 1483 if err != nil { 1484 t.Fatalf("Manager expects no error but got %v", err) 1485 } 1486 1487 // we should have disk pressure 1488 if !manager.IsUnderDiskPressure() { 1489 t.Fatalf("Manager should report disk pressure") 1490 } 1491 1492 // check the right pod was killed 1493 if podKiller.pod != podToEvict { 1494 t.Fatalf("Manager chose to kill pod: %v, but should have chosen %v", podKiller.pod.Name, podToEvict.Name) 1495 } 1496 observedGracePeriod = *podKiller.gracePeriodOverride 1497 if observedGracePeriod != int64(1) { 1498 t.Fatalf("Manager chose to kill pod with incorrect grace period. Expected: %d, actual: %d", 1, observedGracePeriod) 1499 } 1500 1501 // try to admit our pod (should fail) 1502 if result := manager.Admit(&lifecycle.PodAdmitAttributes{Pod: podToAdmit}); result.Admit { 1503 t.Fatalf("Admit pod: %v, expected: %v, actual: %v", podToAdmit, false, result.Admit) 1504 } 1505 1506 // reduce disk pressure 1507 fakeClock.Step(1 * time.Minute) 1508 1509 summaryProvider.result = summaryStatsMaker(diskStatConst) 1510 podKiller.pod = nil // reset state 1511 _, err = manager.synchronize(diskInfoProvider, activePodsFunc) 1512 1513 if err != nil { 1514 t.Fatalf("Manager should not have an error %v", err) 1515 } 1516 // we should have disk pressure (because transition period not yet met) 1517 if !manager.IsUnderDiskPressure() { 1518 t.Fatalf("Manager should report disk pressure") 1519 } 1520 1521 // no pod should have been killed 1522 if podKiller.pod != nil { 1523 t.Fatalf("Manager chose to kill pod: %v when no pod should have been killed", podKiller.pod.Name) 1524 } 1525 1526 // try to admit our pod (should fail) 1527 if result := manager.Admit(&lifecycle.PodAdmitAttributes{Pod: podToAdmit}); result.Admit { 1528 t.Fatalf("Admit pod: %v, expected: %v, actual: %v", podToAdmit, false, result.Admit) 1529 } 1530 1531 // move the clock past transition period to ensure that we stop reporting pressure 1532 fakeClock.Step(5 * time.Minute) 1533 summaryProvider.result = summaryStatsMaker(diskStatConst) 1534 podKiller.pod = nil // reset state 1535 _, err = manager.synchronize(diskInfoProvider, activePodsFunc) 1536 1537 if err != nil { 1538 t.Fatalf("Manager should not have an error %v", err) 1539 } 1540 1541 // we should not have disk pressure (because transition period met) 1542 if manager.IsUnderDiskPressure() { 1543 t.Fatalf("Manager should not report disk pressure") 1544 } 1545 1546 // no pod should have been killed 1547 if podKiller.pod != nil { 1548 t.Fatalf("Manager chose to kill pod: %v when no pod should have been killed", podKiller.pod.Name) 1549 } 1550 1551 // try to admit our pod (should succeed) 1552 if result := manager.Admit(&lifecycle.PodAdmitAttributes{Pod: podToAdmit}); !result.Admit { 1553 t.Fatalf("Admit pod: %v, expected: %v, actual: %v", podToAdmit, true, result.Admit) 1554 } 1555 }) 1556 } 1557 } 1558 1559 // TestMinReclaim verifies that min-reclaim works as desired. 1560 func TestMinReclaim(t *testing.T) { 1561 podMaker := makePodWithMemoryStats 1562 summaryStatsMaker := makeMemoryStats 1563 podsToMake := []podToMake{ 1564 {name: "guaranteed-low-priority-high-usage", priority: lowPriority, requests: newResourceList("100m", "1Gi", ""), limits: newResourceList("100m", "1Gi", ""), memoryWorkingSet: "900Mi"}, 1565 {name: "burstable-below-requests", priority: defaultPriority, requests: newResourceList("100m", "100Mi", ""), limits: newResourceList("200m", "1Gi", ""), memoryWorkingSet: "50Mi"}, 1566 {name: "burstable-above-requests", priority: defaultPriority, requests: newResourceList("100m", "100Mi", ""), limits: newResourceList("200m", "1Gi", ""), memoryWorkingSet: "400Mi"}, 1567 {name: "best-effort-high-priority-high-usage", priority: highPriority, requests: newResourceList("", "", ""), limits: newResourceList("", "", ""), memoryWorkingSet: "400Mi"}, 1568 {name: "best-effort-low-priority-low-usage", priority: lowPriority, requests: newResourceList("", "", ""), limits: newResourceList("", "", ""), memoryWorkingSet: "100Mi"}, 1569 } 1570 pods := []*v1.Pod{} 1571 podStats := map[*v1.Pod]statsapi.PodStats{} 1572 for _, podToMake := range podsToMake { 1573 pod, podStat := podMaker(podToMake.name, podToMake.priority, podToMake.requests, podToMake.limits, podToMake.memoryWorkingSet) 1574 pods = append(pods, pod) 1575 podStats[pod] = podStat 1576 } 1577 podToEvict := pods[4] 1578 activePodsFunc := func() []*v1.Pod { 1579 return pods 1580 } 1581 1582 fakeClock := testingclock.NewFakeClock(time.Now()) 1583 podKiller := &mockPodKiller{} 1584 diskInfoProvider := &mockDiskInfoProvider{dedicatedImageFs: ptr.To(false)} 1585 diskGC := &mockDiskGC{err: nil} 1586 nodeRef := &v1.ObjectReference{Kind: "Node", Name: "test", UID: types.UID("test"), Namespace: ""} 1587 1588 config := Config{ 1589 MaxPodGracePeriodSeconds: 5, 1590 PressureTransitionPeriod: time.Minute * 5, 1591 Thresholds: []evictionapi.Threshold{ 1592 { 1593 Signal: evictionapi.SignalMemoryAvailable, 1594 Operator: evictionapi.OpLessThan, 1595 Value: evictionapi.ThresholdValue{ 1596 Quantity: quantityMustParse("1Gi"), 1597 }, 1598 MinReclaim: &evictionapi.ThresholdValue{ 1599 Quantity: quantityMustParse("500Mi"), 1600 }, 1601 }, 1602 }, 1603 } 1604 summaryProvider := &fakeSummaryProvider{result: summaryStatsMaker("2Gi", podStats)} 1605 manager := &managerImpl{ 1606 clock: fakeClock, 1607 killPodFunc: podKiller.killPodNow, 1608 imageGC: diskGC, 1609 containerGC: diskGC, 1610 config: config, 1611 recorder: &record.FakeRecorder{}, 1612 summaryProvider: summaryProvider, 1613 nodeRef: nodeRef, 1614 nodeConditionsLastObservedAt: nodeConditionsObservedAt{}, 1615 thresholdsFirstObservedAt: thresholdsObservedAt{}, 1616 } 1617 1618 // synchronize 1619 _, err := manager.synchronize(diskInfoProvider, activePodsFunc) 1620 if err != nil { 1621 t.Errorf("Manager should not report any errors") 1622 } 1623 // we should not have memory pressure 1624 if manager.IsUnderMemoryPressure() { 1625 t.Errorf("Manager should not report memory pressure") 1626 } 1627 1628 // induce memory pressure! 1629 fakeClock.Step(1 * time.Minute) 1630 summaryProvider.result = summaryStatsMaker("500Mi", podStats) 1631 _, err = manager.synchronize(diskInfoProvider, activePodsFunc) 1632 1633 if err != nil { 1634 t.Fatalf("Manager should not have an error %v", err) 1635 } 1636 1637 // we should have memory pressure 1638 if !manager.IsUnderMemoryPressure() { 1639 t.Errorf("Manager should report memory pressure") 1640 } 1641 1642 // check the right pod was killed 1643 if podKiller.pod != podToEvict { 1644 t.Errorf("Manager chose to kill pod: %v, but should have chosen %v", podKiller.pod.Name, podToEvict.Name) 1645 } 1646 observedGracePeriod := *podKiller.gracePeriodOverride 1647 if observedGracePeriod != int64(1) { 1648 t.Errorf("Manager chose to kill pod with incorrect grace period. Expected: %d, actual: %d", 1, observedGracePeriod) 1649 } 1650 1651 // reduce memory pressure, but not below the min-reclaim amount 1652 fakeClock.Step(1 * time.Minute) 1653 summaryProvider.result = summaryStatsMaker("1.2Gi", podStats) 1654 podKiller.pod = nil // reset state 1655 _, err = manager.synchronize(diskInfoProvider, activePodsFunc) 1656 1657 if err != nil { 1658 t.Fatalf("Manager should not have an error %v", err) 1659 } 1660 1661 // we should have memory pressure (because transition period not yet met) 1662 if !manager.IsUnderMemoryPressure() { 1663 t.Errorf("Manager should report memory pressure") 1664 } 1665 1666 // check the right pod was killed 1667 if podKiller.pod != podToEvict { 1668 t.Errorf("Manager chose to kill pod: %v, but should have chosen %v", podKiller.pod.Name, podToEvict.Name) 1669 } 1670 observedGracePeriod = *podKiller.gracePeriodOverride 1671 if observedGracePeriod != int64(1) { 1672 t.Errorf("Manager chose to kill pod with incorrect grace period. Expected: %d, actual: %d", 1, observedGracePeriod) 1673 } 1674 1675 // reduce memory pressure and ensure the min-reclaim amount 1676 fakeClock.Step(1 * time.Minute) 1677 summaryProvider.result = summaryStatsMaker("2Gi", podStats) 1678 podKiller.pod = nil // reset state 1679 _, err = manager.synchronize(diskInfoProvider, activePodsFunc) 1680 1681 if err != nil { 1682 t.Fatalf("Manager should not have an error %v", err) 1683 } 1684 1685 // we should have memory pressure (because transition period not yet met) 1686 if !manager.IsUnderMemoryPressure() { 1687 t.Errorf("Manager should report memory pressure") 1688 } 1689 1690 // no pod should have been killed 1691 if podKiller.pod != nil { 1692 t.Errorf("Manager chose to kill pod: %v when no pod should have been killed", podKiller.pod.Name) 1693 } 1694 1695 // move the clock past transition period to ensure that we stop reporting pressure 1696 fakeClock.Step(5 * time.Minute) 1697 summaryProvider.result = summaryStatsMaker("2Gi", podStats) 1698 podKiller.pod = nil // reset state 1699 _, err = manager.synchronize(diskInfoProvider, activePodsFunc) 1700 1701 if err != nil { 1702 t.Fatalf("Manager should not have an error %v", err) 1703 } 1704 1705 // we should not have memory pressure (because transition period met) 1706 if manager.IsUnderMemoryPressure() { 1707 t.Errorf("Manager should not report memory pressure") 1708 } 1709 1710 // no pod should have been killed 1711 if podKiller.pod != nil { 1712 t.Errorf("Manager chose to kill pod: %v when no pod should have been killed", podKiller.pod.Name) 1713 } 1714 } 1715 1716 func TestNodeReclaimFuncs(t *testing.T) { 1717 testCases := map[string]struct { 1718 nodeFsStats string 1719 imageFsStats string 1720 containerFsStats string 1721 kubeletSeparateDiskFeature bool 1722 writeableSeparateFromReadOnly bool 1723 expectContainerGcCall bool 1724 expectImageGcCall bool 1725 thresholdToMonitor evictionapi.Threshold 1726 podToMakes []podToMake 1727 dedicatedImageFs *bool 1728 expectErr string 1729 inducePressureOnWhichFs string 1730 softDiskPressure string 1731 hardDiskPressure string 1732 }{ 1733 "eviction due to disk pressure; no image fs": { 1734 dedicatedImageFs: ptr.To(false), 1735 nodeFsStats: "16Gi", 1736 imageFsStats: "16Gi", 1737 containerFsStats: "16Gi", 1738 inducePressureOnWhichFs: "nodefs", 1739 softDiskPressure: "1.5Gi", 1740 hardDiskPressure: "750Mi", 1741 expectContainerGcCall: true, 1742 expectImageGcCall: true, 1743 thresholdToMonitor: evictionapi.Threshold{ 1744 Signal: evictionapi.SignalNodeFsAvailable, 1745 Operator: evictionapi.OpLessThan, 1746 Value: evictionapi.ThresholdValue{ 1747 Quantity: quantityMustParse("1Gi"), 1748 }, 1749 MinReclaim: &evictionapi.ThresholdValue{ 1750 Quantity: quantityMustParse("500Mi"), 1751 }, 1752 }, 1753 podToMakes: []podToMake{ 1754 {name: "low-priority-high-usage", priority: lowPriority, requests: newResourceList("100m", "1Gi", ""), limits: newResourceList("100m", "1Gi", ""), rootFsUsed: "900Mi"}, 1755 {name: "below-requests", priority: defaultPriority, requests: newResourceList("100m", "100Mi", ""), limits: newResourceList("200m", "1Gi", ""), logsFsUsed: "50Mi"}, 1756 {name: "above-requests", priority: defaultPriority, requests: newResourceList("100m", "100Mi", ""), limits: newResourceList("200m", "1Gi", ""), rootFsUsed: "400Mi"}, 1757 {name: "high-priority-high-usage", priority: highPriority, requests: newResourceList("", "", ""), limits: newResourceList("", "", ""), perLocalVolumeUsed: "400Mi"}, 1758 {name: "low-priority-low-usage", priority: lowPriority, requests: newResourceList("", "", ""), limits: newResourceList("", "", ""), rootFsUsed: "100Mi"}, 1759 }, 1760 }, 1761 "eviction due to image disk pressure; image fs": { 1762 dedicatedImageFs: ptr.To(true), 1763 nodeFsStats: "16Gi", 1764 imageFsStats: "16Gi", 1765 containerFsStats: "16Gi", 1766 softDiskPressure: "1.5Gi", 1767 hardDiskPressure: "750Mi", 1768 inducePressureOnWhichFs: "imagefs", 1769 expectContainerGcCall: true, 1770 expectImageGcCall: true, 1771 thresholdToMonitor: evictionapi.Threshold{ 1772 Signal: evictionapi.SignalImageFsAvailable, 1773 Operator: evictionapi.OpLessThan, 1774 Value: evictionapi.ThresholdValue{ 1775 Quantity: quantityMustParse("1Gi"), 1776 }, 1777 MinReclaim: &evictionapi.ThresholdValue{ 1778 Quantity: quantityMustParse("500Mi"), 1779 }, 1780 }, 1781 podToMakes: []podToMake{ 1782 {name: "low-priority-high-usage", priority: lowPriority, requests: newResourceList("100m", "1Gi", ""), limits: newResourceList("100m", "1Gi", ""), rootFsUsed: "900Mi"}, 1783 {name: "below-requests", priority: defaultPriority, requests: newResourceList("100m", "100Mi", ""), limits: newResourceList("200m", "1Gi", ""), logsFsUsed: "50Mi"}, 1784 {name: "above-requests", priority: defaultPriority, requests: newResourceList("100m", "100Mi", ""), limits: newResourceList("200m", "1Gi", ""), rootFsUsed: "400Mi"}, 1785 {name: "high-priority-high-usage", priority: highPriority, requests: newResourceList("", "", ""), limits: newResourceList("", "", ""), perLocalVolumeUsed: "400Mi"}, 1786 {name: "low-priority-low-usage", priority: lowPriority, requests: newResourceList("", "", ""), limits: newResourceList("", "", ""), rootFsUsed: "100Mi"}, 1787 }, 1788 }, 1789 "eviction due to container disk pressure; container fs": { 1790 dedicatedImageFs: ptr.To(true), 1791 kubeletSeparateDiskFeature: true, 1792 writeableSeparateFromReadOnly: true, 1793 nodeFsStats: "16Gi", 1794 imageFsStats: "16Gi", 1795 containerFsStats: "16Gi", 1796 softDiskPressure: "1.5Gi", 1797 hardDiskPressure: "750Mi", 1798 inducePressureOnWhichFs: "nodefs", 1799 expectContainerGcCall: true, 1800 expectImageGcCall: false, 1801 thresholdToMonitor: evictionapi.Threshold{ 1802 Signal: evictionapi.SignalNodeFsAvailable, 1803 Operator: evictionapi.OpLessThan, 1804 Value: evictionapi.ThresholdValue{ 1805 Quantity: quantityMustParse("1Gi"), 1806 }, 1807 MinReclaim: &evictionapi.ThresholdValue{ 1808 Quantity: quantityMustParse("500Mi"), 1809 }, 1810 }, 1811 podToMakes: []podToMake{ 1812 {name: "low-priority-high-usage", priority: lowPriority, requests: newResourceList("100m", "1Gi", ""), limits: newResourceList("100m", "1Gi", ""), rootFsUsed: "900Mi"}, 1813 {name: "below-requests", priority: defaultPriority, requests: newResourceList("100m", "100Mi", ""), limits: newResourceList("200m", "1Gi", ""), logsFsUsed: "50Mi"}, 1814 {name: "above-requests", priority: defaultPriority, requests: newResourceList("100m", "100Mi", ""), limits: newResourceList("200m", "1Gi", ""), rootFsUsed: "400Mi"}, 1815 {name: "high-priority-high-usage", priority: highPriority, requests: newResourceList("", "", ""), limits: newResourceList("", "", ""), perLocalVolumeUsed: "400Mi"}, 1816 {name: "low-priority-low-usage", priority: lowPriority, requests: newResourceList("", "", ""), limits: newResourceList("", "", ""), rootFsUsed: "100Mi"}, 1817 }, 1818 }, 1819 "eviction due to image disk pressure; container fs": { 1820 dedicatedImageFs: ptr.To(true), 1821 kubeletSeparateDiskFeature: true, 1822 writeableSeparateFromReadOnly: true, 1823 nodeFsStats: "16Gi", 1824 imageFsStats: "16Gi", 1825 containerFsStats: "16Gi", 1826 softDiskPressure: "1.5Gi", 1827 hardDiskPressure: "750Mi", 1828 inducePressureOnWhichFs: "imagefs", 1829 expectContainerGcCall: false, 1830 expectImageGcCall: true, 1831 thresholdToMonitor: evictionapi.Threshold{ 1832 Signal: evictionapi.SignalImageFsAvailable, 1833 Operator: evictionapi.OpLessThan, 1834 Value: evictionapi.ThresholdValue{ 1835 Quantity: quantityMustParse("1Gi"), 1836 }, 1837 MinReclaim: &evictionapi.ThresholdValue{ 1838 Quantity: quantityMustParse("500Mi"), 1839 }, 1840 }, 1841 podToMakes: []podToMake{ 1842 {name: "low-priority-high-usage", priority: lowPriority, requests: newResourceList("100m", "1Gi", ""), limits: newResourceList("100m", "1Gi", ""), rootFsUsed: "900Mi"}, 1843 {name: "below-requests", priority: defaultPriority, requests: newResourceList("100m", "100Mi", ""), limits: newResourceList("200m", "1Gi", ""), logsFsUsed: "50Mi"}, 1844 {name: "above-requests", priority: defaultPriority, requests: newResourceList("100m", "100Mi", ""), limits: newResourceList("200m", "1Gi", ""), rootFsUsed: "400Mi"}, 1845 {name: "high-priority-high-usage", priority: highPriority, requests: newResourceList("", "", ""), limits: newResourceList("", "", ""), perLocalVolumeUsed: "400Mi"}, 1846 {name: "low-priority-low-usage", priority: lowPriority, requests: newResourceList("", "", ""), limits: newResourceList("", "", ""), rootFsUsed: "100Mi"}, 1847 }, 1848 }, 1849 } 1850 1851 for name, tc := range testCases { 1852 t.Run(name, func(t *testing.T) { 1853 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.KubeletSeparateDiskGC, tc.kubeletSeparateDiskFeature) 1854 1855 podMaker := makePodWithDiskStats 1856 summaryStatsMaker := makeDiskStats 1857 podsToMake := tc.podToMakes 1858 pods := []*v1.Pod{} 1859 podStats := map[*v1.Pod]statsapi.PodStats{} 1860 for _, podToMake := range podsToMake { 1861 pod, podStat := podMaker(podToMake.name, podToMake.priority, podToMake.requests, podToMake.limits, podToMake.rootFsUsed, podToMake.logsFsUsed, podToMake.perLocalVolumeUsed, nil) 1862 pods = append(pods, pod) 1863 podStats[pod] = podStat 1864 } 1865 podToEvict := pods[0] 1866 activePodsFunc := func() []*v1.Pod { 1867 return pods 1868 } 1869 1870 fakeClock := testingclock.NewFakeClock(time.Now()) 1871 podKiller := &mockPodKiller{} 1872 diskInfoProvider := &mockDiskInfoProvider{dedicatedImageFs: tc.dedicatedImageFs} 1873 nodeRef := &v1.ObjectReference{Kind: "Node", Name: "test", UID: types.UID("test"), Namespace: ""} 1874 1875 config := Config{ 1876 MaxPodGracePeriodSeconds: 5, 1877 PressureTransitionPeriod: time.Minute * 5, 1878 Thresholds: []evictionapi.Threshold{tc.thresholdToMonitor}, 1879 } 1880 diskStatStart := diskStats{ 1881 rootFsAvailableBytes: tc.nodeFsStats, 1882 imageFsAvailableBytes: tc.imageFsStats, 1883 containerFsAvailableBytes: tc.containerFsStats, 1884 podStats: podStats, 1885 } 1886 // This is a constant that we use to test that disk pressure is over. Don't change! 1887 diskStatConst := diskStatStart 1888 summaryProvider := &fakeSummaryProvider{result: summaryStatsMaker(diskStatStart)} 1889 diskGC := &mockDiskGC{fakeSummaryProvider: summaryProvider, err: nil, readAndWriteSeparate: tc.writeableSeparateFromReadOnly} 1890 manager := &managerImpl{ 1891 clock: fakeClock, 1892 killPodFunc: podKiller.killPodNow, 1893 imageGC: diskGC, 1894 containerGC: diskGC, 1895 config: config, 1896 recorder: &record.FakeRecorder{}, 1897 summaryProvider: summaryProvider, 1898 nodeRef: nodeRef, 1899 nodeConditionsLastObservedAt: nodeConditionsObservedAt{}, 1900 thresholdsFirstObservedAt: thresholdsObservedAt{}, 1901 } 1902 1903 // synchronize 1904 _, err := manager.synchronize(diskInfoProvider, activePodsFunc) 1905 1906 if err != nil { 1907 t.Fatalf("Manager should not have an error %v", err) 1908 } 1909 1910 // we should not have disk pressure 1911 if manager.IsUnderDiskPressure() { 1912 t.Errorf("Manager should not report disk pressure") 1913 } 1914 1915 // induce hard threshold 1916 fakeClock.Step(1 * time.Minute) 1917 1918 setDiskStatsBasedOnFs := func(whichFs string, diskPressure string, diskStat diskStats) diskStats { 1919 if tc.inducePressureOnWhichFs == "nodefs" { 1920 diskStat.rootFsAvailableBytes = diskPressure 1921 } else if tc.inducePressureOnWhichFs == "imagefs" { 1922 diskStat.imageFsAvailableBytes = diskPressure 1923 } else if tc.inducePressureOnWhichFs == "containerfs" { 1924 diskStat.containerFsAvailableBytes = diskPressure 1925 } 1926 return diskStat 1927 } 1928 newDiskAfterHardEviction := setDiskStatsBasedOnFs(tc.inducePressureOnWhichFs, tc.hardDiskPressure, diskStatStart) 1929 summaryProvider.result = summaryStatsMaker(newDiskAfterHardEviction) 1930 // make GC successfully return disk usage to previous levels 1931 diskGC.summaryAfterGC = summaryStatsMaker(diskStatConst) 1932 _, err = manager.synchronize(diskInfoProvider, activePodsFunc) 1933 1934 if err != nil { 1935 t.Fatalf("Manager should not have an error %v", err) 1936 } 1937 1938 // we should have disk pressure 1939 if !manager.IsUnderDiskPressure() { 1940 t.Fatalf("Manager should report disk pressure since soft threshold was met") 1941 } 1942 1943 // verify image, container or both gc were called. 1944 // split filesystem can have container gc called without image. 1945 // same filesystem should have both. 1946 if diskGC.imageGCInvoked != tc.expectImageGcCall && diskGC.containerGCInvoked != tc.expectContainerGcCall { 1947 t.Fatalf("Manager should have invoked image gc") 1948 } 1949 1950 // verify no pod was killed because image gc was sufficient 1951 if podKiller.pod != nil { 1952 t.Fatalf("Manager should not have killed a pod, but killed: %v", podKiller.pod.Name) 1953 } 1954 1955 // reset state 1956 diskGC.imageGCInvoked = false 1957 diskGC.containerGCInvoked = false 1958 1959 // remove disk pressure 1960 fakeClock.Step(20 * time.Minute) 1961 summaryProvider.result = summaryStatsMaker(diskStatConst) 1962 _, err = manager.synchronize(diskInfoProvider, activePodsFunc) 1963 1964 if err != nil { 1965 t.Fatalf("Manager should not have an error %v", err) 1966 } 1967 1968 // we should not have disk pressure 1969 if manager.IsUnderDiskPressure() { 1970 t.Fatalf("Manager should not report disk pressure") 1971 } 1972 1973 // synchronize 1974 _, err = manager.synchronize(diskInfoProvider, activePodsFunc) 1975 1976 if err != nil { 1977 t.Fatalf("Manager should not have an error %v", err) 1978 } 1979 1980 // we should not have disk pressure 1981 if manager.IsUnderDiskPressure() { 1982 t.Fatalf("Manager should not report disk pressure") 1983 } 1984 1985 // induce hard threshold 1986 fakeClock.Step(1 * time.Minute) 1987 newDiskAfterHardEviction = setDiskStatsBasedOnFs(tc.inducePressureOnWhichFs, tc.hardDiskPressure, diskStatStart) 1988 summaryProvider.result = summaryStatsMaker(newDiskAfterHardEviction) 1989 // make GC return disk usage bellow the threshold, but not satisfying minReclaim 1990 gcBelowThreshold := setDiskStatsBasedOnFs(tc.inducePressureOnWhichFs, "1.1G", newDiskAfterHardEviction) 1991 diskGC.summaryAfterGC = summaryStatsMaker(gcBelowThreshold) 1992 _, err = manager.synchronize(diskInfoProvider, activePodsFunc) 1993 1994 if err != nil { 1995 t.Fatalf("Manager should not have an error %v", err) 1996 } 1997 1998 // we should have disk pressure 1999 if !manager.IsUnderDiskPressure() { 2000 t.Fatalf("Manager should report disk pressure since soft threshold was met") 2001 } 2002 2003 // verify image, container or both gc were called. 2004 // split filesystem can have container gc called without image. 2005 // same filesystem should have both. 2006 if diskGC.imageGCInvoked != tc.expectImageGcCall && diskGC.containerGCInvoked != tc.expectContainerGcCall { 2007 t.Fatalf("Manager should have invoked image gc") 2008 } 2009 2010 // verify a pod was killed because image gc was not enough to satisfy minReclaim 2011 if podKiller.pod == nil { 2012 t.Fatalf("Manager should have killed a pod, but didn't") 2013 } 2014 2015 // reset state 2016 diskGC.imageGCInvoked = false 2017 diskGC.containerGCInvoked = false 2018 podKiller.pod = nil 2019 2020 // remove disk pressure 2021 fakeClock.Step(20 * time.Minute) 2022 summaryProvider.result = summaryStatsMaker(diskStatConst) 2023 _, err = manager.synchronize(diskInfoProvider, activePodsFunc) 2024 2025 if err != nil { 2026 t.Fatalf("Manager should not have an error %v", err) 2027 } 2028 2029 // we should not have disk pressure 2030 if manager.IsUnderDiskPressure() { 2031 t.Fatalf("Manager should not report disk pressure") 2032 } 2033 2034 // induce disk pressure! 2035 fakeClock.Step(1 * time.Minute) 2036 softDiskPressure := setDiskStatsBasedOnFs(tc.inducePressureOnWhichFs, tc.hardDiskPressure, diskStatStart) 2037 summaryProvider.result = summaryStatsMaker(softDiskPressure) 2038 // Don't reclaim any disk 2039 diskGC.summaryAfterGC = summaryStatsMaker(softDiskPressure) 2040 _, err = manager.synchronize(diskInfoProvider, activePodsFunc) 2041 2042 if err != nil { 2043 t.Fatalf("Manager should not have an error %v", err) 2044 } 2045 2046 // we should have disk pressure 2047 if !manager.IsUnderDiskPressure() { 2048 t.Fatalf("Manager should report disk pressure") 2049 } 2050 2051 // verify image, container or both gc were called. 2052 // split filesystem can have container gc called without image. 2053 // same filesystem should have both. 2054 if diskGC.imageGCInvoked != tc.expectImageGcCall && diskGC.containerGCInvoked != tc.expectContainerGcCall { 2055 t.Fatalf("Manager should have invoked image gc") 2056 } 2057 2058 // check the right pod was killed 2059 if podKiller.pod != podToEvict { 2060 t.Fatalf("Manager chose to kill pod: %v, but should have chosen %v", podKiller.pod.Name, podToEvict.Name) 2061 } 2062 observedGracePeriod := *podKiller.gracePeriodOverride 2063 if observedGracePeriod != int64(1) { 2064 t.Fatalf("Manager chose to kill pod with incorrect grace period. Expected: %d, actual: %d", 1, observedGracePeriod) 2065 } 2066 2067 // reduce disk pressure 2068 fakeClock.Step(1 * time.Minute) 2069 summaryProvider.result = summaryStatsMaker(diskStatConst) 2070 diskGC.imageGCInvoked = false // reset state 2071 diskGC.containerGCInvoked = false // reset state 2072 podKiller.pod = nil // reset state 2073 _, err = manager.synchronize(diskInfoProvider, activePodsFunc) 2074 2075 if err != nil { 2076 t.Fatalf("Manager should not have an error %v", err) 2077 } 2078 2079 // we should have disk pressure (because transition period not yet met) 2080 if !manager.IsUnderDiskPressure() { 2081 t.Fatalf("Manager should report disk pressure") 2082 } 2083 2084 if diskGC.imageGCInvoked || diskGC.containerGCInvoked { 2085 t.Errorf("Manager chose to perform image gc when it was not needed") 2086 } 2087 2088 // no pod should have been killed 2089 if podKiller.pod != nil { 2090 t.Fatalf("Manager chose to kill pod: %v when no pod should have been killed", podKiller.pod.Name) 2091 } 2092 2093 // move the clock past transition period to ensure that we stop reporting pressure 2094 fakeClock.Step(5 * time.Minute) 2095 summaryProvider.result = summaryStatsMaker(diskStatConst) 2096 diskGC.imageGCInvoked = false // reset state 2097 diskGC.containerGCInvoked = false // reset state 2098 podKiller.pod = nil // reset state 2099 _, err = manager.synchronize(diskInfoProvider, activePodsFunc) 2100 2101 if err != nil { 2102 t.Fatalf("Manager should not have an error %v", err) 2103 } 2104 2105 // we should not have disk pressure (because transition period met) 2106 if manager.IsUnderDiskPressure() { 2107 t.Fatalf("Manager should not report disk pressure") 2108 } 2109 2110 if diskGC.imageGCInvoked || diskGC.containerGCInvoked { 2111 t.Errorf("Manager chose to perform image gc when it was not needed") 2112 } 2113 2114 // no pod should have been killed 2115 if podKiller.pod != nil { 2116 t.Fatalf("Manager chose to kill pod: %v when no pod should have been killed", podKiller.pod.Name) 2117 } 2118 }) 2119 } 2120 } 2121 2122 func TestInodePressureFsInodes(t *testing.T) { 2123 podMaker := func(name string, priority int32, requests v1.ResourceList, limits v1.ResourceList, rootInodes, logInodes, volumeInodes string) (*v1.Pod, statsapi.PodStats) { 2124 pod := newPod(name, priority, []v1.Container{ 2125 newContainer(name, requests, limits), 2126 }, nil) 2127 podStats := newPodInodeStats(pod, parseQuantity(rootInodes), parseQuantity(logInodes), parseQuantity(volumeInodes)) 2128 return pod, podStats 2129 } 2130 summaryStatsMaker := func(rootFsInodesFree, rootFsInodes, imageFsInodesFree, imageFsInodes, containerFsInodesFree, containerFsInodes string, podStats map[*v1.Pod]statsapi.PodStats) *statsapi.Summary { 2131 rootFsInodesFreeVal := resource.MustParse(rootFsInodesFree) 2132 internalRootFsInodesFree := uint64(rootFsInodesFreeVal.Value()) 2133 rootFsInodesVal := resource.MustParse(rootFsInodes) 2134 internalRootFsInodes := uint64(rootFsInodesVal.Value()) 2135 2136 imageFsInodesFreeVal := resource.MustParse(imageFsInodesFree) 2137 internalImageFsInodesFree := uint64(imageFsInodesFreeVal.Value()) 2138 imageFsInodesVal := resource.MustParse(imageFsInodes) 2139 internalImageFsInodes := uint64(imageFsInodesVal.Value()) 2140 2141 containerFsInodesFreeVal := resource.MustParse(containerFsInodesFree) 2142 internalContainerFsInodesFree := uint64(containerFsInodesFreeVal.Value()) 2143 containerFsInodesVal := resource.MustParse(containerFsInodes) 2144 internalContainerFsInodes := uint64(containerFsInodesVal.Value()) 2145 2146 result := &statsapi.Summary{ 2147 Node: statsapi.NodeStats{ 2148 Fs: &statsapi.FsStats{ 2149 InodesFree: &internalRootFsInodesFree, 2150 Inodes: &internalRootFsInodes, 2151 }, 2152 Runtime: &statsapi.RuntimeStats{ 2153 ImageFs: &statsapi.FsStats{ 2154 InodesFree: &internalImageFsInodesFree, 2155 Inodes: &internalImageFsInodes, 2156 }, 2157 ContainerFs: &statsapi.FsStats{ 2158 InodesFree: &internalContainerFsInodesFree, 2159 Inodes: &internalContainerFsInodes, 2160 }, 2161 }, 2162 }, 2163 Pods: []statsapi.PodStats{}, 2164 } 2165 for _, podStat := range podStats { 2166 result.Pods = append(result.Pods, podStat) 2167 } 2168 return result 2169 } 2170 2171 setINodesFreeBasedOnFs := func(whichFs string, inodesFree string, diskStat *statsapi.Summary) *statsapi.Summary { 2172 inodesFreeVal := resource.MustParse(inodesFree) 2173 internalFsInodesFree := uint64(inodesFreeVal.Value()) 2174 2175 if whichFs == "nodefs" { 2176 diskStat.Node.Fs.InodesFree = &internalFsInodesFree 2177 } else if whichFs == "imagefs" { 2178 diskStat.Node.Runtime.ImageFs.InodesFree = &internalFsInodesFree 2179 } else if whichFs == "containerfs" { 2180 diskStat.Node.Runtime.ContainerFs.InodesFree = &internalFsInodesFree 2181 } 2182 return diskStat 2183 } 2184 2185 testCases := map[string]struct { 2186 nodeFsInodesFree string 2187 nodeFsInodes string 2188 imageFsInodesFree string 2189 imageFsInodes string 2190 containerFsInodesFree string 2191 containerFsInodes string 2192 kubeletSeparateDiskFeature bool 2193 writeableSeparateFromReadOnly bool 2194 thresholdToMonitor []evictionapi.Threshold 2195 podToMakes []podToMake 2196 dedicatedImageFs *bool 2197 expectErr string 2198 inducePressureOnWhichFs string 2199 softINodePressure string 2200 hardINodePressure string 2201 }{ 2202 "eviction due to disk pressure; no image fs": { 2203 dedicatedImageFs: ptr.To(false), 2204 nodeFsInodesFree: "3Mi", 2205 nodeFsInodes: "4Mi", 2206 imageFsInodesFree: "3Mi", 2207 imageFsInodes: "4Mi", 2208 containerFsInodesFree: "3Mi", 2209 containerFsInodes: "4Mi", 2210 inducePressureOnWhichFs: "nodefs", 2211 softINodePressure: "1.5Mi", 2212 hardINodePressure: "0.5Mi", 2213 thresholdToMonitor: []evictionapi.Threshold{ 2214 { 2215 Signal: evictionapi.SignalNodeFsInodesFree, 2216 Operator: evictionapi.OpLessThan, 2217 Value: evictionapi.ThresholdValue{ 2218 Quantity: quantityMustParse("1Mi"), 2219 }, 2220 }, 2221 { 2222 Signal: evictionapi.SignalNodeFsInodesFree, 2223 Operator: evictionapi.OpLessThan, 2224 Value: evictionapi.ThresholdValue{ 2225 Quantity: quantityMustParse("2Mi"), 2226 }, 2227 GracePeriod: time.Minute * 2, 2228 }, 2229 }, 2230 podToMakes: []podToMake{ 2231 {name: "low-priority-high-usage", priority: lowPriority, requests: newResourceList("100m", "1Gi", ""), limits: newResourceList("100m", "1Gi", ""), rootFsInodesUsed: "900Mi"}, 2232 {name: "below-requests", priority: defaultPriority, requests: newResourceList("100m", "100Mi", ""), limits: newResourceList("200m", "1Gi", ""), rootFsInodesUsed: "50Mi"}, 2233 {name: "above-requests", priority: defaultPriority, requests: newResourceList("100m", "100Mi", ""), limits: newResourceList("200m", "1Gi", ""), rootFsInodesUsed: "400Mi"}, 2234 {name: "high-priority-high-usage", priority: highPriority, requests: newResourceList("", "", ""), limits: newResourceList("", "", ""), rootFsInodesUsed: "400Mi"}, 2235 {name: "low-priority-low-usage", priority: lowPriority, requests: newResourceList("", "", ""), limits: newResourceList("", "", ""), rootFsInodesUsed: "100Mi"}, 2236 }, 2237 }, 2238 "eviction due to image disk pressure; image fs": { 2239 dedicatedImageFs: ptr.To(true), 2240 nodeFsInodesFree: "3Mi", 2241 nodeFsInodes: "4Mi", 2242 imageFsInodesFree: "3Mi", 2243 imageFsInodes: "4Mi", 2244 containerFsInodesFree: "3Mi", 2245 containerFsInodes: "4Mi", 2246 softINodePressure: "1.5Mi", 2247 hardINodePressure: "0.5Mi", 2248 inducePressureOnWhichFs: "imagefs", 2249 thresholdToMonitor: []evictionapi.Threshold{ 2250 { 2251 Signal: evictionapi.SignalImageFsInodesFree, 2252 Operator: evictionapi.OpLessThan, 2253 Value: evictionapi.ThresholdValue{ 2254 Quantity: quantityMustParse("1Mi"), 2255 }, 2256 }, 2257 { 2258 Signal: evictionapi.SignalImageFsInodesFree, 2259 Operator: evictionapi.OpLessThan, 2260 Value: evictionapi.ThresholdValue{ 2261 Quantity: quantityMustParse("2Mi"), 2262 }, 2263 GracePeriod: time.Minute * 2, 2264 }, 2265 }, 2266 podToMakes: []podToMake{ 2267 {name: "low-priority-high-usage", priority: lowPriority, requests: newResourceList("100m", "1Gi", ""), limits: newResourceList("100m", "1Gi", ""), rootFsInodesUsed: "900Mi"}, 2268 {name: "below-requests", priority: defaultPriority, requests: newResourceList("100m", "100Mi", ""), limits: newResourceList("200m", "1Gi", ""), rootFsInodesUsed: "50Mi"}, 2269 {name: "above-requests", priority: defaultPriority, requests: newResourceList("100m", "100Mi", ""), limits: newResourceList("200m", "1Gi", ""), rootFsInodesUsed: "400Mi"}, 2270 {name: "high-priority-high-usage", priority: highPriority, requests: newResourceList("", "", ""), limits: newResourceList("", "", ""), rootFsInodesUsed: "400Mi"}, 2271 {name: "low-priority-low-usage", priority: lowPriority, requests: newResourceList("", "", ""), limits: newResourceList("", "", ""), rootFsInodesUsed: "100Mi"}, 2272 }, 2273 }, 2274 "eviction due to container disk pressure; container fs": { 2275 dedicatedImageFs: ptr.To(true), 2276 kubeletSeparateDiskFeature: true, 2277 writeableSeparateFromReadOnly: true, 2278 nodeFsInodesFree: "3Mi", 2279 nodeFsInodes: "4Mi", 2280 imageFsInodesFree: "3Mi", 2281 imageFsInodes: "4Mi", 2282 containerFsInodesFree: "3Mi", 2283 containerFsInodes: "4Mi", 2284 softINodePressure: "1.5Mi", 2285 hardINodePressure: "0.5Mi", 2286 inducePressureOnWhichFs: "nodefs", 2287 thresholdToMonitor: []evictionapi.Threshold{ 2288 { 2289 Signal: evictionapi.SignalNodeFsInodesFree, 2290 Operator: evictionapi.OpLessThan, 2291 Value: evictionapi.ThresholdValue{ 2292 Quantity: quantityMustParse("1Mi"), 2293 }, 2294 }, 2295 { 2296 Signal: evictionapi.SignalNodeFsInodesFree, 2297 Operator: evictionapi.OpLessThan, 2298 Value: evictionapi.ThresholdValue{ 2299 Quantity: quantityMustParse("2Mi"), 2300 }, 2301 GracePeriod: time.Minute * 2, 2302 }, 2303 }, 2304 podToMakes: []podToMake{ 2305 {name: "low-priority-high-usage", priority: lowPriority, requests: newResourceList("100m", "1Gi", ""), limits: newResourceList("100m", "1Gi", ""), rootFsInodesUsed: "900Mi"}, 2306 {name: "below-requests", priority: defaultPriority, requests: newResourceList("100m", "100Mi", ""), limits: newResourceList("200m", "1Gi", ""), rootFsInodesUsed: "50Mi"}, 2307 {name: "above-requests", priority: defaultPriority, requests: newResourceList("100m", "100Mi", ""), limits: newResourceList("200m", "1Gi", ""), rootFsInodesUsed: "400Mi"}, 2308 {name: "high-priority-high-usage", priority: highPriority, requests: newResourceList("", "", ""), limits: newResourceList("", "", ""), rootFsInodesUsed: "400Mi"}, 2309 {name: "low-priority-low-usage", priority: lowPriority, requests: newResourceList("", "", ""), limits: newResourceList("", "", ""), rootFsInodesUsed: "100Mi"}, 2310 }, 2311 }, 2312 } 2313 2314 for name, tc := range testCases { 2315 t.Run(name, func(t *testing.T) { 2316 featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.KubeletSeparateDiskGC, tc.kubeletSeparateDiskFeature) 2317 2318 podMaker := podMaker 2319 summaryStatsMaker := summaryStatsMaker 2320 podsToMake := tc.podToMakes 2321 pods := []*v1.Pod{} 2322 podStats := map[*v1.Pod]statsapi.PodStats{} 2323 for _, podToMake := range podsToMake { 2324 pod, podStat := podMaker(podToMake.name, podToMake.priority, podToMake.requests, podToMake.limits, podToMake.rootFsInodesUsed, podToMake.logsFsInodesUsed, podToMake.perLocalVolumeInodesUsed) 2325 pods = append(pods, pod) 2326 podStats[pod] = podStat 2327 } 2328 podToEvict := pods[0] 2329 activePodsFunc := func() []*v1.Pod { 2330 return pods 2331 } 2332 2333 fakeClock := testingclock.NewFakeClock(time.Now()) 2334 podKiller := &mockPodKiller{} 2335 diskInfoProvider := &mockDiskInfoProvider{dedicatedImageFs: tc.dedicatedImageFs} 2336 diskGC := &mockDiskGC{err: nil, readAndWriteSeparate: tc.writeableSeparateFromReadOnly} 2337 nodeRef := &v1.ObjectReference{Kind: "Node", Name: "test", UID: types.UID("test"), Namespace: ""} 2338 2339 config := Config{ 2340 MaxPodGracePeriodSeconds: 5, 2341 PressureTransitionPeriod: time.Minute * 5, 2342 Thresholds: tc.thresholdToMonitor, 2343 } 2344 startingStatsConst := summaryStatsMaker(tc.nodeFsInodesFree, tc.nodeFsInodes, tc.imageFsInodesFree, tc.imageFsInodes, tc.containerFsInodesFree, tc.containerFsInodes, podStats) 2345 startingStatsModified := summaryStatsMaker(tc.nodeFsInodesFree, tc.nodeFsInodes, tc.imageFsInodesFree, tc.imageFsInodes, tc.containerFsInodesFree, tc.containerFsInodes, podStats) 2346 summaryProvider := &fakeSummaryProvider{result: startingStatsModified} 2347 manager := &managerImpl{ 2348 clock: fakeClock, 2349 killPodFunc: podKiller.killPodNow, 2350 imageGC: diskGC, 2351 containerGC: diskGC, 2352 config: config, 2353 recorder: &record.FakeRecorder{}, 2354 summaryProvider: summaryProvider, 2355 nodeRef: nodeRef, 2356 nodeConditionsLastObservedAt: nodeConditionsObservedAt{}, 2357 thresholdsFirstObservedAt: thresholdsObservedAt{}, 2358 } 2359 2360 // create a best effort pod to test admission 2361 podToAdmit, _ := podMaker("pod-to-admit", defaultPriority, newResourceList("", "", ""), newResourceList("", "", ""), "0", "0", "0") 2362 2363 // synchronize 2364 _, err := manager.synchronize(diskInfoProvider, activePodsFunc) 2365 2366 if err != nil { 2367 t.Fatalf("Manager should not have an error %v", err) 2368 } 2369 2370 // we should not have disk pressure 2371 if manager.IsUnderDiskPressure() { 2372 t.Fatalf("Manager should not report inode pressure") 2373 } 2374 2375 // try to admit our pod (should succeed) 2376 if result := manager.Admit(&lifecycle.PodAdmitAttributes{Pod: podToAdmit}); !result.Admit { 2377 t.Fatalf("Admit pod: %v, expected: %v, actual: %v", podToAdmit, true, result.Admit) 2378 } 2379 2380 // induce soft threshold 2381 fakeClock.Step(1 * time.Minute) 2382 summaryProvider.result = setINodesFreeBasedOnFs(tc.inducePressureOnWhichFs, tc.softINodePressure, startingStatsModified) 2383 _, err = manager.synchronize(diskInfoProvider, activePodsFunc) 2384 2385 if err != nil { 2386 t.Fatalf("Manager should not have an error %v", err) 2387 } 2388 2389 // we should have disk pressure 2390 if !manager.IsUnderDiskPressure() { 2391 t.Fatalf("Manager should report inode pressure since soft threshold was met") 2392 } 2393 2394 // verify no pod was yet killed because there has not yet been enough time passed. 2395 if podKiller.pod != nil { 2396 t.Fatalf("Manager should not have killed a pod yet, but killed: %v", podKiller.pod.Name) 2397 } 2398 2399 // step forward in time pass the grace period 2400 fakeClock.Step(3 * time.Minute) 2401 summaryProvider.result = setINodesFreeBasedOnFs(tc.inducePressureOnWhichFs, tc.softINodePressure, startingStatsModified) 2402 _, err = manager.synchronize(diskInfoProvider, activePodsFunc) 2403 2404 if err != nil { 2405 t.Fatalf("Manager should not have an error %v", err) 2406 } 2407 2408 // we should have disk pressure 2409 if !manager.IsUnderDiskPressure() { 2410 t.Fatalf("Manager should report inode pressure since soft threshold was met") 2411 } 2412 2413 // verify the right pod was killed with the right grace period. 2414 if podKiller.pod != podToEvict { 2415 t.Fatalf("Manager chose to kill pod: %v, but should have chosen %v", podKiller.pod.Name, podToEvict.Name) 2416 } 2417 if podKiller.gracePeriodOverride == nil { 2418 t.Fatalf("Manager chose to kill pod but should have had a grace period override.") 2419 } 2420 observedGracePeriod := *podKiller.gracePeriodOverride 2421 if observedGracePeriod != manager.config.MaxPodGracePeriodSeconds { 2422 t.Fatalf("Manager chose to kill pod with incorrect grace period. Expected: %d, actual: %d", manager.config.MaxPodGracePeriodSeconds, observedGracePeriod) 2423 } 2424 // reset state 2425 podKiller.pod = nil 2426 podKiller.gracePeriodOverride = nil 2427 2428 // remove inode pressure 2429 fakeClock.Step(20 * time.Minute) 2430 summaryProvider.result = startingStatsConst 2431 _, err = manager.synchronize(diskInfoProvider, activePodsFunc) 2432 2433 if err != nil { 2434 t.Fatalf("Manager should not have an error %v", err) 2435 } 2436 2437 // we should not have disk pressure 2438 if manager.IsUnderDiskPressure() { 2439 t.Fatalf("Manager should not report inode pressure") 2440 } 2441 2442 // induce inode pressure! 2443 fakeClock.Step(1 * time.Minute) 2444 summaryProvider.result = setINodesFreeBasedOnFs(tc.inducePressureOnWhichFs, tc.hardINodePressure, startingStatsModified) 2445 _, err = manager.synchronize(diskInfoProvider, activePodsFunc) 2446 2447 if err != nil { 2448 t.Fatalf("Manager should not have an error %v", err) 2449 } 2450 2451 // we should have disk pressure 2452 if !manager.IsUnderDiskPressure() { 2453 t.Fatalf("Manager should report inode pressure") 2454 } 2455 2456 // check the right pod was killed 2457 if podKiller.pod != podToEvict { 2458 t.Fatalf("Manager chose to kill pod: %v, but should have chosen %v", podKiller.pod.Name, podToEvict.Name) 2459 } 2460 observedGracePeriod = *podKiller.gracePeriodOverride 2461 if observedGracePeriod != int64(1) { 2462 t.Fatalf("Manager chose to kill pod with incorrect grace period. Expected: %d, actual: %d", 1, observedGracePeriod) 2463 } 2464 2465 // try to admit our pod (should fail) 2466 if result := manager.Admit(&lifecycle.PodAdmitAttributes{Pod: podToAdmit}); result.Admit { 2467 t.Fatalf("Admit pod: %v, expected: %v, actual: %v", podToAdmit, false, result.Admit) 2468 } 2469 2470 // reduce inode pressure 2471 fakeClock.Step(1 * time.Minute) 2472 summaryProvider.result = startingStatsConst 2473 podKiller.pod = nil // reset state 2474 _, err = manager.synchronize(diskInfoProvider, activePodsFunc) 2475 2476 if err != nil { 2477 t.Fatalf("Manager should not have an error %v", err) 2478 } 2479 2480 // we should have disk pressure (because transition period not yet met) 2481 if !manager.IsUnderDiskPressure() { 2482 t.Fatalf("Manager should report inode pressure") 2483 } 2484 2485 // no pod should have been killed 2486 if podKiller.pod != nil { 2487 t.Fatalf("Manager chose to kill pod: %v when no pod should have been killed", podKiller.pod.Name) 2488 } 2489 2490 // try to admit our pod (should fail) 2491 if result := manager.Admit(&lifecycle.PodAdmitAttributes{Pod: podToAdmit}); result.Admit { 2492 t.Fatalf("Admit pod: %v, expected: %v, actual: %v", podToAdmit, false, result.Admit) 2493 } 2494 2495 // move the clock past transition period to ensure that we stop reporting pressure 2496 fakeClock.Step(5 * time.Minute) 2497 summaryProvider.result = startingStatsConst 2498 podKiller.pod = nil // reset state 2499 _, err = manager.synchronize(diskInfoProvider, activePodsFunc) 2500 2501 if err != nil { 2502 t.Fatalf("Manager should not have an error %v", err) 2503 } 2504 2505 // we should not have disk pressure (because transition period met) 2506 if manager.IsUnderDiskPressure() { 2507 t.Fatalf("Manager should not report inode pressure") 2508 } 2509 2510 // no pod should have been killed 2511 if podKiller.pod != nil { 2512 t.Fatalf("Manager chose to kill pod: %v when no pod should have been killed", podKiller.pod.Name) 2513 } 2514 2515 // try to admit our pod (should succeed) 2516 if result := manager.Admit(&lifecycle.PodAdmitAttributes{Pod: podToAdmit}); !result.Admit { 2517 t.Fatalf("Admit pod: %v, expected: %v, actual: %v", podToAdmit, true, result.Admit) 2518 } 2519 }) 2520 } 2521 } 2522 2523 // TestStaticCriticalPodsAreNotEvicted 2524 func TestStaticCriticalPodsAreNotEvicted(t *testing.T) { 2525 podMaker := makePodWithMemoryStats 2526 summaryStatsMaker := makeMemoryStats 2527 podsToMake := []podToMake{ 2528 {name: "critical", priority: scheduling.SystemCriticalPriority, requests: newResourceList("100m", "1Gi", ""), limits: newResourceList("100m", "1Gi", ""), memoryWorkingSet: "800Mi"}, 2529 } 2530 pods := []*v1.Pod{} 2531 podStats := map[*v1.Pod]statsapi.PodStats{} 2532 for _, podToMake := range podsToMake { 2533 pod, podStat := podMaker(podToMake.name, podToMake.priority, podToMake.requests, podToMake.limits, podToMake.memoryWorkingSet) 2534 pods = append(pods, pod) 2535 podStats[pod] = podStat 2536 } 2537 2538 pods[0].Annotations = map[string]string{ 2539 kubelettypes.ConfigSourceAnnotationKey: kubelettypes.FileSource, 2540 } 2541 // Mark the pod as critical 2542 podPriority := scheduling.SystemCriticalPriority 2543 pods[0].Spec.Priority = &podPriority 2544 pods[0].Namespace = kubeapi.NamespaceSystem 2545 2546 podToEvict := pods[0] 2547 activePodsFunc := func() []*v1.Pod { 2548 return pods 2549 } 2550 2551 fakeClock := testingclock.NewFakeClock(time.Now()) 2552 podKiller := &mockPodKiller{} 2553 diskInfoProvider := &mockDiskInfoProvider{dedicatedImageFs: ptr.To(false)} 2554 diskGC := &mockDiskGC{err: nil} 2555 nodeRef := &v1.ObjectReference{ 2556 Kind: "Node", Name: "test", UID: types.UID("test"), Namespace: "", 2557 } 2558 2559 config := Config{ 2560 MaxPodGracePeriodSeconds: 5, 2561 PressureTransitionPeriod: time.Minute * 5, 2562 Thresholds: []evictionapi.Threshold{ 2563 { 2564 Signal: evictionapi.SignalMemoryAvailable, 2565 Operator: evictionapi.OpLessThan, 2566 Value: evictionapi.ThresholdValue{ 2567 Quantity: quantityMustParse("1Gi"), 2568 }, 2569 }, 2570 { 2571 Signal: evictionapi.SignalMemoryAvailable, 2572 Operator: evictionapi.OpLessThan, 2573 Value: evictionapi.ThresholdValue{ 2574 Quantity: quantityMustParse("2Gi"), 2575 }, 2576 GracePeriod: time.Minute * 2, 2577 }, 2578 }, 2579 } 2580 summaryProvider := &fakeSummaryProvider{result: summaryStatsMaker("2Gi", podStats)} 2581 manager := &managerImpl{ 2582 clock: fakeClock, 2583 killPodFunc: podKiller.killPodNow, 2584 imageGC: diskGC, 2585 containerGC: diskGC, 2586 config: config, 2587 recorder: &record.FakeRecorder{}, 2588 summaryProvider: summaryProvider, 2589 nodeRef: nodeRef, 2590 nodeConditionsLastObservedAt: nodeConditionsObservedAt{}, 2591 thresholdsFirstObservedAt: thresholdsObservedAt{}, 2592 } 2593 2594 fakeClock.Step(1 * time.Minute) 2595 summaryProvider.result = summaryStatsMaker("1500Mi", podStats) 2596 _, err := manager.synchronize(diskInfoProvider, activePodsFunc) 2597 2598 if err != nil { 2599 t.Fatalf("Manager should not have an error %v", err) 2600 } 2601 2602 // we should have memory pressure 2603 if !manager.IsUnderMemoryPressure() { 2604 t.Errorf("Manager should report memory pressure since soft threshold was met") 2605 } 2606 2607 // verify no pod was yet killed because there has not yet been enough time passed. 2608 if podKiller.pod != nil { 2609 t.Errorf("Manager should not have killed a pod yet, but killed: %v", podKiller.pod.Name) 2610 } 2611 2612 // step forward in time pass the grace period 2613 fakeClock.Step(3 * time.Minute) 2614 summaryProvider.result = summaryStatsMaker("1500Mi", podStats) 2615 _, err = manager.synchronize(diskInfoProvider, activePodsFunc) 2616 2617 if err != nil { 2618 t.Fatalf("Manager should not have an error %v", err) 2619 } 2620 2621 // we should have memory pressure 2622 if !manager.IsUnderMemoryPressure() { 2623 t.Errorf("Manager should report memory pressure since soft threshold was met") 2624 } 2625 2626 // verify the right pod was killed with the right grace period. 2627 if podKiller.pod == podToEvict { 2628 t.Errorf("Manager chose to kill critical pod: %v, but should have ignored it", podKiller.pod.Name) 2629 } 2630 // reset state 2631 podKiller.pod = nil 2632 podKiller.gracePeriodOverride = nil 2633 2634 // remove memory pressure 2635 fakeClock.Step(20 * time.Minute) 2636 summaryProvider.result = summaryStatsMaker("3Gi", podStats) 2637 _, err = manager.synchronize(diskInfoProvider, activePodsFunc) 2638 2639 if err != nil { 2640 t.Fatalf("Manager should not have an error %v", err) 2641 } 2642 2643 // we should not have memory pressure 2644 if manager.IsUnderMemoryPressure() { 2645 t.Errorf("Manager should not report memory pressure") 2646 } 2647 2648 pods[0].Annotations = map[string]string{ 2649 kubelettypes.ConfigSourceAnnotationKey: kubelettypes.FileSource, 2650 } 2651 pods[0].Spec.Priority = nil 2652 pods[0].Namespace = kubeapi.NamespaceSystem 2653 2654 // induce memory pressure! 2655 fakeClock.Step(1 * time.Minute) 2656 summaryProvider.result = summaryStatsMaker("500Mi", podStats) 2657 _, err = manager.synchronize(diskInfoProvider, activePodsFunc) 2658 2659 if err != nil { 2660 t.Fatalf("Manager should not have an error %v", err) 2661 } 2662 2663 // we should have memory pressure 2664 if !manager.IsUnderMemoryPressure() { 2665 t.Errorf("Manager should report memory pressure") 2666 } 2667 } 2668 2669 func TestStorageLimitEvictions(t *testing.T) { 2670 volumeSizeLimit := resource.MustParse("1Gi") 2671 2672 testCases := map[string]struct { 2673 pod podToMake 2674 volumes []v1.Volume 2675 }{ 2676 "eviction due to rootfs above limit": { 2677 pod: podToMake{name: "rootfs-above-limits", priority: defaultPriority, requests: newResourceList("", "", "1Gi"), limits: newResourceList("", "", "1Gi"), rootFsUsed: "2Gi"}, 2678 }, 2679 "eviction due to logsfs above limit": { 2680 pod: podToMake{name: "logsfs-above-limits", priority: defaultPriority, requests: newResourceList("", "", "1Gi"), limits: newResourceList("", "", "1Gi"), logsFsUsed: "2Gi"}, 2681 }, 2682 "eviction due to local volume above limit": { 2683 pod: podToMake{name: "localvolume-above-limits", priority: defaultPriority, requests: newResourceList("", "", ""), limits: newResourceList("", "", ""), perLocalVolumeUsed: "2Gi"}, 2684 volumes: []v1.Volume{{ 2685 Name: "emptyDirVolume", 2686 VolumeSource: v1.VolumeSource{ 2687 EmptyDir: &v1.EmptyDirVolumeSource{ 2688 SizeLimit: &volumeSizeLimit, 2689 }, 2690 }, 2691 }}, 2692 }, 2693 } 2694 for name, tc := range testCases { 2695 t.Run(name, func(t *testing.T) { 2696 podMaker := makePodWithDiskStats 2697 summaryStatsMaker := makeDiskStats 2698 podsToMake := []podToMake{ 2699 tc.pod, 2700 } 2701 pods := []*v1.Pod{} 2702 podStats := map[*v1.Pod]statsapi.PodStats{} 2703 for _, podToMake := range podsToMake { 2704 pod, podStat := podMaker(podToMake.name, podToMake.priority, podToMake.requests, podToMake.limits, podToMake.rootFsUsed, podToMake.logsFsUsed, podToMake.perLocalVolumeUsed, tc.volumes) 2705 pods = append(pods, pod) 2706 podStats[pod] = podStat 2707 } 2708 2709 podToEvict := pods[0] 2710 activePodsFunc := func() []*v1.Pod { 2711 return pods 2712 } 2713 2714 fakeClock := testingclock.NewFakeClock(time.Now()) 2715 podKiller := &mockPodKiller{} 2716 diskInfoProvider := &mockDiskInfoProvider{dedicatedImageFs: ptr.To(false)} 2717 diskGC := &mockDiskGC{err: nil} 2718 nodeRef := &v1.ObjectReference{ 2719 Kind: "Node", Name: "test", UID: types.UID("test"), Namespace: "", 2720 } 2721 2722 config := Config{ 2723 MaxPodGracePeriodSeconds: 5, 2724 PressureTransitionPeriod: time.Minute * 5, 2725 Thresholds: []evictionapi.Threshold{ 2726 { 2727 Signal: evictionapi.SignalNodeFsAvailable, 2728 Operator: evictionapi.OpLessThan, 2729 Value: evictionapi.ThresholdValue{ 2730 Quantity: quantityMustParse("1Gi"), 2731 }, 2732 }, 2733 }, 2734 } 2735 2736 diskStat := diskStats{ 2737 rootFsAvailableBytes: "200Mi", 2738 imageFsAvailableBytes: "200Mi", 2739 podStats: podStats, 2740 } 2741 summaryProvider := &fakeSummaryProvider{result: summaryStatsMaker(diskStat)} 2742 manager := &managerImpl{ 2743 clock: fakeClock, 2744 killPodFunc: podKiller.killPodNow, 2745 imageGC: diskGC, 2746 containerGC: diskGC, 2747 config: config, 2748 recorder: &record.FakeRecorder{}, 2749 summaryProvider: summaryProvider, 2750 nodeRef: nodeRef, 2751 nodeConditionsLastObservedAt: nodeConditionsObservedAt{}, 2752 thresholdsFirstObservedAt: thresholdsObservedAt{}, 2753 localStorageCapacityIsolation: true, 2754 } 2755 2756 _, err := manager.synchronize(diskInfoProvider, activePodsFunc) 2757 if err != nil { 2758 t.Fatalf("Manager expects no error but got %v", err) 2759 } 2760 2761 if podKiller.pod == nil { 2762 t.Fatalf("Manager should have selected a pod for eviction") 2763 } 2764 if podKiller.pod != podToEvict { 2765 t.Errorf("Manager should have killed pod: %v, but instead killed: %v", podToEvict.Name, podKiller.pod.Name) 2766 } 2767 if *podKiller.gracePeriodOverride != 1 { 2768 t.Errorf("Manager should have evicted with gracePeriodOverride of 1, but used: %v", *podKiller.gracePeriodOverride) 2769 } 2770 }) 2771 } 2772 } 2773 2774 // TestAllocatableMemoryPressure 2775 func TestAllocatableMemoryPressure(t *testing.T) { 2776 podMaker := makePodWithMemoryStats 2777 summaryStatsMaker := makeMemoryStats 2778 podsToMake := []podToMake{ 2779 {name: "guaranteed-low-priority-high-usage", priority: lowPriority, requests: newResourceList("100m", "1Gi", ""), limits: newResourceList("100m", "1Gi", ""), memoryWorkingSet: "900Mi"}, 2780 {name: "burstable-below-requests", priority: defaultPriority, requests: newResourceList("100m", "100Mi", ""), limits: newResourceList("200m", "1Gi", ""), memoryWorkingSet: "50Mi"}, 2781 {name: "burstable-above-requests", priority: defaultPriority, requests: newResourceList("100m", "100Mi", ""), limits: newResourceList("200m", "1Gi", ""), memoryWorkingSet: "400Mi"}, 2782 {name: "best-effort-high-priority-high-usage", priority: highPriority, requests: newResourceList("", "", ""), limits: newResourceList("", "", ""), memoryWorkingSet: "400Mi"}, 2783 {name: "best-effort-low-priority-low-usage", priority: lowPriority, requests: newResourceList("", "", ""), limits: newResourceList("", "", ""), memoryWorkingSet: "100Mi"}, 2784 } 2785 pods := []*v1.Pod{} 2786 podStats := map[*v1.Pod]statsapi.PodStats{} 2787 for _, podToMake := range podsToMake { 2788 pod, podStat := podMaker(podToMake.name, podToMake.priority, podToMake.requests, podToMake.limits, podToMake.memoryWorkingSet) 2789 pods = append(pods, pod) 2790 podStats[pod] = podStat 2791 } 2792 podToEvict := pods[4] 2793 activePodsFunc := func() []*v1.Pod { 2794 return pods 2795 } 2796 2797 fakeClock := testingclock.NewFakeClock(time.Now()) 2798 podKiller := &mockPodKiller{} 2799 diskInfoProvider := &mockDiskInfoProvider{dedicatedImageFs: ptr.To(false)} 2800 diskGC := &mockDiskGC{err: nil} 2801 nodeRef := &v1.ObjectReference{Kind: "Node", Name: "test", UID: types.UID("test"), Namespace: ""} 2802 2803 config := Config{ 2804 MaxPodGracePeriodSeconds: 5, 2805 PressureTransitionPeriod: time.Minute * 5, 2806 Thresholds: []evictionapi.Threshold{ 2807 { 2808 Signal: evictionapi.SignalAllocatableMemoryAvailable, 2809 Operator: evictionapi.OpLessThan, 2810 Value: evictionapi.ThresholdValue{ 2811 Quantity: quantityMustParse("1Gi"), 2812 }, 2813 }, 2814 }, 2815 } 2816 summaryProvider := &fakeSummaryProvider{result: summaryStatsMaker("4Gi", podStats)} 2817 manager := &managerImpl{ 2818 clock: fakeClock, 2819 killPodFunc: podKiller.killPodNow, 2820 imageGC: diskGC, 2821 containerGC: diskGC, 2822 config: config, 2823 recorder: &record.FakeRecorder{}, 2824 summaryProvider: summaryProvider, 2825 nodeRef: nodeRef, 2826 nodeConditionsLastObservedAt: nodeConditionsObservedAt{}, 2827 thresholdsFirstObservedAt: thresholdsObservedAt{}, 2828 } 2829 2830 // create a best effort pod to test admission 2831 bestEffortPodToAdmit, _ := podMaker("best-admit", defaultPriority, newResourceList("", "", ""), newResourceList("", "", ""), "0Gi") 2832 burstablePodToAdmit, _ := podMaker("burst-admit", defaultPriority, newResourceList("100m", "100Mi", ""), newResourceList("200m", "200Mi", ""), "0Gi") 2833 2834 // synchronize 2835 _, err := manager.synchronize(diskInfoProvider, activePodsFunc) 2836 2837 if err != nil { 2838 t.Fatalf("Manager should not have an error %v", err) 2839 } 2840 2841 // we should not have memory pressure 2842 if manager.IsUnderMemoryPressure() { 2843 t.Errorf("Manager should not report memory pressure") 2844 } 2845 2846 // try to admit our pods (they should succeed) 2847 expected := []bool{true, true} 2848 for i, pod := range []*v1.Pod{bestEffortPodToAdmit, burstablePodToAdmit} { 2849 if result := manager.Admit(&lifecycle.PodAdmitAttributes{Pod: pod}); expected[i] != result.Admit { 2850 t.Errorf("Admit pod: %v, expected: %v, actual: %v", pod, expected[i], result.Admit) 2851 } 2852 } 2853 2854 // induce memory pressure! 2855 fakeClock.Step(1 * time.Minute) 2856 pod, podStat := podMaker("guaranteed-high-2", defaultPriority, newResourceList("100m", "1Gi", ""), newResourceList("100m", "1Gi", ""), "1Gi") 2857 podStats[pod] = podStat 2858 summaryProvider.result = summaryStatsMaker("500Mi", podStats) 2859 _, err = manager.synchronize(diskInfoProvider, activePodsFunc) 2860 2861 if err != nil { 2862 t.Fatalf("Manager should not have an error %v", err) 2863 } 2864 2865 // we should have memory pressure 2866 if !manager.IsUnderMemoryPressure() { 2867 t.Errorf("Manager should report memory pressure") 2868 } 2869 2870 // check the right pod was killed 2871 if podKiller.pod != podToEvict { 2872 t.Errorf("Manager chose to kill pod: %v, but should have chosen %v", podKiller.pod.Name, podToEvict.Name) 2873 } 2874 observedGracePeriod := *podKiller.gracePeriodOverride 2875 if observedGracePeriod != int64(1) { 2876 t.Errorf("Manager chose to kill pod with incorrect grace period. Expected: %d, actual: %d", 1, observedGracePeriod) 2877 } 2878 // reset state 2879 podKiller.pod = nil 2880 podKiller.gracePeriodOverride = nil 2881 2882 // the best-effort pod should not admit, burstable should 2883 expected = []bool{false, true} 2884 for i, pod := range []*v1.Pod{bestEffortPodToAdmit, burstablePodToAdmit} { 2885 if result := manager.Admit(&lifecycle.PodAdmitAttributes{Pod: pod}); expected[i] != result.Admit { 2886 t.Errorf("Admit pod: %v, expected: %v, actual: %v", pod, expected[i], result.Admit) 2887 } 2888 } 2889 2890 // reduce memory pressure 2891 fakeClock.Step(1 * time.Minute) 2892 for pod := range podStats { 2893 if pod.Name == "guaranteed-high-2" { 2894 delete(podStats, pod) 2895 } 2896 } 2897 summaryProvider.result = summaryStatsMaker("2Gi", podStats) 2898 podKiller.pod = nil // reset state 2899 _, err = manager.synchronize(diskInfoProvider, activePodsFunc) 2900 2901 if err != nil { 2902 t.Fatalf("Manager should not have an error %v", err) 2903 } 2904 2905 // we should have memory pressure (because transition period not yet met) 2906 if !manager.IsUnderMemoryPressure() { 2907 t.Errorf("Manager should report memory pressure") 2908 } 2909 2910 // no pod should have been killed 2911 if podKiller.pod != nil { 2912 t.Errorf("Manager chose to kill pod: %v when no pod should have been killed", podKiller.pod.Name) 2913 } 2914 2915 // the best-effort pod should not admit, burstable should 2916 expected = []bool{false, true} 2917 for i, pod := range []*v1.Pod{bestEffortPodToAdmit, burstablePodToAdmit} { 2918 if result := manager.Admit(&lifecycle.PodAdmitAttributes{Pod: pod}); expected[i] != result.Admit { 2919 t.Errorf("Admit pod: %v, expected: %v, actual: %v", pod, expected[i], result.Admit) 2920 } 2921 } 2922 2923 // move the clock past transition period to ensure that we stop reporting pressure 2924 fakeClock.Step(5 * time.Minute) 2925 summaryProvider.result = summaryStatsMaker("2Gi", podStats) 2926 podKiller.pod = nil // reset state 2927 _, err = manager.synchronize(diskInfoProvider, activePodsFunc) 2928 2929 if err != nil { 2930 t.Fatalf("Manager should not have an error %v", err) 2931 } 2932 2933 // we should not have memory pressure (because transition period met) 2934 if manager.IsUnderMemoryPressure() { 2935 t.Errorf("Manager should not report memory pressure") 2936 } 2937 2938 // no pod should have been killed 2939 if podKiller.pod != nil { 2940 t.Errorf("Manager chose to kill pod: %v when no pod should have been killed", podKiller.pod.Name) 2941 } 2942 2943 // all pods should admit now 2944 expected = []bool{true, true} 2945 for i, pod := range []*v1.Pod{bestEffortPodToAdmit, burstablePodToAdmit} { 2946 if result := manager.Admit(&lifecycle.PodAdmitAttributes{Pod: pod}); expected[i] != result.Admit { 2947 t.Errorf("Admit pod: %v, expected: %v, actual: %v", pod, expected[i], result.Admit) 2948 } 2949 } 2950 } 2951 2952 func TestUpdateMemcgThreshold(t *testing.T) { 2953 activePodsFunc := func() []*v1.Pod { 2954 return []*v1.Pod{} 2955 } 2956 2957 fakeClock := testingclock.NewFakeClock(time.Now()) 2958 podKiller := &mockPodKiller{} 2959 diskInfoProvider := &mockDiskInfoProvider{dedicatedImageFs: ptr.To(false)} 2960 diskGC := &mockDiskGC{err: nil} 2961 nodeRef := &v1.ObjectReference{Kind: "Node", Name: "test", UID: types.UID("test"), Namespace: ""} 2962 2963 config := Config{ 2964 MaxPodGracePeriodSeconds: 5, 2965 PressureTransitionPeriod: time.Minute * 5, 2966 Thresholds: []evictionapi.Threshold{ 2967 { 2968 Signal: evictionapi.SignalMemoryAvailable, 2969 Operator: evictionapi.OpLessThan, 2970 Value: evictionapi.ThresholdValue{ 2971 Quantity: quantityMustParse("1Gi"), 2972 }, 2973 }, 2974 }, 2975 PodCgroupRoot: "kubepods", 2976 } 2977 summaryProvider := &fakeSummaryProvider{result: makeMemoryStats("2Gi", map[*v1.Pod]statsapi.PodStats{})} 2978 2979 mockCtrl := gomock.NewController(t) 2980 defer mockCtrl.Finish() 2981 2982 thresholdNotifier := NewMockThresholdNotifier(mockCtrl) 2983 thresholdNotifier.EXPECT().UpdateThreshold(summaryProvider.result).Return(nil).Times(2) 2984 2985 manager := &managerImpl{ 2986 clock: fakeClock, 2987 killPodFunc: podKiller.killPodNow, 2988 imageGC: diskGC, 2989 containerGC: diskGC, 2990 config: config, 2991 recorder: &record.FakeRecorder{}, 2992 summaryProvider: summaryProvider, 2993 nodeRef: nodeRef, 2994 nodeConditionsLastObservedAt: nodeConditionsObservedAt{}, 2995 thresholdsFirstObservedAt: thresholdsObservedAt{}, 2996 thresholdNotifiers: []ThresholdNotifier{thresholdNotifier}, 2997 } 2998 2999 // The UpdateThreshold method should have been called once, since this is the first run. 3000 _, err := manager.synchronize(diskInfoProvider, activePodsFunc) 3001 3002 if err != nil { 3003 t.Fatalf("Manager should not have an error %v", err) 3004 } 3005 3006 // The UpdateThreshold method should not have been called again, since not enough time has passed 3007 _, err = manager.synchronize(diskInfoProvider, activePodsFunc) 3008 3009 if err != nil { 3010 t.Fatalf("Manager should not have an error %v", err) 3011 } 3012 3013 // The UpdateThreshold method should be called again since enough time has passed 3014 fakeClock.Step(2 * notifierRefreshInterval) 3015 _, err = manager.synchronize(diskInfoProvider, activePodsFunc) 3016 3017 if err != nil { 3018 t.Fatalf("Manager should not have an error %v", err) 3019 } 3020 3021 // new memory threshold notifier that returns an error 3022 thresholdNotifier = NewMockThresholdNotifier(mockCtrl) 3023 thresholdNotifier.EXPECT().UpdateThreshold(summaryProvider.result).Return(fmt.Errorf("error updating threshold")).Times(1) 3024 thresholdNotifier.EXPECT().Description().Return("mock thresholdNotifier").Times(1) 3025 manager.thresholdNotifiers = []ThresholdNotifier{thresholdNotifier} 3026 3027 // The UpdateThreshold method should be called because at least notifierRefreshInterval time has passed. 3028 // The Description method should be called because UpdateThreshold returned an error 3029 fakeClock.Step(2 * notifierRefreshInterval) 3030 _, err = manager.synchronize(diskInfoProvider, activePodsFunc) 3031 3032 if err != nil { 3033 t.Fatalf("Manager should not have an error %v", err) 3034 } 3035 } 3036 3037 func TestManagerWithLocalStorageCapacityIsolationOpen(t *testing.T) { 3038 podMaker := makePodWithLocalStorageCapacityIsolationOpen 3039 summaryStatsMaker := makeDiskStats 3040 podsToMake := []podToMake{ 3041 {name: "empty-dir", requests: newResourceList("", "900Mi", ""), limits: newResourceList("", "1Gi", "")}, 3042 {name: "container-ephemeral-storage-limit", requests: newResourceList("", "", "900Mi"), limits: newResourceList("", "", "800Mi")}, 3043 {name: "pod-ephemeral-storage-limit", requests: newResourceList("", "", "1Gi"), limits: newResourceList("", "", "800Mi")}, 3044 } 3045 3046 pods := []*v1.Pod{} 3047 podStats := map[*v1.Pod]statsapi.PodStats{} 3048 for _, podToMake := range podsToMake { 3049 pod, podStat := podMaker(podToMake.name, podToMake.priority, podToMake.requests, podToMake.limits, podToMake.memoryWorkingSet) 3050 pods = append(pods, pod) 3051 podStats[pod] = podStat 3052 } 3053 3054 diskStat := diskStats{ 3055 rootFsAvailableBytes: "1Gi", 3056 imageFsAvailableBytes: "200Mi", 3057 podStats: podStats, 3058 } 3059 summaryProvider := &fakeSummaryProvider{result: summaryStatsMaker(diskStat)} 3060 3061 config := Config{ 3062 MaxPodGracePeriodSeconds: 5, 3063 PressureTransitionPeriod: time.Minute * 5, 3064 Thresholds: []evictionapi.Threshold{ 3065 { 3066 Signal: evictionapi.SignalAllocatableMemoryAvailable, 3067 Operator: evictionapi.OpLessThan, 3068 Value: evictionapi.ThresholdValue{ 3069 Quantity: quantityMustParse("1Gi"), 3070 }, 3071 }, 3072 }, 3073 } 3074 3075 podKiller := &mockPodKiller{} 3076 diskGC := &mockDiskGC{err: nil} 3077 nodeRef := &v1.ObjectReference{Kind: "Node", Name: "test", UID: types.UID("test"), Namespace: ""} 3078 fakeClock := testingclock.NewFakeClock(time.Now()) 3079 diskInfoProvider := &mockDiskInfoProvider{dedicatedImageFs: ptr.To(false)} 3080 3081 mgr := &managerImpl{ 3082 clock: fakeClock, 3083 killPodFunc: podKiller.killPodNow, 3084 imageGC: diskGC, 3085 containerGC: diskGC, 3086 config: config, 3087 recorder: &record.FakeRecorder{}, 3088 summaryProvider: summaryProvider, 3089 nodeRef: nodeRef, 3090 localStorageCapacityIsolation: true, 3091 dedicatedImageFs: diskInfoProvider.dedicatedImageFs, 3092 } 3093 3094 activePodsFunc := func() []*v1.Pod { 3095 return pods 3096 } 3097 3098 evictedPods, err := mgr.synchronize(diskInfoProvider, activePodsFunc) 3099 3100 if err != nil { 3101 t.Fatalf("Manager should not have error but got %v", err) 3102 } 3103 if podKiller.pod == nil { 3104 t.Fatalf("Manager should have selected a pod for eviction") 3105 } 3106 3107 if diff := cmp.Diff(pods, evictedPods); diff != "" { 3108 t.Fatalf("Unexpected evicted pod (-want,+got):\n%s", diff) 3109 } 3110 }