Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >【Pod Terminating原因追踪系列之三】让docker事件处理罢工的cancel状态码

【Pod Terminating原因追踪系列之三】让docker事件处理罢工的cancel状态码

作者头像
腾讯云原生
修改于 2020-09-02 02:21:13
修改于 2020-09-02 02:21:13
2.1K0
举报

岳家瑞,腾讯云后台开发工程师,日常负责K8s生态和运行时相关工作,包括K8s插件开发和运行时问题排查。

本篇为Pod Terminating原因追踪系列的第三篇,前两篇【Pod Terminating原因追踪系列】之 containerd 中被漏掉的 runc 错误信息【Pod Terminating原因追踪系列之二】exec连接未关闭导致的事件阻塞,分别介绍了两种可能导致Pod Terminating的原因。在处理现网问题时,Pod Terminating属于比较常见的问题,而本系列的初衷便是记录导致Pod Terminating问题的原因,希望能够帮助大家在遇到此类问题时,开拓排查思路。

本篇将再介绍一种造成Pod Terminating的原因,即处理事件流的方法异常退出导致的Pod Terminating,docker版本在19以下且containerd进程由于各种原因(比如OOM)频繁重启时,会有概率导致此问题产生对于本文中提到的问题,在docker19中已经得到解决,但由于docker18无法直接升级到docker19,且dockerd19修复的改动较大,难以cherry-pick到docker18,因此本文在结尾参考docker19的实现给出了一种简单的解决方案。

Pod Terminating

前一阵有客户反馈使用docker18版本的节点上Pod一直处在Terminating状态,客户通过查看kubelet日志怀疑是Volume卸载失败导致的。现象如下图:

代码语言:txt
AI代码解释
复制
Jul 31 09:53:52 VM_90_48_centos kubelet: E0731 09:53:52.860699     702 plugin_watcher.go:120] error could not find plugin for deleted file /var/lib/kubelet/plugins/kubernetes.io/qcloud-cbs/mounts/disk-o3yxvywa/WTEST.TMP when handling delete event: "/var/lib/kubelet/plugins/kubernetes.io/qcloud-cbs/mounts/disk-o3yxvywa/WTEST.TMP": REMOVEJul 31 09:53:52 VM_90_48_centos kubelet: E0731 09:53:52.860717     702 plugin_watcher.go:115] error stat file /var/lib/kubelet/plugins/kubernetes.io/qcloud-cbs/mounts/disk-o3yxvywa/WTEST.TMP failed: stat /var/lib/kubelet/plugins/kubernetes.io/qcloud-cbs/mounts/disk-o3yxvywa/WTEST.TMP: no such file or directory when handling create event: "/var/lib/kubelet/plugins/kubernetes.io/qcloud-cbs/mounts/disk-o3yxvywa/WTEST.TMP": CREATE

通过查看客户Pod的部署情况,发现客户同时使用了in-tree和out-tree的方式挂载cbs,kubelet中的报错是因为在in-tree中检测到了来自out-tree的旁路信息而报错,本质上并不会造成Pod Terminating不掉的问题,看来造成Pod Terminating的原因并非这么简单。

分析日志及源码

在排除了cbs卸载的问题后,我们首先想到会不会还是dockerd和containerd状态不一致的问题呢?通过下面两个指令查看了一下容器和task的状态,发现容器的状态是up而task的状态为STOPPED,果然又是状态不一致导致的问题。按照前两篇的经验来看应该是来自containerd的事件在dockerd中没有得到处理或处理的过程阻塞了。

代码语言:txt
AI代码解释
复制
#查看容器状态,看到容器状态为updocker ps | grep <container-id>#查看task状态,显示task的状态为STOPPEDdocker-container-ctr --namespace moby --address var/run/docker/containerd/docker-containerd.sock task ls | grep <container-id>

这里提供一种简单验证方法来验证是否为task事件没有得到处理造成的Pod Terminating,随便起一个容器(例如CentOS),并通过exec进入容器并退出,这时去查看docker的堆栈(发送SIGUSR1信号给dockerd),如果发现如下有一条堆栈信息:

