Z.S.K.'s Records

Kubernetes学习(为什么Pod突然就不见了?)

最近发生一件很诡异的事情, 某个ns下的pods会莫名其妙地被删了, 困扰了好一阵子,排查后发现问题的起因还是挺有意思。

问题现象

交代一下背景, 这些pod都是由argo-workflow发起的pod, 执行完特定的任务之后就会变成Succeeded, 如果执行时有问题,状态可能是Failed.

结果很直接,就是在ns下的某些状态为 Failed pod会被删掉(后来证实Succeeded状态的也会被删掉),所以会出现尴尬的情况是想找这个pod的时候,发现这个pod却没了,之前反映过类似的问题,但一直以为是被别人删了,没有在意,但是第二次出现,感觉不是偶然

开发同学肯定没权限做这个事,运维侧也可以肯定没有这类操作,排查了一圈几乎可以肯定的是,不是人为的, 那不是人做的,就只能中k8s这边的某些机制触发了这个删除的操作,kubernetes可以管理千千万万的pod资源,因此gc机制是必不可少的,作者也是第一时间想到了可能是gc机制引起的.

在详细追踪k8s的podGC问题之前,其实还有一个嫌疑犯需要排查,那就是argo-workflow, argo-workflow做为一种任务workflow的实现方式,argo-workflow本身也可以通过CRD来检测当workflow执行到达什么状态时进行podGC, 如下图:

但作者可以肯定的是,那些被删除的pod中并未使用argo-workflow的podGC,因此argo-workflow的嫌疑可以排除.

那么现在就剩k8s本身的机制了

PodGC

k8s中存在在各种各样的controller(感兴趣的可以看看controllermanager.go中的NewControllerInitializers中列出来的controllers对象), 每一个controller专注于解决一个方面的问题, podGC controller也是如此,专门回收pod。

既然pod被回收了,是不是可以从controllermanager的日志中看到什么呢?果然

从上面的日志也可以证实,pod确实是controller被回收了,但是怎么个回收法呢?依据是什么,时间间隔多久等等一系列问题相继涌出

gc_controller.go

源码能够得到一切答案,大多数都来自于pkg/controller/podgc/gc_controller.go

1
2
3
4
5
6
7
const (
// gcCheckPeriod defines frequency of running main controller loop
gcCheckPeriod = 20 * time.Second
// quarantineTime defines how long Orphaned GC waits for nodes to show up
// in an informer before issuing a GET call to check if they are truly gone
quarantineTime = 40 * time.Second
)

首先是gc的时间间隔,很显然是20s,而且这个数值不支持从命令参数中配置

quarantineTime是在删除孤儿pod时等待节点ready前的时间

那根据什么删除的呢, 同样,在源码中给了答案

pod.status.phase

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
func (gcc *PodGCController) gcTerminated(pods []*v1.Pod) {
terminatedPods := []*v1.Pod{}
for _, pod := range pods {
if isPodTerminated(pod) {
terminatedPods = append(terminatedPods, pod)
}
}

terminatedPodCount := len(terminatedPods)
deleteCount := terminatedPodCount - gcc.terminatedPodThreshold

if deleteCount > terminatedPodCount {
deleteCount = terminatedPodCount
}
if deleteCount <= 0 {
return
}

klog.Infof("garbage collecting %v pods", deleteCount)
// sort only when necessary
sort.Sort(byCreationTimestamp(terminatedPods))
var wait sync.WaitGroup
for i := 0; i < deleteCount; i++ {
wait.Add(1)
go func(namespace string, name string) {
defer wait.Done()
if err := gcc.deletePod(namespace, name); err != nil {
// ignore not founds
defer utilruntime.HandleError(err)
}
}(terminatedPods[i].Namespace, terminatedPods[i].Name)
}
wait.Wait()
}

这里的日志输出刚好也是controllermanager.go中的日志输出,主要的逻辑在如何判定一个pod是否需要被删除

1
2
3
4
5
6
func isPodTerminated(pod *v1.Pod) bool {
if phase := pod.Status.Phase; phase != v1.PodPending && phase != v1.PodRunning && phase != v1.PodUnknown {
return true
}
return false
}

判断一个pod是否需要被删除,主要看一个pod的状态,在k8s,一个pod大概会有以下的状态(phases)

  • Pending
  • Running
  • Succeeded
  • Failed
  • Unknown

