Kubernetes Pod 删除操作源码解析
作者 阳明
来源 | k8s技术圈(ID:kube100)
比如现在我有一个更新策略为 Recreate 的应用,然后执行删除命令,如下所示:
☸ ➜ kubectl get pods
NAME READY STATUS RESTARTS AGE
minio-875749785-sv5ns 1/1 Running 1 (2m52s ago) 42h
☸ ➜ kubectl delete pod minio-875749785-sv5ns
pod "minio-875749785-sv5ns" deleted
在删除之前在另外一个终端观察应用状态:
☸ ➜ kubectl get pods -w
NAME READY STATUS RESTARTS AGE
minio-875749785-sv5ns 1/1 Running 1 (2m46s ago) 42h
minio-875749785-sv5ns 1/1 Terminating 1 (2m57s ago) 42h
minio-875749785-h2j2b 0/1 Pending 0 0s
minio-875749785-h2j2b 0/1 Pending 0 0s
minio-875749785-h2j2b 0/1 ContainerCreating 0 0s
minio-875749785-sv5ns 0/1 Terminating 1 (2m59s ago) 42h
minio-875749785-sv5ns 0/1 Terminating 1 (2m59s ago) 42h
minio-875749785-sv5ns 0/1 Terminating 1 (2m59s ago) 42h
minio-875749785-h2j2b 0/1 Running 0 17s
minio-875749785-h2j2b 1/1 Running 0 30s
从上面的过程可以看到当我们执行 kubectl delete 命令后 Pod 变成了 Terminating 状态,然后才消失。接下来我们会从代码角度来介绍下删除 Pod 的整体流程。
这里我们以 v1.22.8 版本的 Kubernetes 为例进行说明,其他版本不保证代码完全一致,但是整体思路是一致的。
删除状态
我们可以根据 kubectl 操作后看到的状态来进行跟踪,上面的格式化结果是通过代码 https://github.com/kubernetes/kubernetes/blob/v1.22.8/pkg/printers/internalversion/printers.go#L88-L102 实现的,如下所示:
对于 Pod 的输出结果是通过 printPod 函数获取的,代码位于:https://github.com/kubernetes/kubernetes/blob/v1.22.8/pkg/printers/internalversion/printers.go#L756-L840,其中有一段代码提到了 Terminating 值,是在 pod.DeletionTimestamp != nil 的情况下变成该状态的,如下所示:
也就是说当执行删除操作的时候,会设置 Pod 的 DeletionTimestamp 属性,这个时候就会显示成 Terminating 状态。
当执行删除操作的时候,会向 apiserver 发送一次 DELETE 请求:
I0408 11:25:33.002155 42938 round_trippers.go:435] curl -v -XDELETE -H "Content-Type: application/json" -H "User-Agent: kubectl/v1.22.7 (darwin/amd64) kubernetes/b56e432" -H "Accept: application/json" 'https://192.168.0.111:6443/api/v1/namespaces/default/pods/minio-875749785-sv5ns'
I0408 11:25:33.037245 42938 round_trippers.go:454] DELETE https://192.168.0.111:6443/api/v1/namespaces/default/pods/minio-875749785-sv5ns 200 OK in 35 milliseconds
接收到删除请求的处理器位于代码 https://github.com/kubernetes/kubernetes/blob/v1.22.8/staging/src/k8s.io/apiserver/pkg/registry/generic/registry/store.go#L986,如下所示:
在 BeforeDelete 函数中判断是否需要优雅删除,判断的标准是 DeletionGracePeriodSeconds 值是否为 0,不为零则认为是优雅删除,apiserver 不会立即将这个对象从 etcd 中删除,否则直接删除。对于 Pod 而言,默认 DeletionGracePeriodSeconds 为 30 秒,因此这里不会被立刻删除掉,而是将 DeletionTimestamp 设置为当前时间,DeletionGracePeriodSeconds 设置为默认值 30 秒。代码位于 https://github.com/kubernetes/kubernetes/blob/v1.22.8/staging/src/k8s.io/apiserver/pkg/registry/rest/delete.go#L93-L159,在该函数中会设置 DeletionTimestamp 的值,如下所示:
上面的代码验证了当执行删除操作的时候,apiserver 会先设置 Pod 的 DeletionTimestamp 属性为当前时间加上优雅删除宽限时长的时间点,设置了该属性后,我们客户端格式化过后看到的就是 Terminating 状态了。
优雅删除
由于 Pod 中涉及到其他很多资源,比如 sandbox 容器、volume 卷等等,在删除后都需要进行回收,而删除 Pod 最终也是去删除对应的容器,这个就需要 Pod 所在节点的 kubelet 来完成清理了。kubelet 首先同样会一直 watch 我们的 Pod,当 Pod 的删除时间更新后,自然就会接收到事件,然后进行相应的清理工作。
kubelet 对 Pod 的处理主要在 syncLoop 函数中,会去调用和事件相关的处理函数 syncLoopIteration,代码位于 https://github.com/kubernetes/kubernetes/blob/v1.22.8/pkg/kubelet/kubelet.go#L2040-L2079 中,如下所示:
当执行删除操作的时候,apiserver 首先会更新 Pod 中的 DeletionTimestamp 属性,这个改变对于 kubelet 来说属于更新操作,所以会对应 kubetypes.UPDATE 操作,会调用 HandlePodUpdates 函数进行更新。
在 HandlePodUpdates 中会调用 dispatchWork 将 Pod 删除分配给具体的 worker 处理,podWorker 是具体的执行者,也就是每次 Pod 需要更新都会发送给 podWorker。
dispatchWork 方法会调用 UpdatePod 函数对 Pod 进行删除,代码位于 https://github.com/kubernetes/kubernetes/blob/v1.22.8/pkg/kubelet/pod_workers.go#L540-L765,在该函数中会通过一个 channel 传递 Pod 信息,在一个 goroutine 中调用 managePodLoop 函数进行处理,该函数中会调用 syncTerminatingPod/syncPod 方法来进行删除操作。
最终都会调用 killPod 函数去执行删除 Pod:
killPod 函数中会调用容器运行时去停止该 Pod 中的容器,代码位于https://github.com/kubernetes/kubernetes/blob/v1.22.8/pkg/kubelet/kubelet_pods.go#L856-L868:
容器运行时的 KillPod 方法位于 https://github.com/kubernetes/kubernetes/blob/v1.22.8/pkg/kubelet/kuberuntime/kuberuntime_manager.go#L969-L998,如下所示:
killPodWithSyncResult 方法中首先调用函数 killContainersWithSyncResult 杀掉所有运行的容器,然后删除 Pod 的 sandbox。
在该函数中,利用多个 goroutine 来对 Pod 中的每一个容器进行删除,删除容器的方法是 killContainer,在该函数中首先会执行 pre-stop 这个 hooks(如果存在的话),然后才停止容器,代码位于 https://github.com/kubernetes/kubernetes/blob/v1.22.8/pkg/kubelet/kuberuntime/kuberuntime_container.go#L660-L736。
首先获取优雅删除的宽限时间:
其中 TerminationGracePeriodSeconds 可以在资源清单文件中进行设置,默认为 30 秒,这个时间是,给 Pod 发出关闭指令后会给应用发送 SIGTERM 信号,程序只需要捕获 SIGTERM 信号并做相应处理即可。也就是 Pod 接收到 SIGTERM 信号后,应用能够优雅关闭的时间。该时间是由 apiserver 设置的,前面已经分析过。
如果配置了 pre-stop hook 并且还有足够的时间,则会执行该 hook,pre-stop 主要是为了业务在容器删除前前,能够优雅的停止,比如资源回收等操作:
最后才会真正去调用底层容器运行时来停止容器:
容器删掉后回到前面的 killPodWithSyncResult 函数中,接下来就会去调用运行时服务的 StopPodSandbox 函数停止 sandbox 容器,也就是 pause 容器。
// Stop all sandboxes belongs to same pod
for _, podSandbox := range runningPod.Sandboxes {
if err := m.runtimeService.StopPodSandbox(podSandbox.ID.ID); err != nil && !crierror.IsNotFound(err) {
killSandboxResult.Fail(kubecontainer.ErrKillPodSandbox, err.Error())
klog.ErrorS(nil, "Failed to stop sandbox", "podSandboxID", podSandbox.ID)
}
}
到这里 kubelet 就完成了对 Pod 的优雅删除,但是这并没有结束。
同步状态
对于优雅删除一开始在 apiserver 只是给 Pod 设置了 DeletionTimestamp 属性,然后 kubelet watch 来更新后去完成了 Pod 的优雅删除,但是现在服务端中还有 Pod 的记录,并没有真正去删除。
在 kubelet 启动的时候同时还去启动了一个 statusManager 的同步循环,该 Manager 是 kubelet pod 状态的真实来源,应该与最新的 v1.PodStatus 保持同步,它还将更新同步回 apiserver,也就是当优雅删除完成后我们还将通过该管理器将状态同步回 apiserver。
状态管理器在与 apiserver 进行状态同步的时候会去调用该管理器下面的 syncPod 方法进行处理,代码位于 https://github.com/kubernetes/kubernetes/blob/v1.22.8/pkg/kubelet/status/status_manager.go#L149-L181,如下所示:
在该方法中会判断 Pod 是否已经优雅停止了,代码位于 https://github.com/kubernetes/kubernetes/blob/v1.22.8/pkg/kubelet/status/status_manager.go#L583-L652,如下所示:
比如会判断是否还有容器在运行、volumes 是否还没有清理、pod cgroup 还没清空等等,如果 canBeDeleted 返回 true,则表示 pod 已经优雅的停止了,那么这个时候就可以向 apiserver 发送 Delete 请求,再次删除 Pod 了。
不过这一次的设置的 GracePeriodSeconds 为 0,表示要强制删除 Pod 了,到这里 apiserver 会再次收到 DELETE 请求,与第一次不同的是,这次是强制删除 Pod,会去 etcd 中删除 Pod 对象了。
这个时候 kubelet 会接受到 REMOVE 的事件,调用 HandlePodRemoves 函数去进行处理:
首先会去调用 deletePod 函数去停掉关联的 pod worker,然后还会调用 probeManager 去移除 Pod 相关的探针 prober worker,到这里就表示 Pod 彻底从节点上删除了。