代码语言:txt
AI代码解释
复制
goroutine 10717529 [select, 16 minutes]:github.com/docker/docker/daemon.(*Daemon).ContainerExecStart(0xc4202b2000, 0x25df8e0, 0xc42010e040, 0xc4347904f1, 0x40, 0x7f7ea8250648, 0xc43240a5c0, 0x7f7ea82505e0, 0xc43240a5c0, 0x0, ...)    /go/src/github.com/docker/docker/daemon/exec.go:264 +0xcb6github.com/docker/docker/api/server/router/container.(*containerRouter).postContainerExecStart(0xc421069b00, 0x25df960, 0xc464e089f0, 0x25dde20, 0xc446f3a1c0, 0xc42b704000, 0xc464e08960, 0x0, 0x0)    /go/src/github.com/docker/docker/api/server/router/container/exec.go:125 +0x34b

之后可以使用《【Pod Terminating原因追踪系列之二】exec连接未关闭导致的事件阻塞》中介绍的方法,确认一下该条堆栈信息是否是刚刚创建的CentOS容器产生的,当然从堆栈的时间上来看很容易看出来,也可以通过gdb判断ContainerExecStart参数(第二个参数的地址)中的execID是否和CentOS容器的execID相等的方式来确认,通过返回结果发现exexID相等,说明虽然我们的exec退出了,但是dockerd却没有正确处理来自containerd的exit事件。

在有了之前的排查经验后,我们很快猜到会不会是处理事件流的方法processEventStream在处理exit事件的时候发生了阻塞?验证方法很简单,只需要查看堆栈有没有goroutine卡在StreamConfig.Wait()即可,通过搜索processEventStream堆栈信息发现并没有goroutine卡在Wait方法上,甚至连processEventStream这个处理事件流的方法在堆栈都中也没有找到,说明事件处理的方法已经return了!自然也就无法处理来自containerd的所有事件了。

那么造成processEventStream方法return的具体原因是什么呢?通过查看源码发现,processEventStream中只有在一种情况下会return,即当gRPC连接返回的错误能够被解析(ok为true)且返回cancel状态码的时候proceEventStream会return,否则会另起协程递归调用proceEventStream:

代码语言:txt
AI代码解释
复制
case err = <-errC:    if err != nil {        errStatus, ok := status.FromError(err)        if !ok || errStatus.Code() != codes.Canceled {            c.logger.WithError(err).Error("failed to get event")            go c.processEventStream(ctx)        } else {            c.logger.WithError(ctx.Err()).Info("stopping event stream following graceful shutdown")        }    }    return

那么为什么gRPC连接会返回cancel状态码呢?

在查看客户docker日志时发现containerd在之前不断的被kill并重启,持续了大概11分钟左右:

代码语言:txt
AI代码解释
复制
#日志省略了部分内容Jul 29 19:23:09 VM_90_48_centos dockerd[11182]: time="2020-07-29T19:23:09.037480352+08:00" level=error msg="containerd did not exit successfully" error="signal: killed" module=libcontainerdJul 29 19:24:06 VM_90_48_centos dockerd[11182]: time="2020-07-29T19:24:06.972243079+08:00" level=info msg="starting containerd" revision=e6b3f5632f50dbc4e9cb6288d911bf4f5e95b18e version=v1.2.4Jul 29 19:24:52 VM_90_48_centos dockerd[11182]: time="2020-07-29T19:24:52.643738767+08:00" level=error msg="containerd did not exit successfully" error="signal: killed" module=libcontainerdJul 29 19:25:02 VM_90_48_centos dockerd[11182]: time="2020-07-29T19:25:02.116798771+08:00" level=info msg="starting containerd" revision=e6b3f5632f50dbc4e9cb6288d911bf4f5e95b18e version=v1.2.4

查看系统日志文件(/var/log/messages)看下为什么containerd会被不断地重启:

代码语言:txt
AI代码解释
复制
#日志省略了部分内容Jul 29 19:23:09 VM_90_48_centos kernel: Memory cgroup out of memory: Kill process 15069 (docker-containe) score 0 or sacrifice childJul 29 19:23:09 VM_90_48_centos kernel: Killed process 15069 (docker-containe) total-vm:51688kB, anon-rss:10068kB, file-rss:324kBJul 29 19:24:52 VM_90_48_centos kernel: Memory cgroup out of memory: Kill process 12411 (docker-containe) score 0 or sacrifice childJul 29 19:24:52 VM_90_48_centos kernel: Killed process 5836 (docker-containe) total-vm:1971688kB, anon-rss:22376kB, file-rss:0kB

可以发现containerd被kill是由于OOM导致的,那么会不会是因为containerd的不断重启导致gRPC返回cancel的状态码呢。先查看一下重启containerd这部分的逻辑:

