kubernetes镜像GC

一、kubernetes镜像GC

镜像主要占用磁盘空间,虽然 docker 使用镜像分层可以让多个镜像共享存储,但是长时间运行的节点如果下载了很多镜像也会导致占用的存储空间过多。如果镜像导致磁盘被占满,会造成应用无法正常工作。docker 默认也不会做镜像清理,镜像一旦下载就会永远留在本地,除非被手动删除。

其实很多镜像并没有被实际使用,这些不用的镜像继续占用空间是非常大的浪费,也是巨大的隐患,因此kubelet也会周期性的去清理镜像。

镜像的清理和容器不同,是以占用的空间作为标准的,用户可以配置当镜像占据多大比例的存储空间时才进行清理。清理的时候会优先清理最久没有被使用的镜像,镜像被pull下来或者被容器使用都会更新它的最近使用时间。

启动kubelet的时候,可以配置这些参数控制镜像清理的策略:

  • image-gc-high-threshold:磁盘使用率的上限,当达到这一使用率的时候会触发镜像清理。默认值为 90%

  • image-gc-low-threshold:磁盘使用率的下限,每次清理直到使用率低于这个值或者没有可以清理的镜像了才会停止.默认值为 80%

  • minimum-image-ttl-duration:镜像最少这么久没有被使用才会被清理,可以使用 h(小时)、m(分钟)、s(秒)和 ms(毫秒)时间单位进行配置,默认是 2m(两分钟)

也就是说,默认情况下,当镜像占满所在盘 90% 容量的时候,kubelet 就会进行清理,一直到镜像占用率低于 80% 为止。

参数配置

用户可以使用以下 kubelet 参数调整相关阈值来优化镜像垃圾回收:

  • image-gc-high-threshold,触发镜像垃圾回收的磁盘使用率百分比。默认值为 8。如果该值设置为100,则会停止镜像垃圾回收。

  • image-gc-low-threshold,镜像垃圾回收试图释放资源后达到的磁盘使用率百分比。默认值为 80。

  • minimum-image-ttl-duration,默认 2m0s,回收 image 最小年龄

垃圾回收过程中可能上报的相关事件有:

  • ContainerGCFailed:每 1min 执行一次容器垃圾回收,如果执行失败,则上报该事件

  • ImageGCFailed:每 5min 执行一次镜像垃圾回收,如果执行失败,则上报该事件

  • FreeDiskSpaceFailed:如果执行镜像垃圾回收时,被清理空间不满足要求,则上报该异常事件

  • InvalidDiskCapacity:如果 image disk 容量为0,则上报该异常事件

1.1 接口定义

type ImageGCManager interface {
  // 执行垃圾回收策略,如果根据垃圾回收策略不能释放足够的空间,则会返回 error
    GarbageCollect() error
    // 启动异步垃圾镜像回收
    Start()

    GetImageList() ([]container.Image, error)
    // 删除所有无用镜像
    DeleteUnusedImages() error
}

1.2 初始化

ImageGCManager 是在 kubelet.NewMainKubelet()方法中完成初始化的:

// setup imageManager
imageManager, err := images.NewImageGCManager(klet.containerRuntime, klet.StatsProvider, kubeDeps.Recorder, nodeRef, imageGCPolicy, crOptions.PodSandboxImage)
if err != nil {
    return nil, fmt.Errorf("failed to initialize image manager: %v", err)
}
klet.imageManager = imageManager

1.3 realImageGCManager.Start()

ImageGCManager 的启动是在 kubelet.initializeModules() 方法中完成的。ImageGCManager 在启动后开始异步执行两个任务:

  • 每5min更新一次正在使用的镜像列表信息

  • 每30s更新一次镜像缓存

func (im *realImageGCManager) Start() {
    go wait.Until(func() {
        var ts time.Time
        if im.initialized {
            ts = time.Now()
        }
        _, err := im.detectImages(ts) // 更新缓存镜像列表,并返回正在使用的镜像列表
        if err != nil {
            klog.Warningf("[imageGCManager] Failed to monitor images: %v", err)
        } else {
            im.initialized = true
        }
    }, 5*time.Minute, wait.NeverStop) // 每5min探测一次

    // 每30s更新一次镜像缓存
    go wait.Until(func() {
        images, err := im.runtime.ListImages()
        if err != nil {
            klog.Warningf("[imageGCManager] Failed to update image list: %v", err)
        } else {
            im.imageCache.set(images)
        }
    }, 30*time.Second, wait.NeverStop)

}

