在运行一段时候之后,节点上会下载很多镜像,也会有很多因为各种原因退出的容器。为了保证节点能够正常运行,kubelet 要防止镜像太多占满磁盘空间,也要防止退出的容器太多导致系统运行缓慢或者出现错误。
GC 的工作不需要手动干预,kubelet 会周期性去执行,不过在启动 kubelet 进程的时候可以通过参数控制 GC 的策略。kubelet会启动两个GC,分别回收container和image。其中container的回收频率为1分钟一次,而image回收频率为5分钟一次。
1. 容器GC
退出的容器也会继续占用系统资源,比如还会在文件系统存储很多数据、docker 应用也要占用 CPU 和内存去维护这些容器。docker 本身并不会自动删除已经退出的容器,因此 kubelet 就负起了这个责任。kubelet 容器的回收是为了删除已经退出的容器以节省节点的空间,提升性能。
容器 GC 虽然有利于空间和性能,但是删除容器也会导致错误现场被清理,不利于 debug 和错误定位,因此不建议把所有退出的容器都删除。因此容器的清理需要一定的策略,主要是告诉 kubelet 你要保存多少已经退出的容器。和容器 GC 有关的可以配置的 kubelet 启动参数包括:
-
MinimumGCAge:container 结束多长时间之后才能够被回收,默认是一分钟
-
MaxPerPodContainerCount:每个 container 最终可以保存多少个已经结束的容器,默认是 1,设置为负数表示不做限制
-
MaxContainerCount:节点上最多能保留多少个结束的容器,默认是 -1,表示不做限制
也就是说默认情况下,kubelet 会自动每分钟去做容器 GC,容器退出一分钟之后就可以被删除,而且每个容器做多只会保留一个已经退出的历史容器。
gc的步骤如下:
-
获取可以清除的容器,这些容器都是非活动的,并且创建时间比 gcPolicy.MinAge 要早;
-
通过强制执行gcPolicy.MaxPerPodContainer,为每个pod删除最老的死亡容器;
-
通过强制执行gcPolicy.MaxContainers来移除最老的死亡容器;
-
获取未准备好且不包含容器的可清除沙箱;
-
移除可移除的沙箱;
首先看一下 evictContainers 方法
- 首先执行 evictableContainers 来获取需要被gc的容器;
使用runtime 获取所有容器
containers, err := cgc.manager.getKubeletContainers(true)
循环查看获取到的所有容器
for _, container := range containers {
过滤掉所有正在运行的容器
if container.State == runtimeapi.ContainerState_CONTAINER_RUNNING {
continue
}
过滤掉时间不到的容器
if newestGCTime.Before(time.Unix(0, container.CreatedAt)) {
continue
}
保存符合要求的过期容器
evictUnits[key] = append(evictUnits[key], containerInfo)
}
根据容器的创建时间进行排序
for uid := range evictUnits {
sort.Sort(byCreated(evictUnits[uid]))
}
- 如果pod已经不存在了,那么就删除其中的所有容器。
if allSourcesReady {
for key, unit := range evictUnits {
if cgc.podStateProvider.IsPodDeleted(key.uid) || (cgc.podStateProvider.IsPodTerminated(key.uid) && evictTerminatedPods) {
调用runtime,删除n个容器
cgc.removeOldestN(unit, len(unit))
}
}
}
-
执行 GC 策略,保证每个 POD 最多只能保存 MaxPerPodContainer 个已经退出的容器
除了配置过的最大容器数量,调用 removeOldestN 删除剩余的所有容器
cgc.enforceMaxContainersPerEvictUnit(evictUnits, gcPolicy.MaxPerPodContainer)
-
执行 GC 策略,保证节点上最多有 MaxContainers 个已经退出的容器
先把最大容器数量平分到 pod,保证每个 pod 在平均数量以下;
numContainersPerEvictUnit := gcPolicy.MaxContainers / evictUnits.NumEvictUnits()
cgc.enforceMaxContainersPerEvictUnit(evictUnits, numContainersPerEvictUnit)
如果还不满足要求的数量,就按照时间顺序先删除最旧的容器
sort.Sort(byCreated(flattened))
cgc.removeOldestN(flattened, numContainers-gcPolicy.MaxContainers)
接下来看一下 evictSandboxes 方法, 这个方法移除所有可移除的沙箱。 一个可移动的沙箱必须符合以下要求:1.未处于就绪状态;2.不包含容器;3.属于一个不存在的(即已经移除的)pod,或者不是该pod的最新创建的沙箱;
获取所有的沙箱容器
containers, err := cgc.manager.getKubeletContainers(true)
for _, container := range containers {
sandboxIDs.Insert(container.PodSandboxId)
}
sandboxes, err := cgc.manager.getKubeletSandboxes(true)
根据沙箱状态,过滤掉存活,或者有容器存在的沙箱
sandbox.State == runtimeapi.PodSandboxState_SANDBOX_READY
sandboxIDs.Has(sandbox.Id)
根据沙箱创建时刻排序
sort.Sort(sandboxByCreated(sandboxesByPod[uid]))
如果pod死掉了,那么删除所有的沙箱
if cgc.podStateProvider.IsPodDeleted(podUID) || (cgc.podStateProvider.IsPodTerminated(podUID) && evictTerminatedPods) {
cgc.removeOldestNSandboxes(sandboxes, len(sandboxes))
}
否则,留一个沙箱,剩下的都可以删除
cgc.removeOldestNSandboxes(sandboxes, len(sandboxes)-1)
最后则回收/var/log/containers下已经被删除的容器的日志文件。
2. 镜像GC
镜像主要占用磁盘空间,虽然 docker 使用镜像分层可以让多个镜像共享存储,但是长时间运行的节点如果下载了很多镜像也会导致占用的存储空间过多。如果镜像导致磁盘被占满,会造成应用无法正常工作。docker 默认也不会做镜像清理,镜像一旦下载就会永远留在本地,除非被手动删除。
其实很多镜像并没有被实际使用,这些不用的镜像继续占用空间是非常大的浪费,也是巨大的隐患,因此 kubelet 也会周期性地去清理镜像。
镜像的清理和容器不同,是以占用的空间作为标准的,用户可以配置当镜像占据多大比例的存储空间时才进行清理。清理的时候会优先清理最久没有被使用的镜像,镜像被 pull 下来或者被容器使用都会更新它的最近使用时间。
启动 kubelet 的时候,可以配置这些参数控制镜像清理的策略:
-
ImageMinimumGCAge.Duration:镜像最少这么久没有被使用才会被清理
-
ImageGCHighThresholdPercent:磁盘使用率的上限,当达到这一使用率的时候会触发镜像清理。默认值为 90%
-
ImageGCLowThresholdPercent:磁盘使用率的下限,每次清理直到使用率低于这个值或者没有可以清理的镜像了才会停止.默认值为 80%
也就是说,默认情况下,当镜像占满所在盘 90% 容量的时候,kubelet 就会进行清理,一直到镜像占用率低于 80% 为止。
我们来看一下 GarbageCollect 方法
从 cadvisor 中获取镜像所在文件系统的信息,包括磁盘的容量和当前的使用量
fsStats, err := im.statsProvider.ImageFsStats()
如果镜像的磁盘使用率达到了设定的最高阈值,就进行清理工作,直到使用率
usagePercent := 100 - int(available*100/capacity)
if usagePercent >= im.policy.HighThresholdPercent {
im.freeSpace(amountToFree, time.Now())
}
接下来看一下 freeSpace
通过 runtime 更新镜像记录列表中的数据,添加刚发现的镜像,移除已经不存在的镜像
im.detectImages(freeTime)
根据镜像的最近使用时间和最近发现时间进行排序
sort.Sort(byLastUsedAndDetected(images))
遍历所有镜像,删除到有所需的空间
for _, image := range images {
略过最近使用时间距离现在小于设置的 MinAge 的镜像
if freeTime.Sub(image.firstDetected) < im.policy.MinAge {
continue
}
删除镜像并更新 imageRecords 对象中缓存的镜像信息,记录删除的镜像大小
im.runtime.RemoveImage(container.ImageSpec{Image: image.id})
delete(im.imageRecords, image.id)
spaceFreed += image.size
如果删除的镜像大小满足需求,停止继续删除
if spaceFreed >= bytesToFree {
break
}
}
realImageGCManager 缓存了当前节点使用的镜像信息,并在 Start() 方法中启动两个 goroutine 周期性地去更新缓存的内容。GarbageCollect 的逻辑是这样的:
调用 cAdvisor 接口获取镜像所在磁盘的文件系统信息,根据当前的使用量和配置的 GC 策略确定是否需要进行清理;
如果需要清理,计算需要清理的总大小,调用 freeSpace 进行镜像清理工作;
把所有可以清理的镜像根据使用时间进行排序,进行逐个清理,直到清理的镜像总大小满足需求才停止;
注意点:
-
默认情况下,container GC 是每分钟进行一次,image GC 是每五分钟一次,如果有不同的需要,可以通过 kubelet 的启动参数进行修改
-
不要手动清理镜像和容器,因为 kubelet 运行的时候会保存当前节点上镜像和容器的缓存,并定时更新。手动清理镜像和容器会让 kubelet 做出误判,带来不确定的问题