在启动dockerd时,会创建一个独立的到containerd的gRPC连接,并启动一个monitor协程基于该gRPC连接对containerd的服务做健康检查,monitor每隔500ms会对到containerd的grpc连接做健康检查并记录失败的次数,如果发现gRPC连接返回状态码为UNKNOWN或者NOT_SERVING时对失败次数加1,当失败次数大于域值(域值为3)并且containerd进程已经down掉(通过向进程发送信号进行判断),则会重启containerd进程,并执行reconnect重置dockerd和containerd之间的gRPC连接,在reconnect的逻辑中,会先close旧的gRPC连接,之后新建一条新的gRPC连接:

代码语言:txt
AI代码解释
复制
// containerd/containerd/client.gofunc (c *Client) Reconnect() error {    ....    // close掉旧的连接    c.conn.Close()    // 建立新的连接    conn, err := c.connector()    ....    c.conn = conn    return nil}connector := func() (*grpc.ClientConn, error) {    ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)    defer cancel()    conn, err := grpc.DialContext(ctx, dialer.DialAddress(address), gopts...)    if err != nil {        return nil, errors.Wrapf(err, "failed to dial %q", address)    }    return conn, nil}

由于reconnect会先close旧连接,那么会不会是close造成的gRPC返回cancel呢?可以写一个简单的demo验证一下,服务端和客户端之间通过unix socket连接,客户端订阅服务端的消息,服务端不断地publish消息给客户端,客户端每隔一段时间close一次gRPC连接,得到的结果如下:

从结果中发现在unix socket下客户端close连接是有概率导致grpc返回cancel状态码的,那么具体什么情况下会产生cancel状态码呢?通过查看gRPC源码发现,当服务端在发送事件过程中,客户端close了连接则会使服务端返回cancel状态码,若此时服务端没有发送事件,则会返回图中的transport is closing错误。

至此,问题已经基本定位了,很有可能是客户端close了gRPC连接导致服务端返回了cancel状态码,使processEventStream方法return,导致来自containerd的事件流无法得到处理,最终导致dockerd和containerd的状态不一致。但由于客户的日志级别较高,我们没法从中获得问题产生时的具体时序,因此希望通过调低日志级别复现问题来定位具体在什么情况下会产生这个问题。

问题复现

这个问题复现起来比较简单,只需要模仿客户产生问题时的情况,不断重启containerd进程即可。在docker18.06.3-ce版本集群下创建一个Pod,我们通过下面的脚本不断kill containerd进程:

代码语言:txt
AI代码解释
复制
#!/bin/bash  for i in $(seq 1 1000)doprocess=`ps -elf | grep  "docker-containerd --config /var/run/docker/containerd/containerd.toml"| grep -v "grep"|awk '{print $4}'`if [ ! -n "$process" ]; then  echo "containerd not running"  else  echo $process;  kill -9 $process;fisleep 1;done

运行上面的脚本便有几率复现该问题,之后删除Pod并查看Pod状态,发现Pod会一直卡在Terminating状态。

查看容器状态和task状态,发现和客户问题的现象完全一致:

由于我们调低了日志级别,查看日志发现下面这样一条日志,而这条日志只有processEventStream方法return时才会打印,且打印日志后processEventStream方法立即return,因此可以确定问题的根本原因就是processEventStream收到了gRPC返回的cancel状态码导致方法return,之后的来自containerd的事件无法得到处理,最终出现dockerd和containerd状态不一致的问题。

代码语言:txt
AI代码解释
复制
Aug 13 15:23:16 VM_1_6_centos dockerd[25650]: time="2020-08-13T15:23:16.686289836+08:00" level=info msg="stopping event stream following graceful shutdown" error="<nil>" module=libcontainerd namespace=moby

问题定位

通过分析docker日志,可以了解到docker18具体在什么情况下会产生processEventStream return的问题,下图是会导致processEventStream return的时序图:

通过该时序图能够看出问题所在,首先当containerd进程被kill后,monitor通过健康检查,发现containerd进程已经停止,便会通过cmd重新启动containerd进程,并重新连接到contaienrd,如果processEventStream在reconnect之前使用旧的gRPC连接成功,订阅到containerd的事件,那么在reconnect时会close这条旧连接,而如果恰好在这时containerd在传输事件,那么该gRPC连接就会返回一个cancel的状态码给processEventStream方法,导致processEventStream方法return。

修复与反思