1.4 启动垃圾回收

kubelet启动完成后, 会开启垃圾回收异步线程。它会

  • 每 1min 执行一次容器垃圾回收,如果执行失败,则上报事件 ContainerGCFailed

func (kl *Kubelet) StartGarbageCollection() {
    loggedContainerGCFailure := false
    go wait.Until(func() {
        if err := kl.containerGC.GarbageCollect(); err != nil {  // 每 1min 执行一次容器垃圾回收,如果执行失败,则上报事件 ContainerGCFailed
            klog.Errorf("Container garbage collection failed: %v", err)
            kl.recorder.Eventf(kl.nodeRef, v1.EventTypeWarning, events.ContainerGCFailed, err.Error())
            loggedContainerGCFailure = true
        } else {
            var vLevel klog.Level = 4
            if loggedContainerGCFailure {
                vLevel = 1
                loggedContainerGCFailure = false
            }

            klog.V(vLevel).Infof("Container garbage collection succeeded")
        }
    }, ContainerGCPeriod, wait.NeverStop)

    // 如果 --image-gc-high-threshold=100,则会停止镜像垃圾回收。
    if kl.kubeletConfiguration.ImageGCHighThresholdPercent == 100 {
        klog.V(2).Infof("ImageGCHighThresholdPercent is set 100, Disable image GC")
        return
    }

    prevImageGCFailed := false
    go wait.Until(func() {
        if err := kl.imageManager.GarbageCollect(); err != nil { // 每 5min 执行一次镜像垃圾回收,如果执行失败,则上报 ImageGCFailed 事件
            if prevImageGCFailed {
                klog.Errorf("Image garbage collection failed multiple times in a row: %v", err)
                // Only create an event for repeated failures
                kl.recorder.Eventf(kl.nodeRef, v1.EventTypeWarning, events.ImageGCFailed, err.Error())
            } else {
                klog.Errorf("Image garbage collection failed once. Stats initialization may not have completed yet: %v", err)
            }
            prevImageGCFailed = true
        } else {
            var vLevel klog.Level = 4
            if prevImageGCFailed {
                vLevel = 1
                prevImageGCFailed = false
            }

            klog.V(vLevel).Infof("Image garbage collection succeeded")
        }
    }, ImageGCPeriod, wait.NeverStop)
}

1.5 realImageGCManager.GarbageCollect()

镜像垃圾回收的执行过程为:

  • 从 cadvisor 获取 image 磁盘信息

  • 计算磁盘容量和磁盘利用率

  • 如果磁盘利用率达到 --image-gc-high-threshold 设置的上限,则进行镜像垃圾回收

  • 如果执行完镜像垃圾回收后,释放的空间没达到预期值,则上报 FreeDiskSpaceFailed异常事件