得到所有的pods实例,对于status.phase不等于Pending、Running、Unknown的且与terminatedPodThreshold的差值的部分的pod进行清除,会对要删除的pod的创建时间戳进行排序后删除差值个数的pod,注意这里也会把succeeded的状态pod给删除,作者对这个把succeeded状态的pod给gc了还是比较奇怪的

gcOrphaned

另外,回收那些Binded的Nodes已经不存在的pods,这个没什么好说的,node都不存在了,pod也没存在的必要了

逻辑是调用apiserver接口,获取所有的Nodes,然后遍历所有pods,如果pod bind的NodeName不为空且不包含在刚刚获取的所有Nodes中,最后串行逐个调用gcc.deletePod删除对应的pod

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
func (gcc *PodGCController) gcOrphaned(pods []*v1.Pod, nodes []*v1.Node) {
klog.V(4).Infof("GC'ing orphaned")
existingNodeNames := sets.NewString()
for _, node := range nodes {
existingNodeNames.Insert(node.Name)
}
// Add newly found unknown nodes to quarantine
for _, pod := range pods {
if pod.Spec.NodeName != "" && !existingNodeNames.Has(pod.Spec.NodeName) {
gcc.nodeQueue.AddAfter(pod.Spec.NodeName, quarantineTime)
}
}
// Check if nodes are still missing after quarantine period
deletedNodesNames, quit := gcc.discoverDeletedNodes(existingNodeNames)
if quit {
return
}
// Delete orphaned pods
for _, pod := range pods {
if !deletedNodesNames.Has(pod.Spec.NodeName) {
continue
}
klog.V(2).Infof("Found orphaned Pod %v/%v assigned to the Node %v. Deleting.", pod.Namespace, pod.Name, pod.Spec.NodeName)
if err := gcc.deletePod(pod.Namespace, pod.Name); err != nil {
utilruntime.HandleError(err)
} else {
klog.V(0).Infof("Forced deletion of orphaned Pod %v/%v succeeded", pod.Namespace, pod.Name)
}
}
}

gcUnscheduledTerminating

另外,回收Unscheduled并且Terminating的pods,逻辑是遍历所有pods,过滤那些terminating(pod.DeletionTimestamp != nil)并且未调度成功的(pod.Spec.NodeName为空)的pods, 然后串行逐个调用gcc.deletePod删除对应的pod

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func (gcc *PodGCController) gcUnscheduledTerminating(pods []*v1.Pod) {
klog.V(4).Infof("GC'ing unscheduled pods which are terminating.")

for _, pod := range pods {
if pod.DeletionTimestamp == nil || len(pod.Spec.NodeName) > 0 {
continue
}

klog.V(2).Infof("Found unscheduled terminating Pod %v/%v not assigned to any Node. Deleting.", pod.Namespace, pod.Name)
if err := gcc.deletePod(pod.Namespace, pod.Name); err != nil {
utilruntime.HandleError(err)
} else {
klog.V(0).Infof("Forced deletion of unscheduled terminating Pod %v/%v succeeded", pod.Namespace, pod.Name)
}
}
}

disable podGC controller

Podgc 是不是可以配置呢?

很遗憾的是,配置项不是很多,可以定义是否开启podgc controller

controller-manager的启动参数中有个参数:

1
2
--terminated-pod-gc-threshold int32     Default: 12500
Number of terminated pods that can exist before the terminated pod garbage collector starts deleting terminated pods. If <= 0, the terminated pod garbage collector is disabled.

这个参数指的是在pod gc前可以保留多少个terminated pods, 默认是12500个,这个数值还是挺大的,一般集群怕是很难能到,作者由于是训练集群,存在着大量的短时间任务,因此会出现大于该值的pod,当该值小于等于0时,相当于不对terminated pods进行删除,但还是会对孤儿pod及处于terminating状态且没有绑定到node的pod进行清除.

参考: https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/

作者只查到这一个跟podgc相关的参数,目测好像在不修改controllermanager的情况下是没办法直接禁用podgc

到此,真相大白:

同时也给作者纠正了一个错误, 不是只有Failed状态的pod才会被gc,Successed状态的pod也会被gc掉,这个出乎作者意料之外

最后,想说的是,podgc跟k8s中的垃圾回收还不是一回事,虽然他们都是以controller运行,

podgc解决的是pod到达gc的条件后会被delete掉.

而garbage则解决的是对节点上的无用镜像和容器的清除

从k8s的源码也能够看出来这两者的不同.

参考文章:

转载请注明原作者: 周淑科(https://izsk.me)

 wechat
Scan Me To Read on Phone
I know you won't do this,but what if you did?