kubernetes 中的垃圾回收機制主要有兩部分組成:
- 一是由 kube-controller-manager 中的 gc controller 自動回收 kubernetes 中被刪除的對象以及其依賴的對象;
- 二是在每個節(jié)點上需要回收已退出的容器以及當(dāng) node 上磁盤資源不足時回收已不再使用的容器鏡像;
本文主要分析 kubelet 中的垃圾回收機制,垃圾回收的主要目的是為了節(jié)約宿主上的資源,gc controller 的回收機制可以參考以前的文章 garbage collector controller 源碼分析。
kubelet 中與容器垃圾回收有關(guān)的主要有以下三個參數(shù):
-
--maximum-dead-containers-per-container: 表示一個 pod 最多可以保存多少個已經(jīng)停止的容器,默認(rèn)為1;(maxPerPodContainerCount) -
--maximum-dead-containers:一個 node 上最多可以保留多少個已經(jīng)停止的容器,默認(rèn)為 -1,表示沒有限制; -
--minimum-container-ttl-duration:已經(jīng)退出的容器可以存活的最小時間,默認(rèn)為 0s;
與鏡像回收有關(guān)的主要有以下三個參數(shù):
-
--image-gc-high-threshold:當(dāng) kubelet 磁盤達(dá)到多少時,kubelet 開始回收鏡像,默認(rèn)為 85% 開始回收,根目錄以及數(shù)據(jù)盤; -
--image-gc-low-threshold:回收鏡像時當(dāng)磁盤使用率減少至多少時停止回收,默認(rèn)為 80%; -
--minimum-image-ttl-duration:未使用的鏡像在被回收前的最小存留時間,默認(rèn)為 2m0s;
kubelet 中容器回收過程如下:
pod 中的容器退出時間超過--minimum-container-ttl-duration后會被標(biāo)記為可回收,一個 pod 中最多可以保留--maximum-dead-containers-per-container個已經(jīng)停止的容器,一個 node 上最多可以保留--maximum-dead-containers個已停止的容器。在回收容器時,kubelet 會按照容器的退出時間排序,最先回收退出時間最久的容器。需要注意的是,kubelet 在回收時會將 pod 中的 container 與 sandboxes 分別進行回收,且在回收容器后會將其對應(yīng)的 log dir 也進行回收;
kubelet 中鏡像回收過程如下:
當(dāng)容器鏡像掛載點文件系統(tǒng)的磁盤使用率大于--image-gc-high-threshold時(containerRuntime 為 docker 時,鏡像存放目錄默認(rèn)為 /var/lib/docker),kubelet 開始刪除節(jié)點中未使用的容器鏡像,直到磁盤使用率降低至--image-gc-low-threshold 時停止鏡像的垃圾回收。
kubelet GarbageCollect 源碼分析
kubernetes 版本:v1.16
GarbageCollect 是在 kubelet 對象初始化完成后啟動的,在 createAndInitKubelet 方法中首先調(diào)用 kubelet.NewMainKubelet 初始化了 kubelet 對象,隨后調(diào)用 k.StartGarbageCollection 啟動了 GarbageCollect。
k8s.io/kubernetes/cmd/kubelet/app/server.go:1089
func createAndInitKubelet(......) {
k, err = kubelet.NewMainKubelet(
......
)
if err != nil {
return nil, err
}
k.BirthCry()
k.StartGarbageCollection()
return k, nil
}
k.StartGarbageCollection
在 kubelet 中鏡像的生命周期和容器的生命周期是通過 imageManager 和 containerGC 管理的。在 StartGarbageCollection 方法中會啟動容器和鏡像垃圾回收兩個任務(wù),其主要邏輯為:
- 1、啟動 containerGC goroutine,ContainerGC 間隔時間默認(rèn)為 1 分鐘;
- 2、檢查
--image-gc-high-threshold參數(shù)的值,若為 100 則禁用 imageGC; - 3、啟動 imageGC goroutine,imageGC 間隔時間默認(rèn)為 5 分鐘;
k8s.io/kubernetes/pkg/kubelet/kubelet.go:1270
func (kl *Kubelet) StartGarbageCollection() {
loggedContainerGCFailure := false
// 1、啟動容器垃圾回收服務(wù)
go wait.Until(func() {
if err := kl.containerGC.GarbageCollect(); err != nil {
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)
// 2、檢查 ImageGCHighThresholdPercent 參數(shù)的值
if kl.kubeletConfiguration.ImageGCHighThresholdPercent == 100 {
return
}
// 3、啟動鏡像垃圾回收服務(wù)
prevImageGCFailed := false
go wait.Until(func() {
if err := kl.imageManager.GarbageCollect(); err != nil {
......
prevImageGCFailed = true
} else {
var vLevel klog.Level = 4
if prevImageGCFailed {
vLevel = 1
prevImageGCFailed = false
}
}
}, ImageGCPeriod, wait.NeverStop)
}
kl.containerGC.GarbageCollect
kl.containerGC.GarbageCollect 調(diào)用的是 ContainerGC manager 中的方法,ContainerGC 是在 NewMainKubelet 中初始化的,ContainerGC 在初始化時需要指定一個 runtime,該 runtime 即 ContainerRuntime,在 kubelet 中即 kubeGenericRuntimeManager,也是在 NewMainKubelet 中初始化的。
k8s.io/kubernetes/pkg/kubelet/kubelet.go
func NewMainKubelet(){
......
// MinAge、MaxPerPodContainer、MaxContainers 分別上文章開頭提到的與容器垃圾回收有關(guān)的
// 三個參數(shù)
containerGCPolicy := kubecontainer.ContainerGCPolicy{
MinAge: minimumGCAge.Duration,
MaxPerPodContainer: int(maxPerPodContainerCount),
MaxContainers: int(maxContainerCount),
}
// 初始化 containerGC 模塊
containerGC, err := kubecontainer.NewContainerGC(klet.containerRuntime, containerGCPolicy, klet.sourcesReady)
if err != nil {
return nil, err
}
......
}
以下是 ContainerGC 的初始化以及 GarbageCollect 的啟動:
k8s.io/kubernetes/pkg/kubelet/container/container_gc.go:68
func NewContainerGC(runtime Runtime, policy ContainerGCPolicy, sourcesReadyProvider SourcesReadyProvider) (ContainerGC, error) {
if policy.MinAge < 0 {
return nil, fmt.Errorf("invalid minimum garbage collection age: %v", policy.MinAge)
}
return &realContainerGC{
runtime: runtime,
policy: policy,
sourcesReadyProvider: sourcesReadyProvider,
}, nil
}
func (cgc *realContainerGC) GarbageCollect() error {
return cgc.runtime.GarbageCollect(cgc.policy, cgc.sourcesReadyProvider.AllReady(), false)
}
可以看到,ContainerGC 中的 GarbageCollect 最終是調(diào)用 runtime 中的 GarbageCollect 方法,runtime 即 kubeGenericRuntimeManager。
cgc.runtime.GarbageCollect
cgc.runtime.GarbageCollect 的實現(xiàn)是在 kubeGenericRuntimeManager 中,其主要邏輯為:
- 1、回收 pod 中的 container;
- 2、回收 pod 中的 sandboxes;
- 3、回收 pod 以及 container 的 log dir;
k8s.io/kubernetes/pkg/kubelet/kuberuntime/kuberuntime_gc.go:378
func (cgc *containerGC) GarbageCollect(gcPolicy kubecontainer.ContainerGCPolicy, allSourcesReady bool, evictTerminatedPods bool) error {
errors := []error{}
// 1、回收 pod 中的 container
if err := cgc.evictContainers(gcPolicy, allSourcesReady, evictTerminatedPods); err != nil {
errors = append(errors, err)
}
// 2、回收 pod 中的 sandboxes
if err := cgc.evictSandboxes(evictTerminatedPods); err != nil {
errors = append(errors, err)
}
// 3、回收 pod 以及 container 的 log dir
if err := cgc.evictPodLogsDirectories(allSourcesReady); err != nil {
errors = append(errors, err)
}
return utilerrors.NewAggregate(errors)
}
cgc.evictContainers
在 cgc.evictContainers 方法中會回收所有可被回收的容器,其主要邏輯為:
- 1、首先調(diào)用
cgc.evictableContainers獲取可被回收的容器作為 evictUnits,可被回收的容器指非 running 狀態(tài)且創(chuàng)建時間超過 MinAge,evictUnits 數(shù)組中包含 pod 與 container 的對應(yīng)關(guān)系; - 2、回收 deleted 狀態(tài)以及 terminated 狀態(tài)的 pod,遍歷 evictUnits,若 pod 是否處于 deleted 或者 terminated 狀態(tài),則調(diào)用
cgc.removeOldestN回收 pod 中的所有容器。deleted 狀態(tài)指 pod 已經(jīng)被刪除或者其status.phase為 failed 且其status.reason為 evicted 或者 pod.deletionTimestamp != nil 且 pod 中所有容器的 status 為 terminated 或者 waiting 狀態(tài),terminated 狀態(tài)指 pod 處于 Failed 或者 succeeded 狀態(tài); - 3、對于非 deleted 或者 terminated 狀態(tài)的 pod,調(diào)用
cgc.enforceMaxContainersPerEvictUnit為其保留MaxPerPodContainer個已經(jīng)退出的容器,按照容器退出的時間進行排序優(yōu)先刪除退出時間最久的,MaxPerPodContainer在上文已經(jīng)提過,表示一個 pod 最多可以保存多少個已經(jīng)停止的容器,默認(rèn)為1,可以使用--maximum-dead-containers-per-container在啟動時指定; - 4、若 kubelet 啟動時指定了
--maximum-dead-containers(默認(rèn)為 -1 即不限制),即需要為 node 保留退出的容器數(shù),若 node 上保留已經(jīng)停止的容器數(shù)超過--maximum-dead-containers,首先計算需要為每個 pod 保留多少個已退出的容器保證其總數(shù)不超過--maximum-dead-containers的值,若計算結(jié)果小于 1 則取 1,即至少保留一個,然后刪除每個 pod 中不需要保留的容器,此時若 node 上保留已經(jīng)停止的容器數(shù)依然超過需要保留的最大值,則將 evictUnits 中的容器按照退出時間進行排序刪除退出時間最久的容器,使 node 上保留已經(jīng)停止的容器數(shù)滿足--maximum-dead-containers值;
k8s.io/kubernetes/pkg/kubelet/kuberuntime/kuberuntime_gc.go:222
func (cgc *containerGC) evictContainers(gcPolicy kubecontainer.ContainerGCPolicy, allSourcesReady bool, evictTerminatedPods bool) error {
// 1、獲取可被回收的容器列表
evictUnits, err := cgc.evictableContainers(gcPolicy.MinAge)
if err != nil {
return err
}
// 2、回收 Deleted 狀態(tài)以及 Terminated 狀態(tài)的 pod,此處 allSourcesReady 指 kubelet
// 支持的三種 podSource 是否都可用
if allSourcesReady {
for key, unit := range evictUnits {
if cgc.podStateProvider.IsPodDeleted(key.uid) || (cgc.podStateProvider.IsPodTerminated(key.uid) && evictTerminatedPods) {
cgc.removeOldestN(unit, len(unit))
delete(evictUnits, key)
}
}
}
// 3、為非 Deleted 狀態(tài)以及 Terminated 狀態(tài)的 pod 保留 MaxPerPodContainer 個已經(jīng)退出的容器
if gcPolicy.MaxPerPodContainer >= 0 {
cgc.enforceMaxContainersPerEvictUnit(evictUnits, gcPolicy.MaxPerPodContainer)
}
// 4、若 kubelet 啟動時指定了 --maximum-dead-containers(默認(rèn)為 -1 即不限制)參數(shù),
// 此時需要為 node 保留退出的容器數(shù)不能超過 --maximum-dead-containers 個
if gcPolicy.MaxContainers >= 0 && evictUnits.NumContainers() > gcPolicy.MaxContainers {
numContainersPerEvictUnit := gcPolicy.MaxContainers / evictUnits.NumEvictUnits()
if numContainersPerEvictUnit < 1 {
numContainersPerEvictUnit = 1
}
cgc.enforceMaxContainersPerEvictUnit(evictUnits, numContainersPerEvictUnit)
numContainers := evictUnits.NumContainers()
if numContainers > gcPolicy.MaxContainers {
flattened := make([]containerGCInfo, 0, numContainers)
for key := range evictUnits {
flattened = append(flattened, evictUnits[key]...)
}
sort.Sort(byCreated(flattened))
cgc.removeOldestN(flattened, numContainers-gcPolicy.MaxContainers)
}
}
return nil
}
cgc.evictSandboxes
cgc.evictSandboxes 方法會回收所有可回收的 sandboxes,其主要邏輯為:
- 1、首先獲取 node 上所有的 containers 和 sandboxes;
- 2、構(gòu)建 sandboxes 與 pod 的對應(yīng)關(guān)系并將其保存在 sandboxesByPodUID 中;
- 3、對 sandboxesByPodUID 列表按創(chuàng)建時間進行排序;
- 4、若 sandboxes 所在的 pod 處于 deleted 狀態(tài),則刪除該 pod 中所有的 sandboxes 否則只保留退出時間最短的一個 sandboxes,deleted 狀態(tài)在上文
cgc.evictContainers方法中已經(jīng)解釋過;
k8s.io/kubernetes/pkg/kubelet/kuberuntime/kuberuntime_gc.go:274
func (cgc *containerGC) evictSandboxes(evictTerminatedPods bool) error {
// 1、獲取 node 上所有的 container
containers, err := cgc.manager.getKubeletContainers(true)
if err != nil {
return err
}
// 2、獲取 node 上所有的 sandboxes
sandboxes, err := cgc.manager.getKubeletSandboxes(true)
if err != nil {
return err
}
// 3、收集所有 container 的 PodSandboxId
sandboxIDs := sets.NewString()
for _, container := range containers {
sandboxIDs.Insert(container.PodSandboxId)
}
// 4、構(gòu)建 sandboxes 與 pod 的對應(yīng)關(guān)系并將其保存在 sandboxesByPodUID 中
sandboxesByPod := make(sandboxesByPodUID)
for _, sandbox := range sandboxes {
podUID := types.UID(sandbox.Metadata.Uid)
sandboxInfo := sandboxGCInfo{
id: sandbox.Id,
createTime: time.Unix(0, sandbox.CreatedAt),
}
if sandbox.State == runtimeapi.PodSandboxState_SANDBOX_READY {
sandboxInfo.active = true
}
if sandboxIDs.Has(sandbox.Id) {
sandboxInfo.active = true
}
sandboxesByPod[podUID] = append(sandboxesByPod[podUID], sandboxInfo)
}
// 5、對 sandboxesByPod 進行排序
for uid := range sandboxesByPod {
sort.Sort(sandboxByCreated(sandboxesByPod[uid]))
}
// 6、遍歷 sandboxesByPod,若 sandboxes 所在的 pod 處于 deleted 狀態(tài),
// 則刪除該 pod 中所有的 sandboxes 否則只保留退出時間最短的一個 sandboxes
for podUID, sandboxes := range sandboxesByPod {
if cgc.podStateProvider.IsPodDeleted(podUID) || (cgc.podStateProvider.IsPodTerminated(podUID) && evictTerminatedPods) {
cgc.removeOldestNSandboxes(sandboxes, len(sandboxes))
} else {
cgc.removeOldestNSandboxes(sandboxes, len(sandboxes)-1)
}
}
return nil
}
cgc.evictPodLogsDirectories
cgc.evictPodLogsDirectories 方法會回收所有可回收 pod 以及 container 的 log dir,其主要邏輯為:
1、首先回收 deleted 狀態(tài) pod logs dir,遍歷 pod logs dir
/var/log/pods,/var/log/pods為 pod logs 的默認(rèn)目錄,pod logs dir 的格式為/var/log/pods/NAMESPACE_NAME_UID,解析 pod logs dir 獲取 pod uid,判斷 pod 是否處于 deleted 狀態(tài),若處于 deleted 狀態(tài)則刪除其 logs dir;-
2、回收 deleted 狀態(tài) container logs 鏈接目錄,
/var/log/containers為 container log 的默認(rèn)目錄,其會軟鏈接到 pod 的 log dir 下,例如:/var/log/containers/storage-provisioner_kube-system_storage-provisioner-acc8386e409dfb3cc01618cbd14c373d8ac6d7f0aaad9ced018746f31d0081e2.log -> /var/log/pods/kube-system_storage-provisioner_b448e496-eb5d-4d71-b93f-ff7ff77d2348/storage-provisioner/0.log
k8s.io/kubernetes/pkg/kubelet/kuberuntime/kuberuntime_gc.go:333
func (cgc *containerGC) evictPodLogsDirectories(allSourcesReady bool) error {
osInterface := cgc.manager.osInterface
// 1、回收 deleted 狀態(tài) pod logs dir
if allSourcesReady {
dirs, err := osInterface.ReadDir(podLogsRootDirectory)
if err != nil {
return fmt.Errorf("failed to read podLogsRootDirectory %q: %v", podLogsRootDirectory, err)
}
for _, dir := range dirs {
name := dir.Name()
podUID := parsePodUIDFromLogsDirectory(name)
if !cgc.podStateProvider.IsPodDeleted(podUID) {
continue
}
err := osInterface.RemoveAll(filepath.Join(podLogsRootDirectory, name))
if err != nil {
klog.Errorf("Failed to remove pod logs directory %q: %v", name, err)
}
}
}
// 2、回收 deleted 狀態(tài) container logs 鏈接目錄
logSymlinks, _ := osInterface.Glob(filepath.Join(legacyContainerLogsDir, fmt.Sprintf("*.%s", legacyLogSuffix)))
for _, logSymlink := range logSymlinks {
if _, err := osInterface.Stat(logSymlink); os.IsNotExist(err) {
err := osInterface.Remove(logSymlink)
if err != nil {
klog.Errorf("Failed to remove container log dead symlink %q: %v", logSymlink, err)
}
}
}
return nil
}
kl.imageManager.GarbageCollect
上面已經(jīng)分析了容器回收的主要流程,下面會繼續(xù)分析鏡像回收的流程,kl.imageManager.GarbageCollect 是鏡像回收任務(wù)啟動的方法,鏡像回收流程是在 imageManager 中進行的,首先了解下 imageManager 的初始化,imageManager 也是在 NewMainKubelet 方法中進行初始化的。
k8s.io/kubernetes/pkg/kubelet/kubelet.go
func NewMainKubelet(){
......
// 初始化時需要指定三個參數(shù),三個參數(shù)已經(jīng)在上文中提到過
imageGCPolicy := images.ImageGCPolicy{
MinAge: kubeCfg.ImageMinimumGCAge.Duration,
HighThresholdPercent: int(kubeCfg.ImageGCHighThresholdPercent),
LowThresholdPercent: int(kubeCfg.ImageGCLowThresholdPercent),
}
......
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
......
}
kl.imageManager.GarbageCollect 方法的主要邏輯為:
- 1、首先調(diào)用
im.statsProvider.ImageFsStats獲取容器鏡像存儲目錄掛載點文件系統(tǒng)的磁盤信息; - 2、獲取掛載點的 available 和 capacity 信息并計算其使用率;
- 3、若使用率大于
HighThresholdPercent,首先根據(jù)LowThresholdPercent值計算需要釋放的磁盤量,然后調(diào)用im.freeSpace釋放未使用的 image 直到滿足磁盤空閑率;
k8s.io/kubernetes/pkg/kubelet/images/image_gc_manager.go:269
func (im *realImageGCManager) GarbageCollect() error {
// 1、獲取容器鏡像存儲目錄掛載點文件系統(tǒng)的磁盤信息
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 {
available = 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
}
// 2、若使用率大于 HighThresholdPercent,此時需要回收鏡像
usagePercent := 100 - int(available*100/capacity)
if usagePercent >= im.policy.HighThresholdPercent {
// 3、計算需要釋放的磁盤量
amountToFree := capacity*int64(100-im.policy.LowThresholdPercent)/100 - available
// 4、調(diào)用 im.freeSpace 回收未使用的鏡像信息
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
}
im.freeSpace
im.freeSpace 是回收未使用鏡像的方法,其主要邏輯為:
- 1、首先調(diào)用
im.detectImages獲取已經(jīng)使用的 images 列表作為 imagesInUse; - 2、遍歷
im.imageRecords根據(jù) imagesInUse 獲取所有未使用的 images 信息,im.imageRecords記錄 node 上所有 images 的信息; - 3、根據(jù)使用時間對未使用的 images 列表進行排序;
- 4、遍歷未使用的 images 列表然后調(diào)用
im.runtime.RemoveImage刪除鏡像,直到回收完所有未使用 images 或者滿足空閑率;
k8s.io/kubernetes/pkg/kubelet/images/image_gc_manager.go:328
func (im *realImageGCManager) freeSpace(bytesToFree int64, freeTime time.Time) (int64, error) {
// 1、獲取已經(jīng)使用的 images 列表
imagesInUse, err := im.detectImages(freeTime)
if err != nil {
return 0, err
}
im.imageRecordsLock.Lock()
defer im.imageRecordsLock.Unlock()
// 2、獲取所有未使用的 images 信息
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,
})
}
// 3、按鏡像使用時間進行排序
sort.Sort(byLastUsedAndDetected(images))
// 4、回收未使用的鏡像
var deletionErrors []error
spaceFreed := int64(0)
for _, image := range images {
if image.lastUsed.Equal(freeTime) || image.lastUsed.After(freeTime) {
continue
}
if freeTime.Sub(image.firstDetected) < im.policy.MinAge {
continue
}
// 5、調(diào)用 im.runtime.RemoveImage 刪除鏡像
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
}
總結(jié)
本文主要分析了 kubelet 中垃圾回收機制的實現(xiàn),kubelet 中會定期回收 node 上已經(jīng)退出的容器已經(jīng)當(dāng) node 磁盤資源不足時回收不再使用的鏡像來釋放磁盤資源,容器以及鏡像回收策略主要是通過 kubelet 中幾個參數(shù)的閾值進行控制的。