关于kubernetes垃圾回收那点事
本篇文章介绍了在kubernetes中kubelet如何对镜像和容器进行垃圾回收。
kubelet垃圾回收介绍及源码分析
使用kubernetes的过程中,为了保持磁盘的空间在一个合理的使用率,kubele提供了垃圾回收机制,kubelet的垃圾回收机制分为镜像的回收和container的回收。
Kubelet 垃圾回收(Garbage Collection)是一个非常有用的功能,它负责自动清理节点上的无用镜像和容器。Kubelet 每隔 1 分钟进行一次容器清理,每隔 5 分钟进行一次镜像清理(截止到 v1.18版本,垃圾回收间隔时间还都是在源码中固化的,不可自定义配置)
我们可以在kubelet的源码src\k8s.io\kubernetes\pkg\kubelet\kubelet.go中看下这个时间的配置,其中定义了2个变量分别是ContainerGCPeriod 和ImageGCPeriod ,表示执行镜像和容器的垃圾回收间隔时间
// ContainerGCPeriod is the period for performing container garbage collection.
ContainerGCPeriod = time.Minute
// ImageGCPeriod is the period for performing image garbage collection.
ImageGCPeriod = 5 * time.Minute执行垃圾回收的入口方式是StartGarbageCollection
func (kl *Kubelet) StartGarbageCollection() {
loggedContainerGCFailure := false
go wait.Until(func() {
if err := kl.containerGC.GarbageCollect(); err != nil {
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)
// when the high threshold is set to 100, stub the image GC manager
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 {
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)
}镜像收集
Kubernetes 通过 imageManager 与 cadvisor 协作的方式管理所有镜像的生命周期。
收集垃圾镜像的策略考虑两个因素: HighThresholdPercent 和 LowThresholdPercent。磁盘使用率超过高阈值将触发垃圾收集策略。该策略将删除最近最少使用的镜像直至满足低阈值。
kl.imageManager.GarbageCollect
上面已经分析了容器回收的主要流程,下面会继续分析镜像回收的流程,kl.imageManager.GarbageCollect 是镜像回收任务启动的方法,镜像回收流程是在 imageManager 中进行的,首先了解下 imageManager 的初始化,imageManager 也是在 NewMainKubelet 方法中进行初始化的。
k8s.io/kubernetes/pkg/kubelet/kubelet.go
kl.imageManager.GarbageCollect 方法的主要逻辑为:
首先调用 im.statsProvider.ImageFsStats 获取容器镜像存储目录挂载点文件系统的磁盘信息;
获取挂载点的 available 和 capacity 信息并计算其使用率;
若使用率大于 HighThresholdPercent,首先根据 LowThresholdPercent 值计算需要释放的磁盘量,然后调用 im.freeSpace 释放未使用的 image 直到满足磁盘空闲率;
k8s.io/kubernetes/pkg/kubelet/images/image_gc_manager.go:269
im.freeSpace
im.freeSpace 是回收未使用镜像的方法,其主要逻辑为:
首先调用 im.detectImages 获取已经使用的 images 列表作为 imagesInUse;
遍历 im.imageRecords 根据 imagesInUse 获取所有未使用的 images 信息,im.imageRecords 记录 node 上所有 images 的信息;
根据使用时间对未使用的 images 列表进行排序;
遍历未使用的 images 列表然后调用 im.runtime.RemoveImage 删除镜像,直到回收完所有未使用 images 或者满足空闲率;
k8s.io/kubernetes/pkg/kubelet/images/image_gc_manager.go:328
容器收集
容器收集策略考虑三个用户自定义变量。MinAge 是容器可以被收集的最小运行时间。MaxPerPodContainer 是每个pod (UID, container name) 中允许拥有死亡容器的最大数。MaxContainers全局死亡容器的最大数。通过将 MinAge 设置为零并将 MaxPerPodContainer 和 MaxContainers 分别设置为小于零,可以单独禁用这些变量。
Kubelet作用于未能被识别的,被删除的或超出上述变量边界的容器。最久远的容器首先被移除。当每个 pod(MaxPerPodContainer) 允许的最大容器数超出全局死亡容器的界限(MaxContainers) 时,MaxPerPodContainer 和 MaxContainer 可能会相互冲突。MaxPerPodContainer 可以在根据以下情形进行调整:最坏的情况是将 MaxPerPodContainer 降级至1并排除最旧的容器。此外,已被删除的 pod 所拥有的容器一旦比MinAge更旧,也会被移除。
kl.containerGC.GarbageCollect
kl.containerGC.GarbageCollect 调用的是 ContainerGC manager 中的方法,ContainerGC 是在 NewMainKubelet 中初始化的,ContainerGC 在初始化时需要指定一个 runtime,该 runtime 即 ContainerRuntime,在 kubelet 中即 kubeGenericRuntimeManager,也是在 NewMainKubelet 中初始化的。
k8s.io/kubernetes/pkg/kubelet/kubelet.go
以下是 ContainerGC 的初始化以及 GarbageCollect 的启动:
k8s.io/kubernetes/pkg/kubelet/container/container_gc.go:68
可以看到,ContainerGC 中的 GarbageCollect 最终是调用 runtime 中的 GarbageCollect 方法,runtime 即 kubeGenericRuntimeManager。
cgc.runtime.GarbageCollect
cgc.runtime.GarbageCollect 的实现是在 kubeGenericRuntimeManager 中,其主要逻辑为:
回收 pod 中的 container;
回收 pod 中的 sandboxes;
回收 pod 以及 container 的 log dir;
k8s.io/kubernetes/pkg/kubelet/kuberuntime/kuberuntime_gc.go:378
kubelet垃圾回收的参数配置实践
src\k8s.io\kubernetes\pkg\kubelet\apis\config\fuzzer\fuzzer.go 配置了kubelet参数的默认配置
镜像垃圾回收参数配置
--image-gc-high-threshold,默认 85,高于此阈值将进行回收
--image-gc-low-threshold,默认 80,低于此阈值不进行会
--minimum-image-ttl-duration,默认 2m0s,回收 image 最小年龄
我们在节点上修改kubelet的镜像回收配置
开始磁盘的使用率如下

执行垃圾回收后,磁盘使用率降到了57%

查看kubelet日志可以发现执行垃圾回收成功
容器垃圾回收参数配置

minimum-container-ttl-duration:容器可被回收的最小生存年龄,默认是 0 分钟,这意味着每个死亡容器都会被立即执行垃圾回收
maximum-dead-containers-per-container:每个 Pod 要保留的死亡容器的最大数量,默认值为 1
maximum-dead-containers:节点可保留的死亡容器的最大数量,默认值是 -1,这意味着节点没有限制死亡容器数量
注意:当MaxPerPodContainer与MaxContainers发生冲突时,Kubelet 会自动调整MaxPerPodContainer的取值以满足MaxContainers要求。
还是以 nginx 为例,创建一个 nginx 服务:
可以看到,Kubelet 启动了一个 sandbox 以及一个 nginx 实例。
手动杀死 nginx 实例,模拟容器异常退出:
可以看到 Kubelet 重新拉起了一个新的 nginx 实例。
等待几分钟,发现 Kubelet 并未清理异常退出的 nginx 容器(因为此时仅有一个 dead container)。
继续杀死当前 nginx 实例:
这下看到效果了,仍然只有一个退出的容器被保留,而且被清理掉的是最老的死亡容器,这与之前的分析是一致的!
删除这个 nginx Deployment,会发现所有的 nginx 容器都会被清理:
进一步,我们修改 Kubelet 参数,设置 maximum-dead-containers 为 0,这就告诉 Kubelet 清理所有死亡容器。
重复前边的实验步骤:
结果显示,nginx Pod 的所有死亡容器都会被清理,因为我们已经强制要求节点不保留任何死亡容器,与预期一致!
那对于手动运行的容器呢?我们通过 docker run 运行 nginx:
杀死该容器:
经过几分钟,我们发现该死亡容器还是会存在的,Kubelet 不会清理这类容器!
小结
Kubelet 每 5 分钟进行一次镜像清理。当磁盘使用率超过上限阈值,Kubelet 会按照 LRU 策略逐一清理没有被任何容器所使用的镜像,直到磁盘使用率降到下限阈值或没有空闲镜像可以清理。Kubelet 认为镜像可被清理的标准是未被任何 Pod 容器(包括那些死亡了的容器)所引用,那些非 Pod 容器(如用户通过 docker run 启动的容器)是不会被用来计算镜像引用关系的。也就是说,即便用户运行的容器使用了 A 镜像,只要没有任何 Pod 容器使用到 A,那 A 镜像对于 Kubelet 而言就是可被回收的。但是我们无需担心手动运行容器使用的镜像会被意外回收,因为 Kubelet 的镜像删除是非 force 类型的,底层容器运行时会使存在容器关联的镜像删除操作失败(因为 Docker 会认为仍有容器使用着 A 镜像)。
Kubelet 每 1 分钟执行一次容器清理。根据启动配置参数,Kubelet 会按照 LRU 策略依次清理每个 Pod 内的死亡容器,直到达到死亡容器限制数要求,对于 sandbox 容器,Kubelet 仅会保留最新的(这不受 GC 策略的控制)。对于日志目录,只要已经没有 Pod 继续占用,就将其清理。对于非 Pod 容器(如用户通过 docker run 启动的容器)不会被 Kubelet 垃圾回收。
参考文档
最后更新于
这有帮助吗?