镜像垃圾回收

关联启动配置/标识

镜像垃圾回收机制有两个关联参数:

  • --image-gc-high-threshold: 磁盘使用百分比,当磁盘使用占比(100 * 已用/总量)大于等于该值,会启动镜像垃圾回收流程。 值必须在[0,100]范围内,要禁用镜像垃圾收集,设置为100。【默认值85
  • --image-gc-low-threshold: 磁盘使用百分比,当垃圾回收流程启动后, 该值与--image-gc-high-threshold通过算术关系控制垃圾回收的空间大小。 该值取值范围为[0,100],且不应大于--image-gc-high-threshold。【默认值80

关于这两个参数的作用,举个栗子

假设: 镜像文件系统的磁盘总容量为100G--image-gc-high-threshold--image-gc-low-threshold默认值

镜像文件系统的磁盘已用容量为90G时,此时镜像文件系统的磁盘使用率为100% * 90G/100G = 90% > --image-gc-high-threshold(85%)。 此时将触发镜像垃圾回收,具体回收流程后续讨论。这里会计算出一个垃圾回收需要释放的空间大小amountToFree:

amountToFree = 磁盘总量 * (100-`--image-gc-low-threshold`值)/100 - 磁盘可用大小

带入值进行计算需要释放出的空间大小为

amountToFree = 100G * (100-80)/100 - (100-90)
amountToFree = 10G

通过上面的分析我们发现,其实kubelet自带的垃圾回收存在一定的利弊:

  • 利: 周期性回收镜像,避免因镜像文件写满磁盘分区导致灾难性事故(例如:当存放镜像的分区为系统/分区)
  • 弊:
    • 只会按比例执行镜像清理,并不会完全清理掉某些无用的镜像(这些镜像会一直存在,直到后续垃圾回收流程触发,才可能被清理掉)
    • 同一时间大批量删除镜像,将导致IO飙升

尽管kubelet自带镜像垃圾回收功能,但并不能完全清理掉所有无用镜像,从而导致过多冗余数据占用系统磁盘空间。所以存放镜像的分区最好单独指定。

docker配置: "data-root": "/data"

流程解析

kubelet垃圾回收模块会周期性(每五分钟)对宿主机上的镜像执行垃圾回收。回收流程主要如下:

  1. 调用运行时接口,获取存放镜像的文件系统信息,主要获取两个值:
  2. 文件系统磁盘总容量
  3. 文件系统磁盘可用容量
  4. 计算磁盘使用率使用到达垃圾回收阈值(--image-gc-high-threshold),如果到达阈值启动镜像垃圾回收流程。
  5. 启动垃圾回收流程后,首先计算出一个要释放出空间大小的值
  6. kubelet对本地镜像进行排序,找到未被容器使用的镜像,调用运行时接口对其释放。并且该镜像--minimum-image-ttl-duration

垃圾回收流程源码实现

kubernetes\pkg\kubelet\images\image_gc_manager.go

func (im *realImageGCManager) GarbageCollect() error {
    // Get disk usage on disk holding images.
    // 调用运行时获取存放镜像的文件系统状态:
    fsStats, err := im.statsProvider.ImageFsStats()
    if err != nil {
        return err
    }

    var capacity, available int64
    if fsStats.CapacityBytes != nil {
        capacity = int64(*fsStats.CapacityBytes)
    }
    if fsStats.AvailableBytes != nil {
        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 {
        err := goerrors.New("invalid capacity 0 on image filesystem")
        im.recorder.Eventf(im.nodeRef, v1.EventTypeWarning, events.InvalidDiskCapacity, err.Error())
        return err
    }

    // If over the max threshold, free enough to place us at the lower threshold.
    usagePercent := 100 - int(available*100/capacity)
    // available=10G capacity=100G HighThresholdPercent=85% LowThresholdPercent=80%
    if usagePercent >= im.policy.HighThresholdPercent {
        // amountToFree=5G
        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 {
            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
}

关于docker运行时获取镜像文件系统信息

由于源码中涉及一些额外的概念(如文件系统唯一标识等),增加了理解负担。这里我们通过代入的方式进行讨论:

我们假设docker信息如下:

  • docker根目录为: /data
  • /data/dev/sdb挂载,/dev/sdb磁盘容量大小为100G

那么获取镜像文件系统相关参数(主要为:总容量、已用容量)主要流程如下:

  1. 首先kubelet调用docker/info接口(类似docker info
  2. 解析上步返回值,获取docker根目录(/data)
  3. 获取存放镜像目录:docker根目录/imgae(如:/data/image)
  4. 递归计算镜像目录下文件总大小,即镜像文件系统已用空间大小(类似: du -sh /data/image
  5. 调用系统接口,获取/data挂载点总容量,即镜像文件系统总容量(100G

获取镜像文件系统使用信息

kubernetes\pkg\kubelet\dockershim\docker_image_linux.go

func (ds *dockerService) ImageFsInfo(_ context.Context, _ *runtimeapi.ImageFsInfoRequest) (*runtimeapi.ImageFsInfoResponse, error) {
    info, err := ds.client.Info()
    if err != nil {
        klog.Errorf("Failed to get docker info: %v", err)
        return nil, err
    }

    bytes, inodes, err := dirSize(filepath.Join(info.DockerRootDir, "image"))
    if err != nil {
        return nil, err
    }

    return &runtimeapi.ImageFsInfoResponse{
        ImageFilesystems: []*runtimeapi.FilesystemUsage{
            {
                Timestamp: time.Now().Unix(),
                FsId: &runtimeapi.FilesystemIdentifier{
                    Mountpoint: info.DockerRootDir,
                },
                UsedBytes: &runtimeapi.UInt64Value{
                    Value: uint64(bytes),
                },
                InodesUsed: &runtimeapi.UInt64Value{
                    Value: uint64(inodes),
                },
            },
        },
    }, nil
}

调用系统接口获取镜像文件系统的磁盘总容量

func (p *criStatsProvider) ImageFsStats() (*statsapi.FsStats, error) {
    resp, err := p.imageService.ImageFsInfo()
    if err != nil {
        return nil, err
    }

    // CRI may return the stats of multiple image filesystems but we only
    // return the first one.
    //
    // TODO(yguo0905): Support returning stats of multiple image filesystems.
    if len(resp) == 0 {
        return nil, fmt.Errorf("imageFs information is unavailable")
    }
    fs := resp[0]
    s := &statsapi.FsStats{
        Time:      metav1.NewTime(time.Unix(0, fs.Timestamp)),
        UsedBytes: &fs.UsedBytes.Value,
    }
    if fs.InodesUsed != nil {
        s.InodesUsed = &fs.InodesUsed.Value
    }
    imageFsInfo := p.getFsInfo(fs.GetFsId())
    if imageFsInfo != nil {
        // The image filesystem id is unknown to the local node or there's
        // an error on retrieving the stats. In these cases, we omit those
        // stats and return the best-effort partial result. See
        // https://github.com/kubernetes/heapster/issues/1793.
        s.AvailableBytes = &imageFsInfo.Available
        s.CapacityBytes = &imageFsInfo.Capacity
        s.InodesFree = imageFsInfo.InodesFree
        s.Inodes = imageFsInfo.Inodes
    }
    return s, nil
}
Copyright © weiliang 2021 all right reserved,powered by Gitbook本书发布时间: 2024-04-22 16:03:41

results matching ""

    No results matching ""