写点什么

我想 Debug 容器运行时

作者:黑客不够黑
  • 2023-12-19
    江苏
  • 本文字数:3319 字

    阅读完需:约 11 分钟

我想 Debug 容器运行时

出于好奇,我想弄明白 Containerd 是如何处理容器 stdio 的,因为我在 k8s 环境中使用 Containerd 作为容器运行时,且观察到容器中的标准 io 为如下形式:


/proc/1/fd # ls -ltotal 0lrwx------    1 root     root            64 Oct 10 02:25 0 -> /dev/nulll-wx------    1 root     root            64 Oct 10 02:25 1 -> pipe:[45731]l-wx------    1 root     root            64 Oct 10 02:25 2 -> pipe:[45732]
复制代码


这是个非常简单的容器,只是往标准输出打印一些内容。从容器进程打开的文件描述符中,我们看到其标准输入被丢弃(通过重定向到 /dev/null ),而标准输出和标准错误都被重定向到 Linux 管道


是的,我对 Containerd 如何做到这一点有着浓厚的兴趣,这驱使我产生了 Debug 容器运行时的念头。


这篇短文仅仅介绍我如何达到 Debug 的目的,并不会介绍 Containerd 如何设置标准 io,这部分内容我会放在接下来想写的《终端闲思录》里面。

Containerd 的生态链位置

首先应该明确一下 Containerd 在整个容器调用链里的位置,我借用一幅容器技术架构图来说明:



从图中可知,Containerd 通过 CRI 接口上承 k8s,通过 OCI 接口下接 runc,真正创建容器和启动容器的是底层的 runc,所有的 CRI 容器运行时均依赖于 runc 。


你可能已经被 OCI 和 CRI 绕晕了,简言之,这是容器圈里的两大标准:


  • Open Container Initiative (OCI): a set of standards for containers, describing the image format, runtime, and distribution.

  • Container Runtime Interface (CRI) in Kubernetes: An API that allows you to use different container runtimes in Kubernetes.


只要记住 OCI 是容器标准,包扩容器创建,镜像格式等等,而 CRI 是属于 k8s 的接口,每个对接 k8s 的容器运行时都要实现这一套接口才能被 kubelet 调用。

Debug 前的准备

我 Debug 的对象就是实现了 CRI 的 Containerd,但我却面临两个问题:


  1. Containerd 需要 root 权限运行,而我并不想在 root 用户下再配置一次 ide 。

  2. 虽然可以在 root 下使用 delve 直接调试二进制文件,但我并不清楚 kubelet 调用 Containerd 时传递的参数,而我 Debug 的初衷就是想弄清楚 kubelet 传递的参数内容。


所以,我决定 Debug 一个运行中的 Containerd!


这很容易做到,使用 dlv attach pid就可以 Debug Containerd 进程。万事俱备,只欠一套 k8s 环境。所幸,我们有 minikube !


minikube 是在一个虚拟机中运行所有相关组件的,所以有两个问题需要解决:


  1. 在 minikube 的虚拟机中准备 delve 工具。

  2. 准备一个可调试的 Containerd 服务。


第一点很好解决,minikube 虚拟机和 Host 之间是通过一个网桥通信的,使用minikube ssh命令登录到 minikube 虚拟机中,scp 宿主机的工具到 minikube 中就可以了。


通过软件仓库安装的 Containerd 都是经过编译优化的二进制文件,里面缺乏调试信息,因此我们需要编译一份带有调试信息的二进制文件,观察 Containerd 的 makefile 可以发现如下内容:


ifndef GODEBUG    EXTRA_LDFLAGS += -s -w    DEBUG_GO_GCFLAGS :=    DEBUG_TAGS :=else    DEBUG_GO_GCFLAGS := -gcflags=all="-N -l"    DEBUG_TAGS := static_buildendif
复制代码


如果环境变量没有设置 GODEBUG ,那么就启用编译优化,否则增加调试信息,所以我们在 make 之前,执行一下export GODEBUG=1即可编译出可调试的二进制文件了。编译完成后,使用 scp 拷贝到 minikube 中,重新启动 Containerd 即可。


delve 读取的本地文件系统中项目的内容用于显示单步中的代码,所以,还需要将编译时所用的源码 copy 到 minikube 中,并保持原有路径!

开始 Debug

上述准备工作完成后,就可以进入 minikube 虚拟机进行 debug 了:


richard@Richard-Manjaro:~ » minikube sshdocker@minikube:~$ sudo -iroot@minikube:~# ps -ef|grep containerd|grep -v shimroot         504       1  3 06:47 ?        00:00:02 /usr/bin/containerd -l debugroot         873       1  3 06:48 ?        00:00:02 /var/lib/minikube/binaries/v1.27.4/kubelet --bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf --config=/var/lib/kubelet/config.yaml --container-runtime-endpoint=unix:///run/containerd/containerd.sock --hostname-override=minikube --kubeconfig=/etc/kubernetes/kubelet.conf --node-ip=192.168.49.2root@minikube:~# dlv attach 504Type 'help' for list of commands.(dlv) bpBreakpoint runtime-fatal-throw (enabled) at 0x559126263aa4,0x559126263b84 for (multiple functions)() <multiple locations>:0 (0)Breakpoint unrecovered-panic (enabled) at 0x559126263f04 for runtime.fatalpanic() /usr/lib/go/src/runtime/panic.go:1188 (0)    print runtime.curg._panic.arg(dlv) b pkg/cri/server/container_create.go:222Breakpoint 1 set at 0x559127d44f4d for github.com/containerd/containerd/pkg/cri/server.(*criService).CreateContainer() /home/richard/opensource/containerd/pkg/cri/server/container_create.go:222(dlv) b services/tasks/local.go:167Breakpoint 2 set at 0x55912734f072 for github.com/containerd/containerd/services/tasks.(*local).Create() /home/richard/opensource/containerd/services/tasks/local.go:167(dlv) bpBreakpoint runtime-fatal-throw (enabled) at 0x559126263aa4,0x559126263b84 for (multiple functions)() <multiple locations>:0 (0)Breakpoint unrecovered-panic (enabled) at 0x559126263f04 for runtime.fatalpanic() /usr/lib/go/src/runtime/panic.go:1188 (0)    print runtime.curg._panic.argBreakpoint 1 (enabled) at 0x559127d44f4d for github.com/containerd/containerd/pkg/cri/server.(*criService).CreateContainer() /home/richard/opensource/containerd/pkg/cri/server/container_create.go:222 (0)Breakpoint 2 (enabled) at 0x55912734f072 for github.com/containerd/containerd/services/tasks.(*local).Create() /home/richard/opensource/containerd/services/tasks/local.go:167 (0)(dlv) c> github.com/containerd/containerd/services/tasks.(*local).Create() /home/richard/opensource/containerd/services/tasks/local.go:167 (hits goroutine(2264):1 total:1) (PC: 0x55912734f072)   162:    monitor   runtime.TaskMonitor   163:    v2Runtime runtime.PlatformRuntime   164:  }   165:     166:  func (l *local) Create(ctx context.Context, r *api.CreateTaskRequest, _ ...grpc.CallOption) (*api.CreateTaskResponse, error) {=> 167:    container, err := l.getContainer(ctx, r.ContainerID)   168:    if err != nil {   169:      return nil, errdefs.ToGRPC(err)   170:    }   171:    checkpointPath, err := getRestorePath(container.Runtime.Name, r.Options)   172:    if err != nil {(dlv) 
复制代码


进入虚拟机后,切换到 root 执行 delve 指令 dlv attach 504,我在此设置了两个断点:


  1. pkg/cri/server/container_create.go:222 这是 Containerd 的 CRI 实现里创建容器的接口函数,此函数隶属于一个 grpc server,当我们使用 kubectl 创建工作负载时,会触发此断点。

  2. services/tasks/local.go:167 这是 Containerd 将 CRI 请求转化为任务事件的创建入口。


使用 kubectl 提交一个负载就可以单步调试啦 !O(∩_∩)O~

一些注意事项

  1. 由图 1-1 可知,k8s 使用 CRI 与容器运行时交互,所以确保 minikube 的运行时使用 Containerd,而不是 docker。docker 虽然也会使用 Containerd,但这种情况下 k8s 不会走 Containerd 的 CRI 接口,相反走的是 docker-shim 的接口,而后者已在 k8s 1.24 中被抛弃。

  2. 如果想通过 Containerd 的日志进行简单的 debug,那么开启 debug 日志即可,方法为在执行文件后追加 —log-level=debug

  3. 一定要在你的玩具环境中调试,任何公共环境都不应被用来 debug !


参考文献


  1. The differences between Docker, containerd, CRI-O and runc


发布于: 刚刚阅读数: 4
用户头像

感而后应,迫而后动,不得已而后起 2018-11-20 加入

非著名程序员,任职过测试,前端,devops,DBA、Go 后端开发等等 个人网站: https://liupzmin.com 联系方式: liupzmin@gmail.com

评论

发布
暂无评论
我想 Debug 容器运行时_Containerd_黑客不够黑_InfoQ写作社区