前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >Docker EOF问题排查

Docker EOF问题排查

作者头像
没有故事的陈师傅
发布于 2021-06-24 10:05:49
发布于 2021-06-24 10:05:49
5.1K00
代码可运行
举报
文章被收录于专栏:运维开发故事运维开发故事
运行总次数:0
代码可运行

一、前言

问题排查过程,源码部分均由我的开发同事排查和记录;在征得其同意后,由我发表在此。

二、问题

某天接到客户反馈,pod的事件中出现大量的 warning event: Readiness probe failed: OCI runtime exec failed: exec failed: EOF: unknown。但不影响客户访问该服务。

三、环境

特别说明:客户在负责运行业务的k8s节点上坚持开启了cpu-manager

组件

版本

k8s

1.14.x

四、排查

1、接到客户反馈后,检查该pod所在节点的kubelet日志,如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
I0507 03:43:28.310630  57003 prober.go:112] Readiness probe for "adsfadofadfabdfhaodsfa(d1aab5f0-
ae8f-11eb-a151-080027049c65):c0" failed (failure): OCI runtime exec failed: exec failed: EOF: unknown
I0507 07:08:49.834093  57003 prober.go:112] Readiness probe for "adsfadofadfabdfhaodsfa(a89a158e-
ae8f-11eb-a151-080027049c65):c0" failed (failure): OCI runtime exec failed: exec failed: unexpected EOF: unknown
I0507 10:06:58.307881  57003 prober.go:112] Readiness probe for "adsfadofadfabdfhaodsfa(d1aab5f0-
ae8f-11eb-a151-080027049c65):c0" failed (failure): OCI runtime exec failed: exec failed: EOF: unknown

probe的错误类型为failure,对应代码如下:

2、再查看docker日志,如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
time="2021-05-06T16:51:40.009989451+08:00" level=error msg="stream copy error: reading from a closed fifo"
time="2021-05-06T16:51:40.010054596+08:00" level=error msg="stream copy error: reading from a closed fifo"
time="2021-05-06T16:51:40.170676532+08:00" level=error msg="Error running exec 8e34e8b910694abe95a467b2936b37635fdabd2f7b7c464d
fef952fa5732aa4e in container: OCI runtime exec failed: exec failed: EOF: unknown"

虽然从Docker日志中显示是 stream copy error,但实际上是底层的 runc 返回了 EOF,导致返回了 error。3、因为日志中显示 probe 类型为 Failure,因此 e.CombinedOutPut() 的 err != nil,并且 ExitStatus 不为 0,data 的值为 OCI runtime exec failed: exec failed: unexpected EOF: unknown,最终会调用到 RunInContainer 方法

ExecSync 是通过 GRPC 调用了 dockershim 的 ExecSync

dockershim 最终调用到 ExecInContainer 方法,并且该方法的返回了 exitcode 不为 0 的 error。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制

func (*NativeExecHandler) ExecInContainer(client libdocker.Interface, container *dockertypes.ContainerJSON, cmd []string, stdin io.Reader, stdout, stderr io.WriteCloser, tty bool, resize <-chan remotecommand.TerminalSize, timeout time.Duration) error {
   execObj, err := client.CreateExec(container.ID, createOpts)
   
   startOpts := dockertypes.ExecStartCheck{Detach: false, Tty: tty}
   streamOpts := libdocker.StreamOptions{
      InputStream:  stdin,
      OutputStream: stdout,
      ErrorStream:  stderr,
      RawTerminal:  tty,
      ExecStarted:  execStarted,
   }
   err = client.StartExec(execObj.ID, startOpts, streamOpts)
   if err != nil {
      return err
   }

    ticker := time.NewTicker(2 * time.Second)
    defer ticker.Stop()
    count := 0
    for {
       inspect, err2 := client.InspectExec(execObj.ID)
       if err2 != nil {
          return err2
       }
       if !inspect.Running {
          if inspect.ExitCode != 0 {
             err = &dockerExitError{inspect}
          }
          break
       }
    
       count++
       if count == 5 {
          klog.Errorf("Exec session %s in container %s terminated but process still running!", execObj.ID, container.ID)
          break
       }
    
       <-ticker.C
    }

   return err
}

ExecInContainer 做了以下几件事:

  1. 调用 CreateExec 创建 ExecID
  2. 调用 StartExec 执行 exec,并通过 holdHijackedConnection 来重定向输入输出。将 inputStream 写入到 connection,并将 response stream 重定向到 stdout,stderr。
  3. 调用 InspectExec 获取本次 exec 的运行状态和 exitcode

