docker exec 失败问题排查之旅

本文通过一起线上异常日志的排查案例,介绍了Kubernetes环境中容器资源限制导致的问题定位过程,详细分析了docker、containerd及runc之间的交互流程。

a12285d287e6bc4ef0edf49c8d707d8e.gif

这篇文章学习的重点:

  • kubelet,docker-shim,dockerd,containerd,containerd-shim,runc 直接的关系

  • 排查方法:如何使用 docker,containerd-ctr,docker-runc 连接容器

  • runc 工作流程

问题描述

今天,在值班排查线上问题的过程中,发现系统日志一直在刷 docker 异常日志:

May 12 09:08:40 HOSTNAME dockerd[4085]: time="2021-05-12T09:08:40.642410594+08:00" level=error msg="stream copy error: reading from a closed fifo"
May 12 09:08:40 HOSTNAME dockerd[4085]: time="2021-05-12T09:08:40.642418571+08:00" level=error msg="stream copy error: reading from a closed fifo"
May 12 09:08:40 HOSTNAME dockerd[4085]: time="2021-05-12T09:08:40.663754355+08:00" level=error msg="Error running exec 110deb1c1b2a2d2671d7368bd02bfc18a968e4712a3c771dedf0b362820e73cb in container: OCI runtime exec failed: exec failed: container_linux.go:348: starting container process caused \"read init-p: connection reset by peer\": unknown"

从系统风险性上来看,异常日志出现的原因需要排查清楚,并摸清是否会对业务产生影响。

下文简单介绍问题排查的流程,以及产生的原因。

问题排查

现在我们唯一掌握的信息,只有系统日志告知 dockerd 执行 exec 失败。

在具体的问题分析之前,我们再来回顾一下 docker 的工作原理与调用链路:

1d0693d2f285712b89d18e6bc3c2bbdf.png
docker调用链路

可见,docker 的调用链路非常长,涉及组件也较多。因此,我们的排查路径主要分为如下两步:

  • 确定引起失败的组件

  • 确定组件失败的原因

定位组件

熟悉 docker 的用户能够一眼定位引起问题的组件。但是,我们还是按照常规的排查流程走一遍:

# 1. 定位问题容器
$ sudo docker ps | grep -v pause | grep -v NAMES | awk '{print $1}' | xargs -ti sudo docker exec {} sleep 1
sudo docker exec aa1e331ec24f sleep 1
OCI runtime exec failed: exec failed: container_linux.go:348: starting container process caused "read init-p: connection reset by peer": unknown


# 2. 排除docker嫌疑
$ docker-containerd-ctr -a /var/run/docker/containerd/docker-containerd.sock -n moby t exec --exec-id stupig1 aa1e331ec24f621ab3152ebe94f1e533734164af86c9df0f551eab2b1967ec4e sleep 1
ctr: OCI runtime exec failed: exec failed: container_linux.go:348: starting container process caused "read init-p: connection reset by peer": unknown


# 3. 排除containerd与containerd-shim嫌疑
$ docker-runc --root /var/run/docker/runtime-runc/moby/ exec aa1e331ec24f621ab3152ebe94f1e533734164af86c9df0f551eab2b1967ec4e sleep
runtime/cgo: pthread_create failed: Resource temporarily unavailable
SIGABRT: abort
PC=0x6b657e m=0 sigcode=18446744073709551610

