kubernetes 中的垃圾回收机制主要有两部分组成:

  • 一是由 kube-controller-manager 中的 gc controller 自动回收 kubernetes 中被删除的对象以及其依赖的对象;
  • 二是在每个节点上需要回收已退出的容器以及当 node 上磁盘资源不足时回收已不再使用的容器镜像;

本文主要分析 kubelet 中的垃圾回收机制,垃圾回收的主要目的是为了节约宿主上的资源,gc controller 的回收机制可以参考以前的文章 garbage collector controller 源码分析

kubelet 中与容器垃圾回收有关的主要有以下三个参数:

  • --maximum-dead-containers-per-container: 表示一个 pod 最多可以保存多少个已经停止的容器,默认为1;(maxPerPodContainerCount)
  • --maximum-dead-containers:一个 node 上最多可以保留多少个已经停止的容器,默认为 -1,表示没有限制;
  • --minimum-container-ttl-duration:已经退出的容器可以存活的最小时间,默认为 0s;

与镜像回收有关的主要有以下三个参数:

  • --image-gc-high-threshold:当 kubelet 磁盘达到多少时,kubelet 开始回收镜像,默认为 85% 开始回收,根目录以及数据盘;
  • --image-gc-low-threshold:回收镜像时当磁盘使用率减少至多少时停止回收,默认为 80%;
  • --minimum-image-ttl-duration:未使用的镜像在被回收前的最小存留时间,默认为 2m0s;

kubelet 中容器回收过程如下: pod 中的容器退出时间超过--minimum-container-ttl-duration后会被标记为可回收,一个 pod 中最多可以保留--maximum-dead-containers-per-container个已经停止的容器,一个 node 上最多可以保留--maximum-dead-containers个已停止的容器。在回收容器时,kubelet 会按照容器的退出时间排序,最先回收退出时间最久的容器。需要注意的是,kubelet 在回收时会将 pod 中的 container 与 sandboxes 分别进行回收,且在回收容器后会将其对应的 log dir 也进行回收;

kubelet 中镜像回收过程如下: 当容器镜像挂载点文件系统的磁盘使用率大于--image-gc-high-threshold时(containerRuntime 为 docker 时,镜像存放目录默认为 /var/lib/docker),kubelet 开始删除节点中未使用的容器镜像,直到磁盘使用率降低至--image-gc-low-threshold 时停止镜像的垃圾回收。

kubelet GarbageCollect 源码分析

kubernetes 版本:v1.16

GarbageCollect 是在 kubelet 对象初始化完成后启动的,在 createAndInitKubelet 方法中首先调用 kubelet.NewMainKubelet 初始化了 kubelet 对象,随后调用 k.StartGarbageCollection 启动了 GarbageCollect。

k8s.io/kubernetes/cmd/kubelet/app/server.go:1089

  1. func createAndInitKubelet(......) {
  2. k, err = kubelet.NewMainKubelet(
  3. ......
  4. )
  5. if err != nil {
  6. return nil, err
  7. }
  8. k.BirthCry()
  9. k.StartGarbageCollection()
  10. return k, nil
  11. }

k.StartGarbageCollection

在 kubelet 中镜像的生命周期和容器的生命周期是通过 imageManager 和 containerGC 管理的。在 StartGarbageCollection 方法中会启动容器和镜像垃圾回收两个任务,其主要逻辑为:

  • 1、启动 containerGC goroutine,ContainerGC 间隔时间默认为 1 分钟;
  • 2、检查 --image-gc-high-threshold 参数的值,若为 100 则禁用 imageGC;
  • 3、启动 imageGC goroutine,imageGC 间隔时间默认为 5 分钟;

