当前位置:   article > 正文

docker exec 失败问题排查之旅

error adding pid to cgroups: failed to write

d9b21e166c74ea87e2ae0d6c05d58b03.gif

这篇文章学习的重点:

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

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

  • runc 工作流程

问题描述

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

  1. 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"
  2. 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"
  3. 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 的工作原理与调用链路:

d16a05a482ba4be207ed184e73d7fac6.png
docker调用链路

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

  • 确定引起失败的组件

  • 确定组件失败的原因

定位组件

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

  1. 1. 定位问题容器
  2. $ sudo docker ps | grep -v pause | grep -v NAMES | awk '{print $1}' | xargs -ti sudo docker exec {} sleep 1
  3. sudo docker exec aa1e331ec24f sleep 1
  4. OCI runtime exec failed: exec failed: container_linux.go:348: starting container process caused "read init-p: connection reset by peer": unknown
  5. 2. 排除docker嫌疑
  6. $ docker-containerd-ctr -a /var/run/docker/containerd/docker-containerd.sock -n moby t exec --exec-id stupig1 aa1e331ec24f621ab3152ebe94f1e533734164af86c9df0f551eab2b1967ec4e sleep 1
  7. ctr: OCI runtime exec failed: exec failed: container_linux.go:348: starting container process caused "read init-p: connection reset by peer": unknown
  8. 3. 排除containerd与containerd-shim嫌疑
  9. $ docker-runc --root /var/run/docker/runtime-runc/moby/ exec aa1e331ec24f621ab3152ebe94f1e533734164af86c9df0f551eab2b1967ec4e sleep
  10. runtime/cgo: pthread_create failed: Resource temporarily unavailable
  11. SIGABRT: abort
  12. PC=0x6b657e m=0 sigcode=18446744073709551610
  13. goroutine 0 [idle]:
  14. runtime: unknown pc 0x6b657e
  15. stack: frame={sp:0x7ffd30f0d218, fp:0x0} stack=[0x7ffd2ab0e738,0x7ffd30f0d760)
  16. 00007ffd30f0d118:  0000000000000002  00007ffd30f7f184
  17. 00007ffd30f0d128:  000000000069c31c  00007ffd30f0d1a8
  18. 00007ffd30f0d138:  000000000045814e <runtime.callCgoMmap+62>  00007ffd30f0d140
  19. 00007ffd30f0d148:  00007ffd30f0d190  0000000000411a88 <runtime.persistentalloc1+456>
  20. 00007ffd30f0d158:  0000000000bf6dd0  0000000000000000
  21. 00007ffd30f0d168:  0000000000010000  0000000000000008
  22. 00007ffd30f0d178:  0000000000bf6dd8  0000000000bf7ca0
  23. 00007ffd30f0d188:  00007fdcbb4b7000  00007ffd30f0d1c8
  24. 00007ffd30f0d198:  0000000000451205 <runtime.persistentalloc.func1+69>  0000000000000000
  25. 00007ffd30f0d1a8:  0000000000000000  0000000000c1c080
  26. 00007ffd30f0d1b8:  00007fdcbb4b7000  00007ffd30f0d1e0
  27. 00007ffd30f0d1c8:  00007ffd30f0d210  00007ffd30f0d220
  28. 00007ffd30f0d1d8:  0000000000000000  00000000000000f1
  29. 00007ffd30f0d1e8:  0000000000000011  0000000000000000
  30. 00007ffd30f0d1f8:  000000000069c31c  0000000000c1c080
  31. 00007ffd30f0d208:  000000000045814e <runtime.callCgoMmap+62>  00007ffd30f0d210
  32. 00007ffd30f0d218: <00007ffd30f0d268  fffffffe7fffffff
  33. 00007ffd30f0d228:  ffffffffffffffff  ffffffffffffffff
  34. 00007ffd30f0d238:  ffffffffffffffff  ffffffffffffffff
  35. 00007ffd30f0d248:  ffffffffffffffff  ffffffffffffffff
  36. 00007ffd30f0d258:  ffffffffffffffff  ffffffffffffffff
  37. 00007ffd30f0d268:  ffffffffffffffff  ffffffffffffffff
  38. 00007ffd30f0d278:  ffffffffffffffff  ffffffffffffffff
  39. 00007ffd30f0d288:  ffffffffffffffff  ffffffffffffffff
  40. 00007ffd30f0d298:  ffffffffffffffff  0000000000000000
  41. 00007ffd30f0d2a8:  00000000006b68ba  0000000000000020
  42. 00007ffd30f0d2b8:  0000000000000000  0000000000000000
  43. 00007ffd30f0d2c8:  0000000000000000  0000000000000000
  44. 00007ffd30f0d2d8:  0000000000000000  0000000000000000
  45. 00007ffd30f0d2e8:  0000000000000000  0000000000000000
  46. 00007ffd30f0d2f8:  0000000000000000  0000000000000000
  47. 00007ffd30f0d308:  0000000000000000  0000000000000000
  48. runtime: unknown pc 0x6b657e
  49. stack: frame={sp:0x7ffd30f0d218, fp:0x0} stack=[0x7ffd2ab0e738,0x7ffd30f0d760)
  50. 00007ffd30f0d118:  0000000000000002  00007ffd30f7f184
  51. 00007ffd30f0d128:  000000000069c31c  00007ffd30f0d1a8
  52. 00007ffd30f0d138:  000000000045814e <runtime.callCgoMmap+62>  00007ffd30f0d140
  53. 00007ffd30f0d148:  00007ffd30f0d190  0000000000411a88 <runtime.persistentalloc1+456>
  54. 00007ffd30f0d158:  0000000000bf6dd0  0000000000000000
  55. 00007ffd30f0d168:  0000000000010000  0000000000000008
  56. 00007ffd30f0d178:  0000000000bf6dd8  0000000000bf7ca0
  57. 00007ffd30f0d188:  00007fdcbb4b7000  00007ffd30f0d1c8
  58. 00007ffd30f0d198:  0000000000451205 <runtime.persistentalloc.func1+69>  0000000000000000
  59. 00007ffd30f0d1a8:  0000000000000000  0000000000c1c080
  60. 00007ffd30f0d1b8:  00007fdcbb4b7000  00007ffd30f0d1e0
  61. 00007ffd30f0d1c8:  00007ffd30f0d210  00007ffd30f0d220
  62. 00007ffd30f0d1d8:  0000000000000000  00000000000000f1
  63. 00007ffd30f0d1e8:  0000000000000011  0000000000000000
  64. 00007ffd30f0d1f8:  000000000069c31c  0000000000c1c080
  65. 00007ffd30f0d208:  000000000045814e <runtime.callCgoMmap+62>  00007ffd30f0d210
  66. 00007ffd30f0d218: <00007ffd30f0d268  fffffffe7fffffff
  67. 00007ffd30f0d228:  ffffffffffffffff  ffffffffffffffff
  68. 00007ffd30f0d238:  ffffffffffffffff  ffffffffffffffff
  69. 00007ffd30f0d248:  ffffffffffffffff  ffffffffffffffff
  70. 00007ffd30f0d258:  ffffffffffffffff  ffffffffffffffff
  71. 00007ffd30f0d268:  ffffffffffffffff  ffffffffffffffff
  72. 00007ffd30f0d278:  ffffffffffffffff  ffffffffffffffff
  73. 00007ffd30f0d288:  ffffffffffffffff  ffffffffffffffff
  74. 00007ffd30f0d298:  ffffffffffffffff  0000000000000000
  75. 00007ffd30f0d2a8:  00000000006b68ba  0000000000000020
  76. 00007ffd30f0d2b8:  0000000000000000  0000000000000000
  77. 00007ffd30f0d2c8:  0000000000000000  0000000000000000
  78. 00007ffd30f0d2d8:  0000000000000000  0000000000000000
  79. 00007ffd30f0d2e8:  0000000000000000  0000000000000000
  80. 00007ffd30f0d2f8:  0000000000000000  0000000000000000
  81. 00007ffd30f0d308:  0000000000000000  0000000000000000
  82. goroutine 1 [running]:
  83. runtime.systemstack_switch()
  84.  /usr/local/go/src/runtime/asm_amd64.s:363 fp=0xc4200fe788 sp=0xc4200fe780 pc=0x454120
  85. runtime.main()
  86.  /usr/local/go/src/runtime/proc.go:128 +0x63 fp=0xc4200fe7e0 sp=0xc4200fe788 pc=0x42bb83
  87. runtime.goexit()
  88.  /usr/local/go/src/runtime/asm_amd64.s:2361 +0x1 fp=0xc4200fe7e8 sp=0xc4200fe7e0 pc=0x456c91
  89. rax    0x0
  90. rbx    0xbe2978
  91. rcx    0x6b657e
  92. rdx    0x0
  93. rdi    0x2
  94. rsi    0x7ffd30f0d1a0
  95. rbp    0x8347ce
  96. rsp    0x7ffd30f0d218
  97. r8     0x0
  98. r9     0x6
  99. r10    0x8
  100. r11    0x246
  101. r12    0x2bedc30
  102. r13    0xf1
  103. r14    0x11
  104. r15    0x0
  105. rip    0x6b657e
  106. rflags 0x246
  107. cs     0x33
  108. fs     0x0
  109. gs     0x0
  110. 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):

  • 线程数达到限制

  • 文件数达到限制

  • 内存达到限制

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