goroutine 0 [idle]:
runtime: unknown pc 0x6b657e
stack: frame={sp:0x7ffd30f0d218, fp:0x0} stack=[0x7ffd2ab0e738,0x7ffd30f0d760)
00007ffd30f0d118:  0000000000000002  00007ffd30f7f184
00007ffd30f0d128:  000000000069c31c  00007ffd30f0d1a8
00007ffd30f0d138:  000000000045814e <runtime.callCgoMmap+62>  00007ffd30f0d140
00007ffd30f0d148:  00007ffd30f0d190  0000000000411a88 <runtime.persistentalloc1+456>
00007ffd30f0d158:  0000000000bf6dd0  0000000000000000
00007ffd30f0d168:  0000000000010000  0000000000000008
00007ffd30f0d178:  0000000000bf6dd8  0000000000bf7ca0
00007ffd30f0d188:  00007fdcbb4b7000  00007ffd30f0d1c8
00007ffd30f0d198:  0000000000451205 <runtime.persistentalloc.func1+69>  0000000000000000
00007ffd30f0d1a8:  0000000000000000  0000000000c1c080
00007ffd30f0d1b8:  00007fdcbb4b7000  00007ffd30f0d1e0
00007ffd30f0d1c8:  00007ffd30f0d210  00007ffd30f0d220
00007ffd30f0d1d8:  0000000000000000  00000000000000f1
00007ffd30f0d1e8:  0000000000000011  0000000000000000
00007ffd30f0d1f8:  000000000069c31c  0000000000c1c080
00007ffd30f0d208:  000000000045814e <runtime.callCgoMmap+62>  00007ffd30f0d210
00007ffd30f0d218: <00007ffd30f0d268  fffffffe7fffffff
00007ffd30f0d228:  ffffffffffffffff  ffffffffffffffff
00007ffd30f0d238:  ffffffffffffffff  ffffffffffffffff
00007ffd30f0d248:  ffffffffffffffff  ffffffffffffffff
00007ffd30f0d258:  ffffffffffffffff  ffffffffffffffff
00007ffd30f0d268:  ffffffffffffffff  ffffffffffffffff
00007ffd30f0d278:  ffffffffffffffff  ffffffffffffffff
00007ffd30f0d288:  ffffffffffffffff  ffffffffffffffff
00007ffd30f0d298:  ffffffffffffffff  0000000000000000
00007ffd30f0d2a8:  00000000006b68ba  0000000000000020
00007ffd30f0d2b8:  0000000000000000  0000000000000000
00007ffd30f0d2c8:  0000000000000000  0000000000000000
00007ffd30f0d2d8:  0000000000000000  0000000000000000
00007ffd30f0d2e8:  0000000000000000  0000000000000000
00007ffd30f0d2f8:  0000000000000000  0000000000000000
00007ffd30f0d308:  0000000000000000  0000000000000000
runtime: unknown pc 0x6b657e
stack: frame={sp:0x7ffd30f0d218, fp:0x0} stack=[0x7ffd2ab0e738,0x7ffd30f0d760)
00007ffd30f0d118:  0000000000000002  00007ffd30f7f184
00007ffd30f0d128:  000000000069c31c  00007ffd30f0d1a8
00007ffd30f0d138:  000000000045814e <runtime.callCgoMmap+62>  00007ffd30f0d140
00007ffd30f0d148:  00007ffd30f0d190  0000000000411a88 <runtime.persistentalloc1+456>
00007ffd30f0d158:  0000000000bf6dd0  0000000000000000
00007ffd30f0d168:  0000000000010000  0000000000000008
00007ffd30f0d178:  0000000000bf6dd8  0000000000bf7ca0
00007ffd30f0d188:  00007fdcbb4b7000  00007ffd30f0d1c8
00007ffd30f0d198:  0000000000451205 <runtime.persistentalloc.func1+69>  0000000000000000
00007ffd30f0d1a8:  0000000000000000  0000000000c1c080
00007ffd30f0d1b8:  00007fdcbb4b7000  00007ffd30f0d1e0
00007ffd30f0d1c8:  00007ffd30f0d210  00007ffd30f0d220
00007ffd30f0d1d8:  0000000000000000  00000000000000f1
00007ffd30f0d1e8:  0000000000000011  0000000000000000
00007ffd30f0d1f8:  000000000069c31c  0000000000c1c080
00007ffd30f0d208:  000000000045814e <runtime.callCgoMmap+62>  00007ffd30f0d210
00007ffd30f0d218: <00007ffd30f0d268  fffffffe7fffffff
00007ffd30f0d228:  ffffffffffffffff  ffffffffffffffff
00007ffd30f0d238:  ffffffffffffffff  ffffffffffffffff
00007ffd30f0d248:  ffffffffffffffff  ffffffffffffffff
00007ffd30f0d258:  ffffffffffffffff  ffffffffffffffff
00007ffd30f0d268:  ffffffffffffffff  ffffffffffffffff
00007ffd30f0d278:  ffffffffffffffff  ffffffffffffffff
00007ffd30f0d288:  ffffffffffffffff  ffffffffffffffff
00007ffd30f0d298:  ffffffffffffffff  0000000000000000
00007ffd30f0d2a8:  00000000006b68ba  0000000000000020
00007ffd30f0d2b8:  0000000000000000  0000000000000000
00007ffd30f0d2c8:  0000000000000000  0000000000000000
00007ffd30f0d2d8:  0000000000000000  0000000000000000
00007ffd30f0d2e8:  0000000000000000  0000000000000000
00007ffd30f0d2f8:  0000000000000000  0000000000000000
00007ffd30f0d308:  0000000000000000  0000000000000000