k8s.io/kubernetes/pkg/kubelet/kubelet.go:1270

  1. func (kl *Kubelet) StartGarbageCollection() {
  2. loggedContainerGCFailure := false
  3. // 1、启动容器垃圾回收服务
  4. go wait.Until(func() {
  5. if err := kl.containerGC.GarbageCollect(); err != nil {
  6. loggedContainerGCFailure = true
  7. } else {
  8. var vLevel klog.Level = 4
  9. if loggedContainerGCFailure {
  10. vLevel = 1
  11. loggedContainerGCFailure = false
  12. }
  13. klog.V(vLevel).Infof("Container garbage collection succeeded")
  14. }
  15. }, ContainerGCPeriod, wait.NeverStop)
  16. // 2、检查 ImageGCHighThresholdPercent 参数的值
  17. if kl.kubeletConfiguration.ImageGCHighThresholdPercent == 100 {
  18. return
  19. }
  20. // 3、启动镜像垃圾回收服务
  21. prevImageGCFailed := false
  22. go wait.Until(func() {
  23. if err := kl.imageManager.GarbageCollect(); err != nil {
  24. ......
  25. prevImageGCFailed = true
  26. } else {
  27. var vLevel klog.Level = 4
  28. if prevImageGCFailed {
  29. vLevel = 1
  30. prevImageGCFailed = false
  31. }
  32. }
  33. }, ImageGCPeriod, wait.NeverStop)
  34. }

kl.containerGC.GarbageCollect

kl.containerGC.GarbageCollect 调用的是 ContainerGC manager 中的方法,ContainerGC 是在 NewMainKubelet 中初始化的,ContainerGC 在初始化时需要指定一个 runtime,该 runtime 即 ContainerRuntime,在 kubelet 中即 kubeGenericRuntimeManager,也是在 NewMainKubelet 中初始化的。

k8s.io/kubernetes/pkg/kubelet/kubelet.go

  1. func NewMainKubelet(){
  2. ......
  3. // MinAge、MaxPerPodContainer、MaxContainers 分别上文章开头提到的与容器垃圾回收有关的
  4. // 三个参数
  5. containerGCPolicy := kubecontainer.ContainerGCPolicy{
  6. MinAge: minimumGCAge.Duration,
  7. MaxPerPodContainer: int(maxPerPodContainerCount),
  8. MaxContainers: int(maxContainerCount),
  9. }
  10. // 初始化 containerGC 模块
  11. containerGC, err := kubecontainer.NewContainerGC(klet.containerRuntime, containerGCPolicy, klet.sourcesReady)
  12. if err != nil {
  13. return nil, err
  14. }
  15. ......
  16. }

以下是 ContainerGC 的初始化以及 GarbageCollect 的启动:

k8s.io/kubernetes/pkg/kubelet/container/container_gc.go:68

  1. func NewContainerGC(runtime Runtime, policy ContainerGCPolicy, sourcesReadyProvider SourcesReadyProvider) (ContainerGC, error) {
  2. if policy.MinAge < 0 {
  3. return nil, fmt.Errorf("invalid minimum garbage collection age: %v", policy.MinAge)
  4. }
  5. return &realContainerGC{
  6. runtime: runtime,
  7. policy: policy,
  8. sourcesReadyProvider: sourcesReadyProvider,
  9. }, nil
  10. }
  11. func (cgc *realContainerGC) GarbageCollect() error {
  12. return cgc.runtime.GarbageCollect(cgc.policy, cgc.sourcesReadyProvider.AllReady(), false)
  13. }

可以看到,ContainerGC 中的 GarbageCollect 最终是调用 runtime 中的 GarbageCollect 方法,runtime 即 kubeGenericRuntimeManager。

cgc.runtime.GarbageCollect

cgc.runtime.GarbageCollect 的实现是在 kubeGenericRuntimeManager 中,其主要逻辑为:

  • 1、回收 pod 中的 container;
  • 2、回收 pod 中的 sandboxes;
  • 3、回收 pod 以及 container 的 log dir;

k8s.io/kubernetes/pkg/kubelet/kuberuntime/kuberuntime_gc.go:378

  1. func (cgc *containerGC) GarbageCollect(gcPolicy kubecontainer.ContainerGCPolicy, allSourcesReady bool, evictTerminatedPods bool) error {
  2. errors := []error{}
  3. // 1、回收 pod 中的 container
  4. if err := cgc.evictContainers(gcPolicy, allSourcesReady, evictTerminatedPods); err != nil {
  5. errors = append(errors, err)
  6. }
  7. // 2、回收 pod 中的 sandboxes
  8. if err := cgc.evictSandboxes(evictTerminatedPods); err != nil {
  9. errors = append(errors, err)
  10. }
  11. // 3、回收 pod 以及 container 的 log dir
  12. if err := cgc.evictPodLogsDirectories(allSourcesReady); err != nil {
  13. errors = append(errors, err)
  14. }
  15. return utilerrors.NewAggregate(errors)
  16. }
cgc.evictContainers