那么日志中打印的报错就是 response stream 传递过来的字符流。也就是说,dockerd 的 response 中包含了错误值。

此时去 docker 代码中查找原因,ExecStart 会调用到 dockerd 的以下代码:

根据上面 docker 的日志,err 的错误信息为:OCI runtime exec failed: exec failed: EOF: unknown。也就是说 ContainerExecStart 返回了错误。ContainerExecStart 会调用到 containerd.Exec,也就是 dockerd 和 containerd 之间进行通信

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// docker/libcontainerd/client_daemon.go
// Exec creates exec process.
//
// The containerd client calls Exec to register the exec config in the shim side.
// When the client calls Start, the shim will create stdin fifo if needs. But
// for the container main process, the stdin fifo will be created in Create not
// the Start call. stdinCloseSync channel should be closed after Start exec
// process.
func (c *client) Exec(ctx context.Context, containerID, processID string, spec *specs.Process, withStdin bool, attachStdio StdioCallback) (int, error) {
   ctr := c.getContainer(containerID)
   if ctr == nil {
      return -1, errors.WithStack(newNotFoundError("no such container"))
   }
   t := ctr.getTask()
   if t == nil {
      return -1, errors.WithStack(newInvalidParameterError("container is not running"))
   }

   if p := ctr.getProcess(processID); p != nil {
      return -1, errors.WithStack(newConflictError("id already in use"))
   }

   var (
      p              containerd.Process
      rio            cio.IO
      err            error
      stdinCloseSync = make(chan struct{})
   )

   fifos := newFIFOSet(ctr.bundleDir, processID, withStdin, spec.Terminal)

   defer func() {
      if err != nil {
         if rio != nil {
            rio.Cancel()
            rio.Close()
         }
      }
   }()

   p, err = t.Exec(ctx, processID, spec, func(id string) (cio.IO, error) {
      rio, err = c.createIO(fifos, containerID, processID, stdinCloseSync, attachStdio)
      return rio, err
   })
   if err != nil {
      close(stdinCloseSync)
      return -1, wrapError(err)
   }

   ctr.addProcess(processID, p)

   // Signal c.createIO that it can call CloseIO
   //
   // the stdin of exec process will be created after p.Start in containerd
   defer close(stdinCloseSync)

   if err = p.Start(ctx); err != nil {
      // use new context for cleanup because old one may be cancelled by user, but leave a timeout to make sure
      // we are not waiting forever if containerd is unresponsive or to work around fifo cancelling issues in
      // older containerd-shim
      ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
      defer cancel()
      p.Delete(ctx)
      ctr.deleteProcess(processID)
      return -1, wrapError(err)
   }

   return int(p.Pid()), nil
}

这里 new 了一个 FIFOSet,而 reading from a closed fifo 仅出现在 fifo 被 close 掉时,仍然在读取的情况。即 f.Close() 发生在 f.Read() 前面。在外层可以看到

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
defer func() {
   if err != nil {
      if rio != nil {
         rio.Cancel()
         rio.Close() // 这里 Close 会导致 fifo close
      }
   }
}()

p, err = t.Exec(ctx, processID, spec, func(id string) (cio.IO, error) {
   rio, err = c.createIO(fifos, containerID, processID, stdinCloseSync, attachStdio)
   return rio, err
})
if err != nil {
   close(stdinCloseSync)
   return -1, wrapError(err)
}

ctr.addProcess(processID, p)

// Signal c.createIO that it can call CloseIO
//
// the stdin of exec process will be created after p.Start in containerd
defer close(stdinCloseSync)

// p.Start 出错,会导致内部的 fifo 关闭,从而导致 reading from a closed fifo 的问题
if err = p.Start(ctx); err != nil {
   // use new context for cleanup because old one may be cancelled by user, but leave a timeout to make sure
   // we are not waiting forever if containerd is unresponsive or to work around fifo cancelling issues in
   // older containerd-shim
   ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
   defer cancel()
   p.Delete(ctx)
   ctr.deleteProcess(processID)
   return -1, wrapError(err)
}

p.Start 调用到下面的代码,通过 GRPC 和 containerd 通信。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// github.com/containerd/containerd/task.go
func (t *task) Start(ctx context.Context) error {
   r, err := t.client.TaskService().Start(ctx, &tasks.StartRequest{
      ContainerID: t.id,
   })
   if err != nil {
      t.io.Cancel()
      t.io.Close()
      return errdefs.FromGRPC(err)
   }
   t.pid = r.Pid
   return nil
}

