在将 k8s 从 1.7.9 升级到 1.10.2 之后, 发现删除 pod 一直处于 terminating 状态, 调查发现删不掉的 pod 都有一个特点就是 pod yaml 中 command 部分写错了, 如下所示:
- apiVersion: v1
- kind: Pod
- metadata:
- name: bad-pod-termation-test
- spec:
- containers:
- - image: nginx
- command:
- - xxxx
- name: pad-pod-test
可以看到此时 pod 中的 command 为一个不存在的命令, 创建该 yaml 后会返回如下状态:
- % kubectl get pods
- NAME READY STATUS RESTARTS AGE
- bad-pod-termation-test 0/1 RunContainerError 0 20s
在宿主机上 docker ps -a 可以看到对应的 docker 是处于 Creted 状态的 (无法正常启动的状态), 因为 pod 起不来会重试, 所以有多个 docker 实例:
- CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
- b66c1a3de3ae nginx "xxxx" 9 seconds ago Created k8s_pad-pod-test_bad-pod-termation-test_default_7786ffea-7de9-11e8-9754-509a4c2d27d1_3
- 148a312b89cf nginx "xxxx" 43 seconds ago Created k8s_pad-pod-test_bad-pod-termation-test_default_7786ffea-7de9-11e8-9754-509a4c2d27d1_2
- 6414f874ffe0 k8s.gcr.io/pause-amd64:3.1 "/pause" About a minute ago Up About a minute k8s_POD_bad-pod-termation-test_default_7786ffea-7de9-11e8-9754-509a4c2d27d1_0
此时删除 pod 就会看到 pod 一直处于 termianting 状态, 只能用 kubectl delete pods bad-pod-termation-test --grace-period=0 --forece 强制删除, 但是强制删除是官方所不建议的, 可能会造成资源的泄露, 这种方案肯定不是长久之计.
调高 kubelet 的日志级别仔细查看发现 kubelet 一直输出一条可疑 log:
I0702 19:26:43.712496 26521 kubelet_pods.go:942] Pod "bad-pod-termation-test_default(9eae939b-7dea-11e8-9754-509a4c2d27d1)" is terminated, but some containers have not been cleaned up: [0xc4218d1260 0xc4228ae540]
也就是说 container 没删干净, kubelet 在等待 container 被删除. 上面 log 打印的是指针, 也就是存放 container 信息的变量地址, 不过可以猜出这就是 pod 对应的 container, 手动 docker rm 上面两个 created 状态的 container 之后, pod 马上就被删除不可见了, 怀疑 kubelet 本身存在某些 bug 导致 Created 状态的 container 某些资源无法释放, 为什么会这样呐?
查看代码发现 kubelet 会有一个 PodCache 来保存所有的 pod 信息, 每创建一个 pod 就会向其中添加一条记录, 且只有在 container 删除的时候才会将对应的 cache 清空, 对应的 cache 清空后才能删除 pod.
之前的环境中为了方便 debug 将 container 退出后的尸体都保存了下来, 在 kubelet 中设置 --minimum-container-ttl-duration=36h flag 来保存容器尸体, 该 flag 已经是 deprecated 状态了, 官方不建议使用, 建用 --eviction-hard 或 --eviction-soft 来代替, 因为在 1.7.9 中 minimum-container-ttl-duration 还是可以正常使用的, 倒也没在意 deprecated 的提醒, 并且在 1.10.2 也同样设置了该 flag, 导致 cache 无法清空, 进而无法删除 pod.
按照上面的分析只有删除了 container,pod 才可以删除, 那么设置了 flag minimum-container-ttl-duration 来保留 container 的后果岂不是所有的 pod 都无法删除吗? 为什么之前正常的 pod 可以被删除? 难道是正常 pod 的 container 尸体都被删了? 做了一下测试, 果然删除正常 pod 之后容器尸体立马也被删除, 设置 minimum-container-ttl-duration 压根没起作用, 但是对于上述 yaml 创建的异常 pod 反倒起作用了, Created 状态的 container, 直到 minimum-container-ttl-duration 之后才被删除.
虽然比较奇怪, 但在一个 deprecated 的 flag 上面发生任何问题都是可以原谅的, 官方已经明确声明不建议使用了, 只能去掉该 flag 避免问题的出现.下了 master 分支最新的代码重新编译试了一下, 版本如下, 发现无论设不设置该 flag, 都会立即删除 pod 的 container, 所以 pod pending 在 terminating 状态的问题就不存在了.
- % kubectl version
- Client Version: version.Info{Major:"1", Minor:"8", GitVersion:"v1.8.2", GitCommit:"bdaeafa71f6c7c04636251031f93464384d54963", GitTreeState:"clean", BuildDate:"2017-10-24T19:48:57Z", GoVersion:"go1.8.3", Compiler:"gc", Platform:"linux/amd64"}
- Server Version: version.Info{Major:"1", Minor:"10+", GitVersion:"v1.10.2-dirty", GitCommit:"81753b10df112992bf51bbc2c2f85208aad78335", GitTreeState:"dirty", BuildDate:"2018-07-02T11:03:02Z", GoVersion:"go1.9.7", Compiler:"gc", Platform:"linux/amd64"}
那现在就剩一个问题了: 在 1.7.9 的版本中设置了 minimum-container-ttl-duration 为什么可以正常删除 pod? 为什么能够既保留容器尸体又能删除 pod, 通过查阅源码发现 k8s 通过调用 PodResourcesAreReclaimed 来判断资源是否回收, 只有资源全部回收才可以删除 pod, 在 1.7.9 的实现代码如下, 依次判断是否有正在运行中的 pod,volume 是否清空, sandbox(也就是 pause) 容器是否清理:
- func (kl *Kubelet) PodResourcesAreReclaimed(pod *v1.Pod, status v1.PodStatus) bool {
- if !notRunning(status.ContainerStatuses) {
- // We shouldnt delete pods that still have running containers
- glog.V(3).Infof("Pod %q is terminated, but some containers are still running", format.Pod(pod))
- return false
- }
- if kl.podVolumesExist(pod.UID) && !kl.kubeletConfiguration.KeepTerminatedPodVolumes {
- // We shouldnt delete pods whose volumes have not been cleaned up if we are not keeping terminated pod volumes
- glog.V(3).Infof("Pod %q is terminated, but some volumes have not been cleaned up", format.Pod(pod))
- return false
- }
- if kl.kubeletConfiguration.CgroupsPerQOS {
- pcm := kl.containerManager.NewPodContainerManager()
- if pcm.Exists(pod) {
- glog.V(3).Infof("Pod %q is terminated, but pod cgroup sandbox has not been cleaned up", format.Pod(pod))
- return false
- }
- }
- return true
- }
而在 v1.10.2 中的实现如下:
- func (kl *Kubelet) PodResourcesAreReclaimed(pod *v1.Pod, status v1.PodStatus) bool {
- if !notRunning(status.ContainerStatuses) {
- // We shouldnt delete pods that still have running containers
- glog.V(3).Infof("Pod %q is terminated, but some containers are still running", format.Pod(pod))
- return false
- }
- // pod's containers should be deleted
- runtimeStatus, err := kl.podCache.Get(pod.UID)
- if err != nil {
- glog.V(3).Infof("Pod %q is terminated, Error getting runtimeStatus from the podCache: %s", format.Pod(pod), err)
- return false
- }
- if len(runtimeStatus.ContainerStatuses)> 0 {
- glog.V(3).Infof("Pod %q is terminated, but some containers have not been cleaned up: % v", format.Pod(pod), runtimeStatus.ContainerStatuses)
- return false
- }
- if kl.podVolumesExist(pod.UID) && !kl.keepTerminatedPodVolumes {
- // We shouldnt delete pods whose volumes have not been cleaned up if we are not keeping terminated pod volumes
- glog.V(3).Infof("Pod %q is terminated, but some volumes have not been cleaned up", format.Pod(pod))
- return false
- }
- if kl.kubeletConfiguration.CgroupsPerQOS {
- pcm := kl.containerManager.NewPodContainerManager()
- if pcm.Exists(pod) {
- glog.V(3).Infof("Pod %q is terminated, but pod cgroup sandbox has not been cleaned up", format.Pod(pod))
- return false
- }
- }
- return true
- }
可以看出 1.7.9 中资源回收的逻辑与 1.10.2 中的不太一样, v1.10.2 增加了判断 cache 是否为空的逻辑, 上面说过只有在容器被删除之后才清空 cache,1.7.9 中设置了 minimum-container-ttl-duration 之后不会清理退出的 container 尸体, 所以 cache 也未清空, 其实在这种情况下是存在资源泄露的. 为了验证这个结论, 专门在 1.7.9 的 PodResourcesAreReclaimed method 中也加入了 cache 是否为空的判断逻辑, 果然出现了一直 pending 在 terminating 状态的情况.
回到我们设置 minimum-container-ttl-duration flag 的初衷: container 退出后保留信息方便 debug, 回溯状态, 那如果不使用这个 flag 该怎么办哪? 去哪里找逝去的信息? 官方的文档中对 minimum-container-ttl-duration 有句描述是`deprecated once old logs are stored outside of container's context`, 将来可能会将 log 保存到容器外面, 但是目前显然是没有实现的. 另外做了几次实验之后发现只要不手动删除 pod, 对应的 container 尸体就会一直保存下来, 如果有多个退出实例尸体, 不会每个实例都保存, 但至少会保存一个退出实例, 可以用来 debug. 反过来思考, 如果保存每个退出实例, 其实是将容器运行的上下文都保存下来了, 如果一个 container 在 writable layer 写入大量的数据的话, 会导致占用大量磁盘空间而不能释放, 所以尽量不要保存太多退出实例, 官方的保留的退出实例个数一般情况下 debug 就够用了, 对于额外信息的保存就需要通过远程备份的方式来实现了.
来源: https://www.cnblogs.com/gaorong/p/9275795.html