镜像垃圾回收
关联启动配置/标识
镜像垃圾回收机制有两个关联参数:
--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
垃圾回收模块会周期性(每五分钟)对宿主机上的镜像执行垃圾回收。回收流程主要如下:
- 调用运行时接口,获取存放镜像的文件系统信息,主要获取两个值:
- 文件系统磁盘总容量
- 文件系统磁盘可用容量
- 计算磁盘使用率使用到达垃圾回收阈值(
--image-gc-high-threshold
),如果到达阈值启动镜像垃圾回收流程。 - 启动垃圾回收流程后,首先计算出一个要释放出空间大小的值
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
那么获取镜像文件系统相关参数(主要为:总容量、已用容量)主要流程如下:
- 首先
kubelet
调用docker
的/info
接口(类似docker info
) - 解析上步返回值,获取
docker
根目录(/data
) - 获取存放镜像目录:
docker根目录/imgae
(如:/data/image
) - 递归计算镜像目录下文件总大小,即镜像文件系统已用空间大小(类似:
du -sh /data/image
) - 调用系统接口,获取
/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
}