在 containerd 的实现中,容器会区分为 container 和 task,其中 container 是容器的元信息,而 task 是实际运行的容器进程。
对应到常用的命令行工具:
- crictl - 匹配 cri 实现的命令行工具,可以查看 Pod、容器信息,但是看不到 task 这一级别
- ctr - containerd 的工具,可以查看 container 和 task 信息
同时,containerd 也有 namespace 的概念,而 k8s 的容器统一在 k8s.io ns 下,是写死的常量,因此,有如下命令:
ctr -n k8s.io t ls # 列出节点上正在运行的容器进程
ctr -n k8s.io c ls # 列出节点上的容器
ctr -n k8s.io t exec -t --exec-id 1 <tskid> sh # 进入容器执行命令
参考:containerd/docs/getting-started.md at main · containerd/containerd
containerd 通过 containerd-shim 进程来操作容器进程,因为容器进程是需要一个父进程来做状态收集、维持 stdin 等 fd 打开等工作,假如这个父进程就是 containerd,那如果 containerd 挂掉的话,这些功能就都不可用了,通过 containerd-shim 这个中间层可以避免这些问题。
然后创建容器需要做一些 namespaces 和 cgroups 的配置,以及挂载 root 文件系统等操作,这些操作其实已经有了标准的规范,那就是 OCI(开放容器标准),runc 就是它的一个参考实现,这个标准其实就是一个文档,主要规定了容器镜像的结构、以及容器需要接收哪些操作指令, 比如 create、start、stop、delete 等这些命令。runc 就可以按照这个 OCI 文档来创建一个符合规范的容器,既然是标准肯定就有其他 OCI 实现,比如 Kata、gVisor 些容器运行时都是符合 OCI 标准的。
所以真正启动容器是通过 containerd-shim 去调用 runc 来启动容器的,runc 启动完容器后本身会直接退出,containerd-shim 则会成为容器进程的父进程, 负责收集容器进程的状态, 上报给 containerd, 并在容器中 pid 为 1 的进程退出后接管容器中的子进程进行清理, 确保不会出现僵尸进程。
参考:OCI 与 CRI - Kubernetes 进阶训练营(第2期)
☞ https://github.com/containerd/containerd/blob/v1.6.19/pkg/cri/server/container_create.go
配置例:
# Kubernetes doesn't use containerd restart manager.
root = "/var/lib/containerd"
state = "/run/containerd"
[plugins.linux]
shim = "/usr/bin/containerd-shim"
runtime = "/usr/bin/runc"
[plugins.cri.containerd.default_runtime]
runtime_type = "io.containerd.runtime.v1.linux"
runtime_engine = ""
runtime_root = ""
步骤:
- 获取 Sandbox 元信息,生成容器 ID
- 创建容器 root 目录,这里会用到
/etc/containerd/config.toml
中的root
配置// Create container root directory. containerRootDir := c.getContainerRootDir(id) if err = c.os.MkdirAll(containerRootDir, 0755); err != nil { return nil, fmt.Errorf("failed to create container root directory %q: %w", containerRootDir, err) }
- 创建容器临时目录,这里会用到
/etc/containerd/config.toml
中的state
配置volatileContainerRootDir := c.getVolatileContainerRootDir(id) if err = c.os.MkdirAll(volatileContainerRootDir, 0755); err != nil { return nil, fmt.Errorf("failed to create volatile container root directory %q: %w", volatileContainerRootDir, err) }
- 获取容器 RuntimeClass,生成容器 spec 信息
ociRuntime, err := c.getSandboxRuntime(sandboxConfig, sandbox.Metadata.RuntimeHandler) spec, err := c.containerSpec(id, sandboxID, sandboxPid, sandbox.NetNSPath, containerName, containerdImage.Name(), config, sandboxConfig, &image.ImageSpec.Config, append(mounts, volumeMounts...), ociRuntime)
- 创建 ContainerIO 对象,看着会关联 tty 和 stdin,会在容器临时目录创建 fifoset 相关文件
containerIO, err := cio.NewContainerIO(id, cio.WithNewFIFOs(volatileContainerRootDir, config.GetTty(), config.GetStdin()))
- 调用 ContainerService(感觉是 containerd-shim)创建容器,同时保存容器信息到 containerstore
if cntr, err = c.client.NewContainer(ctx, id, opts...); err != nil { return nil, fmt.Errorf("failed to create containerd container: %w", err) } container, err := containerstore.NewContainer(meta, containerstore.WithStatus(status, containerRootDir), containerstore.WithContainer(cntr), containerstore.WithContainerIO(containerIO), )
☞ https://github.com/containerd/containerd/blob/v1.6.19/pkg/cri/server/container_start.go
containerd 中的逻辑其实不多,就是根据 sandbox、container、runtime 等元信息调用 TaskService 来创建 Task,同时更新 containerstore 中的容器状态信息。