cgc.evictContainers 方法中会回收所有可被回收的容器,其主要逻辑为:

  • 1、首先调用 cgc.evictableContainers 获取可被回收的容器作为 evictUnits,可被回收的容器指非 running 状态且创建时间超过 MinAge,evictUnits 数组中包含 pod 与 container 的对应关系;
  • 2、回收 deleted 状态以及 terminated 状态的 pod,遍历 evictUnits,若 pod 是否处于 deleted 或者 terminated 状态,则调用 cgc.removeOldestN 回收 pod 中的所有容器。deleted 状态指 pod 已经被删除或者其 status.phase 为 failed 且其 status.reason 为 evicted 或者 pod.deletionTimestamp != nil 且 pod 中所有容器的 status 为 terminated 或者 waiting 状态,terminated 状态指 pod 处于 Failed 或者 succeeded 状态;
  • 3、对于非 deleted 或者 terminated 状态的 pod,调用 cgc.enforceMaxContainersPerEvictUnit 为其保留 MaxPerPodContainer 个已经退出的容器,按照容器退出的时间进行排序优先删除退出时间最久的,MaxPerPodContainer 在上文已经提过,表示一个 pod 最多可以保存多少个已经停止的容器,默认为1,可以使用 --maximum-dead-containers-per-container 在启动时指定;
  • 4、若 kubelet 启动时指定了--maximum-dead-containers(默认为 -1 即不限制),即需要为 node 保留退出的容器数,若 node 上保留已经停止的容器数超过 --maximum-dead-containers,首先计算需要为每个 pod 保留多少个已退出的容器保证其总数不超过 --maximum-dead-containers 的值,若计算结果小于 1 则取 1,即至少保留一个,然后删除每个 pod 中不需要保留的容器,此时若 node 上保留已经停止的容器数依然超过需要保留的最大值,则将 evictUnits 中的容器按照退出时间进行排序删除退出时间最久的容器,使 node 上保留已经停止的容器数满足 --maximum-dead-containers 值;

k8s.io/kubernetes/pkg/kubelet/kuberuntime/kuberuntime_gc.go:222

  1. func (cgc *containerGC) evictContainers(gcPolicy kubecontainer.ContainerGCPolicy, allSourcesReady bool, evictTerminatedPods bool) error {
  2. // 1、获取可被回收的容器列表
  3. evictUnits, err := cgc.evictableContainers(gcPolicy.MinAge)
  4. if err != nil {
  5. return err
  6. }
  7. // 2、回收 Deleted 状态以及 Terminated 状态的 pod,此处 allSourcesReady 指 kubelet
  8. // 支持的三种 podSource 是否都可用
  9. if allSourcesReady {
  10. for key, unit := range evictUnits {
  11. if cgc.podStateProvider.IsPodDeleted(key.uid) || (cgc.podStateProvider.IsPodTerminated(key.uid) && evictTerminatedPods) {
  12. cgc.removeOldestN(unit, len(unit))
  13. delete(evictUnits, key)
  14. }
  15. }
  16. }
  17. // 3、为非 Deleted 状态以及 Terminated 状态的 pod 保留 MaxPerPodContainer 个已经退出的容器
  18. if gcPolicy.MaxPerPodContainer >= 0 {
  19. cgc.enforceMaxContainersPerEvictUnit(evictUnits, gcPolicy.MaxPerPodContainer)
  20. }
  21. // 4、若 kubelet 启动时指定了 --maximum-dead-containers(默认为 -1 即不限制)参数,
  22. // 此时需要为 node 保留退出的容器数不能超过 --maximum-dead-containers 个
  23. if gcPolicy.MaxContainers >= 0 && evictUnits.NumContainers() > gcPolicy.MaxContainers {
  24. numContainersPerEvictUnit := gcPolicy.MaxContainers / evictUnits.NumEvictUnits()
  25. if numContainersPerEvictUnit < 1 {
  26. numContainersPerEvictUnit = 1
  27. }
  28. cgc.enforceMaxContainersPerEvictUnit(evictUnits, numContainersPerEvictUnit)
  29. numContainers := evictUnits.NumContainers()
  30. if numContainers > gcPolicy.MaxContainers {
  31. flattened := make([]containerGCInfo, 0, numContainers)
  32. for key := range evictUnits {
  33. flattened = append(flattened, evictUnits[key]...)
  34. }
  35. sort.Sort(byCreated(flattened))
  36. cgc.removeOldestN(flattened, numContainers-gcPolicy.MaxContainers)
  37. }
  38. }
  39. return nil
  40. }
