kubelet 的 GC

在运行一段时候之后,节点上会下载很多镜像,也会有很多因为各种原因退出的容器。为了保证节点能够正常运行,kubelet 要防止镜像太多占满磁盘空间,也要防止退出的容器太多导致系统运行缓慢或者出现错误。

GC 的工作不需要手动干预,kubelet 会周期性去执行,不过在启动 kubelet 进程的时候可以通过参数控制 GC 的策略。kubelet会启动两个GC,分别回收container和image。其中container的回收频率为1分钟一次,而image回收频率为5分钟一次。

kubernetes/pkg/kubelet/kubelet.go 

1. 容器GC

退出的容器也会继续占用系统资源,比如还会在文件系统存储很多数据、docker 应用也要占用 CPU 和内存去维护这些容器。docker 本身并不会自动删除已经退出的容器,因此 kubelet 就负起了这个责任。kubelet 容器的回收是为了删除已经退出的容器以节省节点的空间,提升性能。

容器 GC 虽然有利于空间和性能,但是删除容器也会导致错误现场被清理,不利于 debug 和错误定位,因此不建议把所有退出的容器都删除。因此容器的清理需要一定的策略,主要是告诉 kubelet 你要保存多少已经退出的容器。和容器 GC 有关的可以配置的 kubelet 启动参数包括:

* MinimumGCAge:container 结束多长时间之后才能够被回收,默认是一分钟

* MaxPerPodContainerCount:每个 container 最终可以保存多少个已经结束的容器,默认是 1,设置为负数表示不做限制

* MaxContainerCount:节点上最多能保留多少个结束的容器,默认是 -1,表示不做限制

也就是说默认情况下,kubelet 会自动每分钟去做容器 GC,容器退出一分钟之后就可以被删除,而且每个容器做多只会保留一个已经退出的历史容器。

pkg/kubelet/container/container_gc.go

gc的步骤如下:

1. 获取可以清除的容器,这些容器都是非活动的,并且创建时间比 gcPolicy.MinAge 要早;

2. 通过强制执行gcPolicy.MaxPerPodContainer,为每个pod删除最老的死亡容器;

3. 通过强制执行gcPolicy.MaxContainers来移除最老的死亡容器;

4. 获取未准备好且不包含容器的可清除沙箱;

5. 移除可移除的沙箱;

pkg/kubelet/runtime/kuberuntime_gc.go

首先看一下 evictContainers 方法

1. 首先执行 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]))    

}

2. 如果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))

        }

    }

}

3. 执行 GC 策略,保证每个 POD 最多只能保存 MaxPerPodContainer 个已经退出的容器

除了配置过的最大容器数量,调用 removeOldestN 删除剩余的所有容器

cgc.enforceMaxContainersPerEvictUnit(evictUnits, gcPolicy.MaxPerPodContainer)

4. 执行 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 进行镜像清理工作;

把所有可以清理的镜像根据使用时间进行排序,进行逐个清理,直到清理的镜像总大小满足需求才停止;

注意点:

1. 默认情况下,container GC 是每分钟进行一次,image GC 是每五分钟一次,如果有不同的需要,可以通过 kubelet 的启动参数进行修改

2. 不要手动清理镜像和容器,因为 kubelet 运行的时候会保存当前节点上镜像和容器的缓存,并定时更新。手动清理镜像和容器会让 kubelet 做出误判,带来不确定的问题

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容