此问题产生的根本原因在于reconnect的逻辑,在重启时无法保证reconnect一定在processEventStream的subscribe之前发生。由于processEventStream会递归调用自动重连,因此实际上并不需要reconnect,在docker19中也已经修复了这个问题,且没有reconnect,但是docker19这部分改动较大,无法cherry-pick到docker18,因此我们可以参考docker19的实现修改docker18代码,只需要将reconnect的逻辑去除即可。

另外在修复时顺便修复了processEventStream方法不断递归导致瞬间产生大量日志的问题,由于subscribe失败以后会不断地启动协程递归调用,因此会在瞬间产生大量日志,在社区也有人已经提交过PR解决这个问题。(https://github.com/moby/moby/pull/39513

解决办法也很简单,在每次递归调用之前sleep 1秒即可,该改动也已经合进了docker19的代码中。

在后续我们将推出产品化运行时版本升级修复本篇中提到的bug,用户可以在控制台看到升级提醒并方便的进行一键升级。

希望本篇文章对您有帮助,谢谢观看!

扫描二维码,添加小助手(微信号:TKEplatform) ,拉你入技术交流群,和更多小伙伴一起交流云原生

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-08-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 腾讯云原生 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
【Pod Terminating原因追踪系列之三】让docker事件处理罢工的cancel状态码
岳家瑞,腾讯云后台开发工程师,日常负责K8s生态和运行时相关工作,包括K8s插件开发和运行时问题排查。
腾讯云原生
2020/08/31
2.1K0
【Pod Terminating原因追踪系列】之 containerd 中被漏掉的 runc 错误信息
李志宇,腾讯云后台开发工程师,日常负责集群节点和运行时相关的工作,以及 containerd、docker、runc 等运行时组件的定制开发和问题排查。
腾讯云原生
2020/08/17
4.9K0
Pod 一直停留在 Terminating 状态,我等得花儿都谢了~
近期,弹性云线上集群发生了几起特殊的容器漂移失败事件,其特殊之处在于容器处于 Pod Terminating 状态,而宿主则处于 Ready 状态。
米开朗基杨
2020/11/30
1.5K0
Pod 一直停留在 Terminating 状态,我等得花儿都谢了~
Dockerd 资源泄露如何处理?通过现象看问题的本质
线上 k8s 集群报警,宿主 fd 利用率超过 80%,登陆查看 dockerd 内存使用 26G
二哥聊运营工具
2021/12/17
7530
Dockerd 资源泄露如何处理?通过现象看问题的本质
Docker EOF问题排查
某天接到客户反馈,pod的事件中出现大量的 warning event: Readiness probe failed: OCI runtime exec failed: exec failed: EOF: unknown。但不影响客户访问该服务。
没有故事的陈师傅
2021/06/24
5.1K0
Docker EOF问题排查
Dockerd资源泄露系列 - 内存&FD泄露 - 1
线上部分宿主机dockerd占用内存过大,有的甚至超过100G,而整个宿主上的容器使用的内存还不如dockerd一个进程使用的多,现在的处理办法是故障自愈,检测到dockerd使用内存超过10G后会设置live-restore,然后重启dockerd,而不影响正常运行的容器,但是重启后还一直存在内存泄露的问题。可以总结为两类内存泄露情况:没有设置live-restore: true的和设置了live-restore: true且重启过dockerd的,这里是针对后者的排查,因为线上默认dockerd没有开启debug模式,要想排查前者的问题,就需要重启docker,又因为没有配置live-restore: true,就会影响到正在运行的容器。
我是阳明
2021/04/26
2.5K0
Dockerd资源泄露系列 - 内存&FD泄露 - 1
Kubernetes 问题排查: Pod 状态一直 Terminating
怀疑是17版本dockerd的BUG。可通过 kubectl -n cn-staging delete pod apigateway-6dc48bf8b6-clcwk --force --grace-period=0 强制删除pod,但 docker ps 仍看得到这个容器
imroc
2019/06/20
9.5K0
Pod疑难杂症(1):由于云盘吞吐过高导致pod创建超时
在创建TKE集群的worker node时,用户往往会单独再购买一块云盘,绑到节点上,用于docker目录挂载所用(将docker目录单独放到数据盘上)。此时,docker的读写层(RWLayer)就会落到云盘上。
keke.
2021/04/06
1.2K1
重定向Kubernetes pod中的tcpdump输出
最新发现一个比较有意思的库ksniff,它是一个kubectl 插件,使用tcpdump来远程捕获Kubernetes集群中的pod流量并保存到文件或输出到wireshark中,发布网络问题定位。使用方式如下:
charlieroro
2023/02/25
1.1K0
kubelet 原理解析五: exec的背后
线上排查pod 问题一般有两种方式,kubectl log 或者 kubectl exec 调试。如果你的 log 写不够优雅,或者需要排除网络问题必须进容器,就只能 exec 了。
YP小站
2020/12/02
3.5K0
kubelet 原理解析五: exec的背后
Pod疑难杂症(3):Device or resource busy
在删除pod时,可能会遇到如下事件'Unlinkat xxxxx: Device or resource busy',设备或资源忙导致某个文件无法被删除,进而导致pod卡在Terminating状态。
keke.
2021/05/10
5.5K2
1.Containerd容器运行时初识与尝试
本章主要讲解,目前K8S使用率最多的容器运行时讲解, 由于k8s在2020年宣布1.20版本之后将弃用dockershim(其中也有kubernetes与Docker爱恨情仇)时才把containerd拉回大众的视野之中,本章主要讲解containerd基础入门。
全栈工程师修炼指南
2021/07/25
2.8K0
1.Containerd容器运行时初识与尝试
你知道 kubectl exec 的运行机制是什么吗?
看到一篇关于 Kubectl 运行的机制,觉得写得非常不错,图文并茂很形象,就翻译成了中文记录一下,原文地址:
米开朗基杨
2021/10/18
1.1K0
你知道 kubectl exec 的运行机制是什么吗?
容器能不能将 volume 挂载直接挂到根目录?—— 浅析 kubelet 到 runc 的调用过程
这件事起源于有小伙伴在某群里问,在 K8s 中,能不能把 volume 挂载直接挂到根目录?我的第一反应是不能。容器会使用 union filesystem 将容器的内容挂到根目录下,这点在正常情况下是无法更改的。但是就止于此吗?发现给不出合理解释的时候,突然感觉自己对于容器的认知只停留在了很表面的阶段。
腾讯云 CODING
2023/03/31
1.3K0
容器能不能将 volume 挂载直接挂到根目录?—— 浅析 kubelet 到 runc 的调用过程
Containerd深度剖析-CRI篇
目前我司现网的K8s集群的运行时已经完成从docker到Containerd的切换,有小伙伴对K8s与Containerd调用链涉及的组件不了解,其中Containerd和RunC是什么关系,docker和containerd又有什么区别,以及K8s调用Containerd创建容器又是怎样的流程,最终RunC又是如何创建容器的,诸如此类的疑问。本文就针对K8s使用Containerd作为运行时的整个调用链进行介绍和源码级别的分析。
zouyee
2023/01/31
1.5K0
Containerd深度剖析-CRI篇
(译)kubectl exec 的来龙去脉
上周五,一个同事问了我一个问题——如何使用 client-go 在 Pod 中执行命令。我答不出来,而且注意到我从来没想过 kubectl exec 的实现机制。我对这个问题有一点认识,但又不是很确定。我记下了这个题目,进行了一番探索,在阅读了大量博客、文档和代码之后,收获了很多知识。本文中我会分享这个过程中的理解和发现。
崔秀龙
2019/08/29
3.2K0
Docker架构分析
大家一定很困惑 dockerd, containerd, ctr,shim, runc,等这几个进程的关系到底是啥
sealyun
2019/07/26
1.7K0
Containerd 介绍与使用
在学习 Containerd 之前我们有必要对 Docker 的发展历史做一个简单的回顾,因为这里面牵涉到的组件实战是有点多,有很多我们会经常听到,但是不清楚这些组件到底是干什么用的,比如 libcontainer、runc、containerd、CRI、OCI 等等。
王先森sec
2023/10/17
1.6K0
Containerd 介绍与使用
ignite
Ignite是一个启动firecracker vm的引擎,它使用容器的方式承载了firecracker vm。目前项目处于停滞阶段,也比较可惜,通过阅读了解ignite的工作方式,学习到了很多,希望能借此维护该项目。
charlieroro
2023/08/17
1.1K0
ignite
n1.Docker容器安装运行所遇异常解决
.Docker日志设置定期清理 1) 设置容器为3个日志文件容,分别是id+.json、id+1.json、id+2.json,但是此时只对新建的容器有效;
全栈工程师修炼指南
2022/09/28
2.3K0
n1.Docker容器安装运行所遇异常解决
推荐阅读
相关推荐
【Pod Terminating原因追踪系列之三】让docker事件处理罢工的cancel状态码
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档