cgc.evictSandboxes

cgc.evictSandboxes 方法会回收所有可回收的 sandboxes,其主要逻辑为:

  • 1、首先获取 node 上所有的 containers 和 sandboxes;
  • 2、构建 sandboxes 与 pod 的对应关系并将其保存在 sandboxesByPodUID 中;
  • 3、对 sandboxesByPodUID 列表按创建时间进行排序;
  • 4、若 sandboxes 所在的 pod 处于 deleted 状态,则删除该 pod 中所有的 sandboxes 否则只保留退出时间最短的一个 sandboxes,deleted 状态在上文 cgc.evictContainers 方法中已经解释过;

k8s.io/kubernetes/pkg/kubelet/kuberuntime/kuberuntime_gc.go:274

  1. func (cgc *containerGC) evictSandboxes(evictTerminatedPods bool) error {
  2. // 1、获取 node 上所有的 container
  3. containers, err := cgc.manager.getKubeletContainers(true)
  4. if err != nil {
  5. return err
  6. }
  7. // 2、获取 node 上所有的 sandboxes
  8. sandboxes, err := cgc.manager.getKubeletSandboxes(true)
  9. if err != nil {
  10. return err
  11. }
  12. // 3、收集所有 container 的 PodSandboxId
  13. sandboxIDs := sets.NewString()
  14. for _, container := range containers {
  15. sandboxIDs.Insert(container.PodSandboxId)
  16. }
  17. // 4、构建 sandboxes 与 pod 的对应关系并将其保存在 sandboxesByPodUID 中
  18. sandboxesByPod := make(sandboxesByPodUID)
  19. for _, sandbox := range sandboxes {
  20. podUID := types.UID(sandbox.Metadata.Uid)
  21. sandboxInfo := sandboxGCInfo{
  22. id: sandbox.Id,
  23. createTime: time.Unix(0, sandbox.CreatedAt),
  24. }
  25. if sandbox.State == runtimeapi.PodSandboxState_SANDBOX_READY {
  26. sandboxInfo.active = true
  27. }
  28. if sandboxIDs.Has(sandbox.Id) {
  29. sandboxInfo.active = true
  30. }
  31. sandboxesByPod[podUID] = append(sandboxesByPod[podUID], sandboxInfo)
  32. }
  33. // 5、对 sandboxesByPod 进行排序
  34. for uid := range sandboxesByPod {
  35. sort.Sort(sandboxByCreated(sandboxesByPod[uid]))
  36. }
  37. // 6、遍历 sandboxesByPod,若 sandboxes 所在的 pod 处于 deleted 状态,
  38. // 则删除该 pod 中所有的 sandboxes 否则只保留退出时间最短的一个 sandboxes
  39. for podUID, sandboxes := range sandboxesByPod {
  40. if cgc.podStateProvider.IsPodDeleted(podUID) || (cgc.podStateProvider.IsPodTerminated(podUID) && evictTerminatedPods) {
  41. cgc.removeOldestNSandboxes(sandboxes, len(sandboxes))
  42. } else {
  43. cgc.removeOldestNSandboxes(sandboxes, len(sandboxes)-1)
  44. }
  45. }
  46. return nil
  47. }
cgc.evictPodLogsDirectories

cgc.evictPodLogsDirectories 方法会回收所有可回收 pod 以及 container 的 log dir,其主要逻辑为:

  • 1、首先回收 deleted 状态 pod logs dir,遍历 pod logs dir /var/log/pods/var/log/pods 为 pod logs 的默认目录,pod logs dir 的格式为 /var/log/pods/NAMESPACE_NAME_UID,解析 pod logs dir 获取 pod uid,判断 pod 是否处于 deleted 状态,若处于 deleted 状态则删除其 logs dir;
  • 2、回收 deleted 状态 container logs 链接目录,/var/log/containers 为 container log 的默认目录,其会软链接到 pod 的 log dir 下,例如:

    1. /var/log/containers/storage-provisioner_kube-system_storage-provisioner-acc8386e409dfb3cc01618cbd14c373d8ac6d7f0aaad9ced018746f31d0081e2.log -> /var/log/pods/kube-system_storage-provisioner_b448e496-eb5d-4d71-b93f-ff7ff77d2348/storage-provisioner/0.log