c444f50251768bf41f0c6d331d9cb35c.png
业务线程数监控指标

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

  1. $ cat /sys/fs/cgroup/pids/kubepods/burstable/pod64a6c0e7-830c-11eb-86d6-b8cef604db88/aa1e331ec24f621ab3152ebe94f1e533734164af86c9df0f551eab2b1967ec4e/pids.max
  2. 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 的初始化参见下图:

a594d55afb30fb8cdd3ae8a9dcc27de2.png
runc工作流程

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

  1. func (p *setnsProcess) start() (err error) {
  2.    defer p.parentPipe.Close()
  3.    err = p.cmd.Start()
  4.    p.childPipe.Close()
  5.    if err != nil {
  6.       return newSystemErrorWithCause(err, "starting setns process")
  7.    }
  8.    if p.bootstrapData != nil {
  9.       if _, err := io.Copy(p.parentPipe, p.bootstrapData); err != nil {       // clone标志位,ns配置
  10.          return newSystemErrorWithCause(err, "copying bootstrap data to pipe")
  11.       }
  12.    }
  13.    if err = p.execSetns(); err != nil {
  14.       return newSystemErrorWithCause(err, "executing setns process")
  15.    }
  16.    if len(p.cgroupPaths) > 0 {
  17.       if err := cgroups.EnterPid(p.cgroupPaths, p.pid()); err != nil {        // 这里将runc init添加到容器cgroup中
  18.          return newSystemErrorWithCausef(err, "adding pid %d to cgroups", p.pid())
  19.       }
  20.    }
  21.    if err := utils.WriteJSON(p.parentPipe, p.config); err != nil {            // 发送配置:命令、环境变量等
  22.       return newSystemErrorWithCause(err, "writing config to pipe")
  23.    }
  24.    ierr := parseSync(p.parentPipe, func(sync *syncT) error {                  // 这里返回 read init-p: connection reset by peer
  25.       switch sync.Type {
  26.       case procReady:
  27.          // This shouldn't happen.
  28.          panic("unexpected procReady in setns")
  29.       case procHooks:
  30.          // This shouldn't happen.
  31.          panic("unexpected procHooks in setns")
  32.       default:
  33.          return newSystemError(fmt.Errorf("invalid JSON payload from child"))
  34.       }
  35.    })
  36.    if ierr != nil {
  37.       p.wait()
  38.       return ierr
  39.    }
  40.    return nil
  41. }

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

Reference

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

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

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

  1. Linux学习指南
  2. 有收获,点个在看
声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop】
推荐阅读
相关标签
  

闽ICP备14008679号