goroutine 1 [running]:
runtime.systemstack_switch()
 /usr/local/go/src/runtime/asm_amd64.s:363 fp=0xc4200fe788 sp=0xc4200fe780 pc=0x454120
runtime.main()
 /usr/local/go/src/runtime/proc.go:128 +0x63 fp=0xc4200fe7e0 sp=0xc4200fe788 pc=0x42bb83
runtime.goexit()
 /usr/local/go/src/runtime/asm_amd64.s:2361 +0x1 fp=0xc4200fe7e8 sp=0xc4200fe7e0 pc=0x456c91

rax    0x0
rbx    0xbe2978
rcx    0x6b657e
rdx    0x0
rdi    0x2
rsi    0x7ffd30f0d1a0
rbp    0x8347ce
rsp    0x7ffd30f0d218
r8     0x0
r9     0x6
r10    0x8
r11    0x246
r12    0x2bedc30
r13    0xf1
r14    0x11
r15    0x0
rip    0x6b657e
rflags 0x246
cs     0x33
fs     0x0
gs     0x0
exec failed: container_linux.go:348: starting container process caused "read init-p: connection reset by peer"

由上可知,异常是 runc 返回的。

定位原因

定位异常组件的同时,runc 还给了我们一个惊喜:提供了详细的异常日志。

异常日志表明:runc exec 失败的原因是因为 Resource temporarily unavailable,比较典型的资源不足问题。而常见的资源不足类型主要包含(ulimit -a):

  • 线程数达到限制

  • 文件数达到限制

  • 内存达到限制

因此,我们需要进一步排查业务容器的监控,以定位不足的资源类型。

143ddcbfdd788b36569104976968a34b.png
业务线程数监控指标

上图展示了业务容器的线程数监控。所有容器的线程数都已经达到 1w,而弹性云默认限制容器的线程数上限就是 1w,设定该上限的原因,也是为了避免单容器线程泄漏而耗尽宿主机的线程资源。

$ cat /sys/fs/cgroup/pids/kubepods/burstable/pod64a6c0e7-830c-11eb-86d6-b8cef604db88/aa1e331ec24f621ab3152ebe94f1e533734164af86c9df0f551eab2b1967ec4e/pids.max
10000

至此,问题的原因已定位清楚,对,就是这么简单。

runc 梳理

虽然,我们已经定位了异常日志的成因,但是,对于 runc 的具体工作机制,一直只有一个模糊的概念。

趁此机会,我们以 runc exec 为例,梳理 runc 的工作流程。

  • runc exec 首先启动子进程 runc init

  • runc init 负责初始化容器 namespace

    • runc init 利用 C 语言的 constructor 特性,实现在 go 代码启动之前,设置容器 namespace

    • C 代码 nsexec 执行两次 clone,共三个线程:父进程,子进程,孙进程,完成对容器 namespace 的初始化

    • 父进程与子进程完成初始化任务后退出,此时,孙进程已经在容器 namespace 内,孙进程开始执行 go 代码初始化,并等待接收 runc exec 发送配置

  • runc exec 将孙进程添加到容器 cgroup

  • runc exec 发送配置给孙进程,配置主要包含:exec 的具体命令与参数等

  • 孙进程调用 system.Execv 执行用户命令