k8s.io/kubernetes/pkg/kubelet/kuberuntime/kuberuntime_gc.go:333

  1. func (cgc *containerGC) evictPodLogsDirectories(allSourcesReady bool) error {
  2. osInterface := cgc.manager.osInterface
  3. // 1、回收 deleted 状态 pod logs dir
  4. if allSourcesReady {
  5. dirs, err := osInterface.ReadDir(podLogsRootDirectory)
  6. if err != nil {
  7. return fmt.Errorf("failed to read podLogsRootDirectory %q: %v", podLogsRootDirectory, err)
  8. }
  9. for _, dir := range dirs {
  10. name := dir.Name()
  11. podUID := parsePodUIDFromLogsDirectory(name)
  12. if !cgc.podStateProvider.IsPodDeleted(podUID) {
  13. continue
  14. }
  15. err := osInterface.RemoveAll(filepath.Join(podLogsRootDirectory, name))
  16. if err != nil {
  17. klog.Errorf("Failed to remove pod logs directory %q: %v", name, err)
  18. }
  19. }
  20. }
  21. // 2、回收 deleted 状态 container logs 链接目录
  22. logSymlinks, _ := osInterface.Glob(filepath.Join(legacyContainerLogsDir, fmt.Sprintf("*.%s", legacyLogSuffix)))
  23. for _, logSymlink := range logSymlinks {
  24. if _, err := osInterface.Stat(logSymlink); os.IsNotExist(err) {
  25. err := osInterface.Remove(logSymlink)
  26. if err != nil {
  27. klog.Errorf("Failed to remove container log dead symlink %q: %v", logSymlink, err)
  28. }
  29. }
  30. }
  31. return nil
  32. }

kl.imageManager.GarbageCollect

上面已经分析了容器回收的主要流程,下面会继续分析镜像回收的流程,kl.imageManager.GarbageCollect 是镜像回收任务启动的方法,镜像回收流程是在 imageManager 中进行的,首先了解下 imageManager 的初始化,imageManager 也是在 NewMainKubelet 方法中进行初始化的。

k8s.io/kubernetes/pkg/kubelet/kubelet.go

  1. func NewMainKubelet(){
  2. ......
  3. // 初始化时需要指定三个参数,三个参数已经在上文中提到过
  4. imageGCPolicy := images.ImageGCPolicy{
  5. MinAge: kubeCfg.ImageMinimumGCAge.Duration,
  6. HighThresholdPercent: int(kubeCfg.ImageGCHighThresholdPercent),
  7. LowThresholdPercent: int(kubeCfg.ImageGCLowThresholdPercent),
  8. }
  9. ......
  10. imageManager, err := images.NewImageGCManager(klet.containerRuntime, klet.StatsProvider, kubeDeps.Recorder, nodeRef, imageGCPolicy, crOptions.PodSandboxImage)
  11. if err != nil {
  12. return nil, fmt.Errorf("failed to initialize image manager: %v", err)
  13. }
  14. klet.imageManager = imageManager
  15. ......
  16. }

kl.imageManager.GarbageCollect 方法的主要逻辑为:

  • 1、首先调用 im.statsProvider.ImageFsStats 获取容器镜像存储目录挂载点文件系统的磁盘信息;
  • 2、获取挂载点的 available 和 capacity 信息并计算其使用率;
  • 3、若使用率大于 HighThresholdPercent,首先根据 LowThresholdPercent 值计算需要释放的磁盘量,然后调用 im.freeSpace 释放未使用的 image 直到满足磁盘空闲率;