这个 GRPC 调用会到达 containerd 以下的代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func (e *execProcess) start(ctx context.Context) (err error) {
   var (
      socket  *runc.Socket
      pidfile = filepath.Join(e.path, fmt.Sprintf("%s.pid", e.id))
   )
   if e.stdio.Terminal {
      if socket, err = runc.NewTempConsoleSocket(); err != nil {
         return errors.Wrap(err, "failed to create runc console socket")
      }
      defer socket.Close()
   } else if e.stdio.IsNull() {
      if e.io, err = runc.NewNullIO(); err != nil {
         return errors.Wrap(err, "creating new NULL IO")
      }
   } else {
      if e.io, err = runc.NewPipeIO(e.parent.IoUID, e.parent.IoGID, withConditionalIO(e.stdio)); err != nil {
         return errors.Wrap(err, "failed to create runc io pipes")
      }
   }
   opts := &runc.ExecOpts{
      PidFile: pidfile,
      IO:      e.io,
      Detach:  true,
   }
   if socket != nil {
      opts.ConsoleSocket = socket
   }
   // err 返回了 exec failed: EOF: unknown
   // 这里的 runtime 就是 runc 的二进制文件执行命令
   if err := e.parent.runtime.Exec(ctx, e.parent.id, e.spec, opts); err != nil {
      close(e.waitBlock)
      return e.parent.runtimeError(err, "OCI runtime exec failed")
   }

Exec 的代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// Exec executres and additional process inside the container based on a full
// OCI Process specification
func (r *Runc) Exec(context context.Context, id string, spec specs.Process, opts *ExecOpts) error {
   f, err := ioutil.TempFile(os.Getenv("XDG_RUNTIME_DIR"), "runc-process")
   if err != nil {
      return err
   }
   defer os.Remove(f.Name())
   err = json.NewEncoder(f).Encode(spec)
   f.Close()
   if err != nil {
      return err
   }
   args := []string{"exec", "--process", f.Name()}
   if opts != nil {
      oargs, err := opts.args()
      if err != nil {
         return err
      }
      args = append(args, oargs...)
   }
   cmd := r.command(context, append(args, id)...)
   if opts != nil && opts.IO != nil {
      opts.Set(cmd)
   }
   if cmd.Stdout == nil && cmd.Stderr == nil {
      data, err := cmdOutput(cmd, true)
      if err != nil {
         return fmt.Errorf("%s: %s", err, data)
      }
      return nil
   }
   ec, err := Monitor.Start(cmd)
   if err != nil {
      return err
   }
   if opts != nil && opts.IO != nil {
      if c, ok := opts.IO.(StartCloser); ok {
         if err := c.CloseAfterStart(); err != nil {
            return err
         }
      }
   }
   status, err := Monitor.Wait(cmd, ec)
   if err == nil && status != 0 {
      err = fmt.Errorf("%s did not terminate sucessfully", cmd.Args[0])
   }
   return err
}

因此是 runc 在运行后输出了 exec failed: EOF: unknown 这个错误。

将 runc 指令循环执行,可少量复现。经过排查,发现 runc exec 在运行期间会读取 container 的 state.json,并使用 json decode 时出现异常。

此时联想到开启 kubelet cpu-manager 后,会 update container,也就是更新这个 state.json 文件。导致 runc 读到了部分 cpu-manager 更新的内容。从而导致 json decode 失败。此时排查 runc EOF 和 kubelet cpu-manager update container(默认每 10s 更新一次) 的时间,发现时间点刚好吻合,验证猜想。

查看 runc 是否有修复,发现了这个 pr: https://github.com/opencontainers/runc/pull/2467。 修复思路是将 saveState 变成原子操作,这样就不会出现读取 state.json 时,读到部分写入的内容,导致 unexpected EOF (或 EOF)的问题

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 原来的
func (c *linuxContainer) saveState(s *State) error {
   f, err := os.Create(filepath.Join(c.root, stateFilename))
   if err != nil {
      return err
   }
   defer f.Close()
   return utils.WriteJSON(f, s)
}
// 修复后的
func (c *linuxContainer) saveState(s *State) (retErr error) {
        tmpFile, err := ioutil.TempFile(c.root, "state-")
        if err != nil {
                return err
        }

        defer func() {
                if retErr != nil {
                        tmpFile.Close()
                        os.Remove(tmpFile.Name())
                }
        }()

        err = utils.WriteJSON(tmpFile, s)
        if err != nil {
                return err
        }
        err = tmpFile.Close()
        if err != nil {
                return err
        }

        stateFilePath := filepath.Join(c.root, stateFilename)
        return os.Rename(tmpFile.Name(), stateFilePath)
}

五、解决

  1. 关闭cpu-manager
  2. 升级runc
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-06-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 运维开发故事 微信公众号,前往查看

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

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

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
【Pod Terminating原因追踪系列之三】让docker事件处理罢工的cancel状态码
岳家瑞,腾讯云后台开发工程师,日常负责K8s生态和运行时相关工作,包括K8s插件开发和运行时问题排查。
腾讯云原生
2020/08/31
2.1K0
golang 源码分析(16):Docker CE 18.03源码
这里做的事情可就多了,加载配置,设置相关的变量和配置,监听端口,设置信号handler, 起了API server,等待接受请求。
golangLeetcode
2022/08/02
4340
docker源码分析-Daemon创建及启动
上一篇分析了Docker Client的源码运行逻辑,本篇接着分析Docker Daemon的运行逻辑。Docker Daemon的运行逻辑很复杂,大家看着来要有耐心了。 Docker Daemon的执行 Docker Daemon的入口在cmd/dockerd/docker.go,先看main函数。 func main() { if reexec.Init() { return } // Set terminal emulation based on platform as required.
jeremyxu
2018/05/10
2.1K0
Dockerd 资源泄露如何处理?通过现象看问题的本质
线上 k8s 集群报警,宿主 fd 利用率超过 80%,登陆查看 dockerd 内存使用 26G
二哥聊运营工具
2021/12/17
7440
Dockerd 资源泄露如何处理?通过现象看问题的本质
运维锅总详解容器OCI规范
OCI是什么?OCI的镜像规范和运行时规范有哪些具体内容?Docker实现了OCI规范了吗?实现OCI规范的开源项目有哪些?OCI诞生背景及历史演进又有哪些内容?希望读完本文,能帮您解答这些疑惑!
锅总
2024/07/20
2490
运维锅总详解容器OCI规范
迪奥布兰度正在挑战fgo 小说_god eater resurrection
在godis中,只有aof持久化,而没有rdb持久化。aof持久化分为三个基本的模块:
全栈程序员站长
2022/11/10
4550
解决国产系统 Docker 拉取大镜像卡顿之谜
离线文件 load -i 后,打上 tag 推送到镜像仓库,然后本地删除这个镜像,然后拉取还是像上面这样卡住,部分小镜像拉取没问题,所以不可能是 docker data-root 的挂载 option 影响。环境信息如下:
米开朗基杨
2023/09/09
1.6K0
解决国产系统 Docker 拉取大镜像卡顿之谜
containerd了解
I recently had a need to manually load some container images into a Linux system running containerd (instead of Docker) as the container runtime. I say “manually load some images” because this system was isolated from the Internet, and so simply running a container and having containerd automatically pull the image from an image registry wasn’t going to work. The process for working around the lack of Internet access isn’t difficult, but didn’t seem to be documented anywhere that I could readily find using a general web search. I thought publishing it here may help individuals seeking this information in the future.
heidsoft
2022/01/13
6820
containerd了解
Containerd深度剖析-Diff上篇
Containerd提供容器进程的管理,镜像的管理,文件系统快照以及元数据和依赖管理,关于Containerd的介绍,可以参看前文,Containerd深度剖析-runtime篇,本文将为从代码层面分析 Containerd diff 服务的实现逻辑
zouyee
2023/02/06
6290
Containerd深度剖析-CRI篇
目前我司现网的K8s集群的运行时已经完成从docker到Containerd的切换,有小伙伴对K8s与Containerd调用链涉及的组件不了解,其中Containerd和RunC是什么关系,docker和containerd又有什么区别,以及K8s调用Containerd创建容器又是怎样的流程,最终RunC又是如何创建容器的,诸如此类的疑问。本文就针对K8s使用Containerd作为运行时的整个调用链进行介绍和源码级别的分析。
zouyee
2023/01/31
1.5K0
Containerd深度剖析-CRI篇
containerd源码分析
本文是对containerd v0.2.4的源码分析。 ##Containerd源码流程图 源码接口调用详情 从ctr调用containerd-api ####checkpoint(用于快照,doc
Walton
2018/04/13
3K0
containerd源码分析
【Pod Terminating原因追踪系列】之 containerd 中被漏掉的 runc 错误信息
李志宇,腾讯云后台开发工程师,日常负责集群节点和运行时相关的工作,以及 containerd、docker、runc 等运行时组件的定制开发和问题排查。
腾讯云原生
2020/08/17
4.9K0
我的 Docker 卡死了,怎么办?在线等
最近升级了一版 kubelet,修复因 kubelet 删除 Pod 慢导致平台删除集群超时的问题。在灰度 redis 隔离集群的时候,发现升级 kubelet 并重启服务后,少量宿主状态变成了 NotReady,并且回滚 kubelet 至之前版本,宿主状态仍然是 NotReady。查看宿主状态时提示 ‘container runtime is down’ ,根据经验,此时一般就是容器运行时出了问题。弹性云使用的容器运行时是 docker,我们就去检查 docker 的状态,检测结果如下:
米开朗基杨
2020/10/30
7.6K0
docker v1.11 源码重构分析
基于docker v1.12的源代码,对docker engine v1.11中重构后的源码结构进行分析,涵盖dockerd, containerd, containerd-shim, runC。 ##docker1.11新特性 docker在v1.11版本进行了重大的重构,对docker engine和container进行了解耦,docker engine运行在containerd上,containerd运行在runC上,通过containerd-shim中间层进行了解耦。之前的docker engin
Walton
2018/04/13
1.6K0
docker v1.11 源码重构分析
在 Kubernetes 实施混沌工程 —— Chaos Mesh 原理分析与控制面开发
作者:黄涵(Mayo Cream)[1],CNCF TAG Security 成员,云原生社区贡献者。
CNCF
2021/11/01
1.3K0
ignite
Ignite是一个启动firecracker vm的引擎,它使用容器的方式承载了firecracker vm。目前项目处于停滞阶段,也比较可惜,通过阅读了解ignite的工作方式,学习到了很多,希望能借此维护该项目。
charlieroro
2023/08/17
1.1K0
ignite
Docker源码分析之容器日志处理与log-driver实现
概要 本文将从docker(1.12.6)源码的角度分析docker daemon怎么将容器的日志收集出来并通过配置的log-driver发送出去,并结合示例介绍了好雨云帮中实现的一个zmq-loger。阅读本文,你也可以实现适合自己业务场景的log-driver。 阅读准备 本文适合能够阅读和编写golang代码的同学。 (1)首先你需要认知以下几个关键词: stdout: 标准输出,进程写数据的流。 stderr: 错误输出,进程写错误数据的流。 子进程: 由一个进程(父进程)创建的进程,集成父
Rainbond开源
2018/05/31
1.4K0
容器开启特权模式后无法通过cadvisor获取GPU metrics指标
开启特权模式(--privileged)的容器,在使用nvidia GPU时,无法通过cAdvisor获取GPU相关的metrics信息。Google大法可以搜到相关的Issue,于2018年提出,至今仍处于Open状态(给cAdvisor贡献代码的机会),由于涉及到的内容较多,分为三篇来讲。
李鹤
2023/03/28
4360
容器开启特权模式后无法通过cadvisor获取GPU metrics指标
容器能不能将 volume 挂载直接挂到根目录?—— 浅析 kubelet 到 runc 的调用过程
这件事起源于有小伙伴在某群里问,在 K8s 中,能不能把 volume 挂载直接挂到根目录?我的第一反应是不能。容器会使用 union filesystem 将容器的内容挂到根目录下,这点在正常情况下是无法更改的。但是就止于此吗?发现给不出合理解释的时候,突然感觉自己对于容器的认知只停留在了很表面的阶段。
腾讯云 CODING
2023/03/31
1.3K0
容器能不能将 volume 挂载直接挂到根目录?—— 浅析 kubelet 到 runc 的调用过程
云原生|k8s pod 的删除过程以及信号处理
m.killContainer --> m.internalLifecycle.PreStopContainer-->m.runtimeService.StopContainer
heidsoft
2022/06/09
9480
云原生|k8s pod 的删除过程以及信号处理
推荐阅读
相关推荐
【Pod Terminating原因追踪系列之三】让docker事件处理罢工的cancel状态码
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
查看详情【社区公告】 技术创作特训营有奖征文
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验