注意:

  • 步骤 2.c 与步骤 3 是并发执行的

  • runc exec 与 runc init 通信基于 socket pair 对(init-p 和 init-c)

runc exec 过程中各进程的交互流程,以及 namespace 与 cgroup 的初始化参见下图:

b3bfd9e16c9b45f8923dfd9ff41fc99e.png
runc工作流程

综合我们对 runc exec 执行流程的梳理,以及 runc exec 返回的错误信息,我们基本定位到了 runc exec 返回错误的代码:

func (p *setnsProcess) start() (err error) {
   defer p.parentPipe.Close()
   err = p.cmd.Start()
   p.childPipe.Close()
   if err != nil {
      return newSystemErrorWithCause(err, "starting setns process")
   }
   if p.bootstrapData != nil {
      if _, err := io.Copy(p.parentPipe, p.bootstrapData); err != nil {       // clone标志位,ns配置
         return newSystemErrorWithCause(err, "copying bootstrap data to pipe")
      }
   }
   if err = p.execSetns(); err != nil {
      return newSystemErrorWithCause(err, "executing setns process")
   }
   if len(p.cgroupPaths) > 0 {
      if err := cgroups.EnterPid(p.cgroupPaths, p.pid()); err != nil {        // 这里将runc init添加到容器cgroup中
         return newSystemErrorWithCausef(err, "adding pid %d to cgroups", p.pid())
      }
   }
   if err := utils.WriteJSON(p.parentPipe, p.config); err != nil {            // 发送配置:命令、环境变量等
      return newSystemErrorWithCause(err, "writing config to pipe")
   }

   ierr := parseSync(p.parentPipe, func(sync *syncT) error {                  // 这里返回 read init-p: connection reset by peer
      switch sync.Type {
      case procReady:
         // This shouldn't happen.
         panic("unexpected procReady in setns")
      case procHooks:
         // This shouldn't happen.
         panic("unexpected procHooks in setns")
      default:
         return newSystemError(fmt.Errorf("invalid JSON payload from child"))
      }
   })
   if ierr != nil {
      p.wait()
      return ierr
   }
   return nil
}

现在,问题的成因与代码分析已全部完成。

Reference

  1. https://www.kernel.org/doc/Documentation/cgroup-v1/pids.txt

  2. https://github.com/opencontainers/runc

原文链接:https://xyz.uscwifi.xyz/post/DdS5a690E/

03ed436dbea8df4b675740a0768ac9b6.gif

627aee96d91cd8d37aef2c877d6fc02c.png

你可能还喜欢

点击下方图片即可阅读

49aa1314bc9eb44854c7a4daeeb5e653.png

服务网格:棋到中盘方见势

e55150adec026053e80d7b2f90c8fc5c.gif

云原生是一种信仰 🤘

关注公众号

后台回复◉k8s◉获取史上最方便快捷的 Kubernetes 高可用部署工具,只需一条命令,连 ssh 都不需要!

dfcac8fb383052a1ae5f9237b226f682.gif

5bca74cb3969942c1f4a24f44da3b77f.gif

点击 "阅读原文" 获取更好的阅读体验!

发现朋友圈变“安静”了吗?

037c090cb11ebab8974582a1ad0b7e4c.gif