func (im *realImageGCManager) GarbageCollect() error {
    // 从 cadvisor 获取 image 磁盘信息
    fsStats, err := im.statsProvider.ImageFsStats()
    if err != nil {
        return err
    }

    var capacity, available int64
    if fsStats.CapacityBytes != nil { // image 磁盘容器
        capacity = int64(*fsStats.CapacityBytes)
    }
    if fsStats.AvailableBytes != nil { // image 磁盘可用空间
        available = int64(*fsStats.AvailableBytes)
    }

    if available > capacity { // 修正磁盘容量大小
        klog.Warningf("available %d is larger than capacity %d", available, capacity)
        available = capacity
    }

    // Check valid capacity.
    if capacity == 0 { // 如果磁盘容量为0,则上报 InvalidDiskCapacity 异常时间
        err := goerrors.New("invalid capacity 0 on image filesystem")
        im.recorder.Eventf(im.nodeRef, v1.EventTypeWarning, events.InvalidDiskCapacity, err.Error())
        return err
    }

    usagePercent := 100 - int(available*100/capacity) // 磁盘使用率达到上限
    if usagePercent >= im.policy.HighThresholdPercent {
        amountToFree := capacity*int64(100-im.policy.LowThresholdPercent)/100 - available // 计算要清理的磁盘空间大小
        klog.Infof("[imageGCManager]: Disk usage on image filesystem is at %d%% which is over the high threshold (%d%%). Trying to free %d bytes down to the low threshold (%d%%).", usagePercent, im.policy.HighThresholdPercent, amountToFree, im.policy.LowThresholdPercent)
        freed, err := im.freeSpace(amountToFree, time.Now()) // 清理镜像,并返回清理的空间大小
        if err != nil {
            return err
        }

        if freed < amountToFree { // 如果被清理空间不满足要求,则上报 FreeDiskSpaceFailed 异常事件
            err := fmt.Errorf("failed to garbage collect required amount of images. Wanted to free %d bytes, but freed %d bytes", amountToFree, freed)
            im.recorder.Eventf(im.nodeRef, v1.EventTypeWarning, events.FreeDiskSpaceFailed, err.Error())
            return err
        }
    }

    return nil
}

1.6 释放磁盘空间(freeSpace)

这里记录了镜像垃圾回收的详细过程:

  • 列出所有未在使用的镜像

  • 按照最后使用时间和探测时间由远到近排序

  • 遍历列表,按时间由远到近清理镜像

  • 再次判断,如果镜像正在使用,则不进行清理。判断镜像初次被探测时间,避免清理拉取时间较短的镜像,因为这些镜像可能刚被拉取下来,马上要被某个容器使用

  • 调用 runtime 接口删除无用镜像,直到释放足够的空间为止

func (im *realImageGCManager) freeSpace(bytesToFree int64, freeTime time.Time) (int64, error) {
    imagesInUse, err := im.detectImages(freeTime) // 更新正在使用的镜像列表,并返回正在使用的镜像列表
    if err != nil {
        return 0, err
    }

    im.imageRecordsLock.Lock()
    defer im.imageRecordsLock.Unlock()

    // 列出所有没在使用的镜像
    images := make([]evictionInfo, 0, len(im.imageRecords))
    for image, record := range im.imageRecords {
        if isImageUsed(image, imagesInUse) {
            klog.V(5).Infof("Image ID %s is being used", image)
            continue
        }
        images = append(images, evictionInfo{
            id:          image,
            imageRecord: *record,
        })
    }
    sort.Sort(byLastUsedAndDetected(images))  // 按照最后使用时间和探测时间排序
    // 删除无用的镜像,直到释放足够的空间为止
    var deletionErrors []error
    spaceFreed := int64(0)
    for _, image := range images {
        klog.V(5).Infof("Evaluating image ID %s for possible garbage collection", image.id)
        // 再次判断镜像是否正在使用
        if image.lastUsed.Equal(freeTime) || image.lastUsed.After(freeTime) {
            klog.V(5).Infof("Image ID %s has lastUsed=%v which is >= freeTime=%v, not eligible for garbage collection", image.id, image.lastUsed, freeTime)
            continue
        }
    // 避免清理拉取时间较短的镜像,因为这些镜像可能刚被拉取下来,马上要被某个容器使用
        if freeTime.Sub(image.firstDetected) < im.policy.MinAge {
            klog.V(5).Infof("Image ID %s has age %v which is less than the policy's minAge of %v, not eligible for garbage collection", image.id, freeTime.Sub(image.firstDetected), im.policy.MinAge)
            continue
        }
        // 清理镜像,即便发生error
        klog.Infof("[imageGCManager]: Removing image %q to free %d bytes", image.id, image.size)
        err := im.runtime.RemoveImage(container.ImageSpec{Image: image.id})
        if err != nil {
            deletionErrors = append(deletionErrors, err)
            continue
        }
        delete(im.imageRecords, image.id)
        spaceFreed += image.size

        if spaceFreed >= bytesToFree {
            break
        }
    }

    if len(deletionErrors) > 0 {
        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))
    }
    return spaceFreed, nil
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值