k8s.io/kubernetes/pkg/kubelet/images/image_gc_manager.go:269

  1. func (im *realImageGCManager) GarbageCollect() error {
  2. // 1、获取容器镜像存储目录挂载点文件系统的磁盘信息
  3. fsStats, err := im.statsProvider.ImageFsStats()
  4. if err != nil {
  5. return err
  6. }
  7. var capacity, available int64
  8. if fsStats.CapacityBytes != nil {
  9. capacity = int64(*fsStats.CapacityBytes)
  10. }
  11. if fsStats.AvailableBytes != nil {
  12. available = int64(*fsStats.AvailableBytes)
  13. }
  14. if available > capacity {
  15. available = capacity
  16. }
  17. if capacity == 0 {
  18. err := goerrors.New("invalid capacity 0 on image filesystem")
  19. im.recorder.Eventf(im.nodeRef, v1.EventTypeWarning, events.InvalidDiskCapacity, err.Error())
  20. return err
  21. }
  22. // 2、若使用率大于 HighThresholdPercent,此时需要回收镜像
  23. usagePercent := 100 - int(available*100/capacity)
  24. if usagePercent >= im.policy.HighThresholdPercent {
  25. // 3、计算需要释放的磁盘量
  26. amountToFree := capacity*int64(100-im.policy.LowThresholdPercent)/100 - available
  27. // 4、调用 im.freeSpace 回收未使用的镜像信息
  28. freed, err := im.freeSpace(amountToFree, time.Now())
  29. if err != nil {
  30. return err
  31. }
  32. if freed < amountToFree {
  33. err := fmt.Errorf("failed to garbage collect required amount of images. Wanted to free %d bytes, but freed %d bytes", amountToFree, freed)
  34. im.recorder.Eventf(im.nodeRef, v1.EventTypeWarning, events.FreeDiskSpaceFailed, err.Error())
  35. return err
  36. }
  37. }
  38. return nil
  39. }
im.freeSpace

im.freeSpace 是回收未使用镜像的方法,其主要逻辑为:

  • 1、首先调用 im.detectImages 获取已经使用的 images 列表作为 imagesInUse;
  • 2、遍历 im.imageRecords 根据 imagesInUse 获取所有未使用的 images 信息,im.imageRecords 记录 node 上所有 images 的信息;
  • 3、根据使用时间对未使用的 images 列表进行排序;
  • 4、遍历未使用的 images 列表然后调用 im.runtime.RemoveImage 删除镜像,直到回收完所有未使用 images 或者满足空闲率;

k8s.io/kubernetes/pkg/kubelet/images/image_gc_manager.go:328

  1. func (im *realImageGCManager) freeSpace(bytesToFree int64, freeTime time.Time) (int64, error) {
  2. // 1、获取已经使用的 images 列表
  3. imagesInUse, err := im.detectImages(freeTime)
  4. if err != nil {
  5. return 0, err
  6. }
  7. im.imageRecordsLock.Lock()
  8. defer im.imageRecordsLock.Unlock()
  9. // 2、获取所有未使用的 images 信息
  10. images := make([]evictionInfo, 0, len(im.imageRecords))
  11. for image, record := range im.imageRecords {
  12. if isImageUsed(image, imagesInUse) {
  13. klog.V(5).Infof("Image ID %s is being used", image)
  14. continue
  15. }
  16. images = append(images, evictionInfo{
  17. id: image,
  18. imageRecord: *record,
  19. })
  20. }
  21. // 3、按镜像使用时间进行排序
  22. sort.Sort(byLastUsedAndDetected(images))
  23. // 4、回收未使用的镜像
  24. var deletionErrors []error
  25. spaceFreed := int64(0)
  26. for _, image := range images {
  27. if image.lastUsed.Equal(freeTime) || image.lastUsed.After(freeTime) {
  28. continue
  29. }
  30. if freeTime.Sub(image.firstDetected) < im.policy.MinAge {
  31. continue
  32. }
  33. // 5、调用 im.runtime.RemoveImage 删除镜像
  34. err := im.runtime.RemoveImage(container.ImageSpec{Image: image.id})
  35. if err != nil {
  36. deletionErrors = append(deletionErrors, err)
  37. continue
  38. }
  39. delete(im.imageRecords, image.id)
  40. spaceFreed += image.size
  41. if spaceFreed >= bytesToFree {
  42. break
  43. }
  44. }
  45. if len(deletionErrors) > 0 {
  46. return spaceFreed, fmt.Errorf("wanted to free %d bytes, but freed %d bytes space with errors in image deletion: %v", bytesToFree, spaceFreed, errors.NewAggregate(deletionErrors))
  47. }
  48. return spaceFreed, nil
  49. }

总结

本文主要分析了 kubelet 中垃圾回收机制的实现,kubelet 中会定期回收 node 上已经退出的容器已经当 node 磁盘资源不足时回收不再使用的镜像来释放磁盘资源,容器以及镜像回收策略主要是通过 kubelet 中几个参数的阈值进行控制的。