<think>好的,我现在需要向用户解释docker exec命令。首先,我得回忆一下自己对这个命令的理解。Docker exec主要用于在正在运行的容器中执行命令,对吧?用户可能已经知道基本的Docker概念,但可能需要具体的使用方法和示例。 接下来,我应该先介绍docker exec的基本作用,让用户明白这个命令的用途。然后,可能需要说明它的基本语法结构,比如docker exec [选项] 容器名称 命令。这里要提到常用的选项,比如-it用于交互式操作,或者指定用户等。 用户可能想知道为什么需要这个命令。比如,他们可能需要在容器内部调试,查看日志,或者安装额外的软件包。这时候举几个例子会有帮助。例如,进入容器的bash shell,或者执行一个简单的命令如ls查看文件列表。 另外,需要注意一些细节,比如容器必须是运行中的状态,否则exec无法使用。还有,如果容器中没有bash,可能需要使用sh或其他shell。这些点也需要提及,避免用户遇到问题。 还要考虑用户可能遇到的常见问题,比如如何保持交互式会话,或者如何以特定用户身份执行命令。这时候需要给出相应的选项,比如-it和-u的用法。 可能需要对比一下docker attach,说明exec的优势,比如不会影响容器的主进程,更适合调试用途。 最后,总结一下使用场景,比如调试、维护、执行一次性任务等,并提醒用户注意安全性,不要随意执行不安全命令。 现在需要把这些点组织成结构清晰的解释,分步骤说明,确保用户容易理解。同时要使用中文,避免技术术语过于晦涩,必要时举例说明。检查一下是否有遗漏的重要选项或使用场景,确保信息准确。</think>Docker 的 `docker exec` 命令是用于在**正在运行的容器内部执行命令**的核心工具。以下是逐步说明和典型使用场景: --- ### 一、基础语法 ``` docker exec [选项] <容器名称或ID> <要执行的命令> ``` --- ### 二、核心作用 1. **调试容器** 当容器已启动但行为不符合预期时,进入容器内部检查环境、进程或日志。 2. **执行一次性操作** 例如备份数据、修改配置、安装临时工具(如 `curl` 或 `vim`)。 3. **交互式操作** 像使用 SSH 一样与容器交互,适合调试或维护。 --- ### 三、常用选项 | 选项 | 作用 | 示例 | |------|------|------| | `-i` | 保持标准输入打开(允许输入命令) | `docker exec -i my-container sh` | | `-t` | 分配伪终端(使命令行界面更友好) | 通常组合使用 `-it` | | `-u` | 指定执行命令的用户 | `docker exec -u root my-container bash` | | `-d` | 后台执行命令 | `docker exec -d my-container touch /tmp/file` | | `-e` | 设置环境变量 | `docker exec -e VAR=value my-container env` | --- ### 四、典型示例 1. **进入容器的交互式 Shell** ```bash docker exec -it my-container /bin/bash ``` - 若容器没有 `bash`,尝试 `/bin/sh` - 按 `Ctrl+D` 或输入 `exit` 退出 2. **执行单条命令并查看结果** ```bash docker exec my-container ls /app # 查看容器内的 /app 目录 docker exec my-container cat /etc/os-release # 检查容器操作系统信息 ``` 3. **以 root 用户执行特权操作** ```bash docker exec -u root -it my-container apt-get update # 在 Debian 容器中更新软件包 ``` 4. **后台执行命令** ```bash docker exec -d my-container python3 /app/script.py # 后台运行 Python 脚本 ``` --- ### 五、注意事项 1. **容器必须处于运行状态** 若容器已停止(`Exited` 状态),需先通过 `docker start` 启动。 2. **谨慎修改容器内部** 直接修改运行中容器的文件系统(如安装软件)可能导致容器与镜像不一致,建议通过 `Dockerfile` 重建镜像。 3. **与 `docker attach` 的区别** - `docker attach` 直接连接到容器的主进程(如前台运行的进程),退出时可能导致容器停止。 - `docker exec` 新建独立进程,退出不会影响容器运行。 --- ### 六、使用场景总结 | 场景 | 命令示例 | |------|----------| | 调试环境变量 | `docker exec my-container env` | | 检查网络连接 | `docker exec my-container ping google.com` | | 查看进程 | `docker exec my-container ps aux` | | 修改配置 | `docker exec my-container vi /etc/nginx/nginx.conf`(需容器内有 `vi`) | 通过灵活使用 `docker exec`,可以高效管理容器化应用,而无需重新构建镜像或重启服务。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值