背景
最近看一些社区 issue 中正好接触到 containerd 源码, 早年大家基本都使用 docker/podman 这类容器上层包装, 包括我也是, 故对真正容器运行时的结构部分了解得不够深入, 最近 k8s 社区在 1.21 规划上计划从 kubelet 中移除 docker-shim 交互的逻辑, 大势上组件走上 containerd-shim, 故需要着手对容器运行时更深入了解.
带着问题作为引入,
容器底层还是对 Linux LXC 的交互, 那究竟 docker daemon 中是哪个组件来完成这一步的呢?
1、背景知识
首先我们需要了解 Docker Daemon 生产出容器的过程.
当前整个 Docker 调用链架构可以用下图来简单概括
[站外图片上传中...(image-1c45f5-1618194667876)]
从 Docker 1.11 之后,Docker Daemon 被分成了多个模块以适应 OCI 标准。拆分之后,结构分成了以下几个部分:
16年12月 Docker公司宣布将 containerd 从Docker Engine中分离,并捐赠到开源社区独立发展和运营。
一个工业标准的容器运行时,注重简单、 健壮性、可移植性
Docker本身其实已经被剥离干净了,只剩下 Docker 自身作为 cli 的一些特色功能了,真正容器的管控都在 containerd 里实现。
在 17 年 docker重命名为moby, 从命名上逐渐脱离和 container 的关系, 而 Moby 更像是一个“乐高积木”,它包含了容器化的组件库 底层的构建、日志处理、卷管理、网络、镜像管理、containerd、SwarmKit等模块, 是个大杂烩.
19年2月containerd 正式从cncf社区毕业,成为继 Kubernetes, Prometheus, Envoy, and CoreDNS 之后第五个毕业项目。
2、生态架构
单看 docker 生态圈是非常庞大的, 故今天我们聚焦在运行时 containerd 生态架构上, 周边生态如下:
- OCI
- CRI
- kubelet
- dockerd
- docker.sock (/var/run/docker.sock)
- dockershim
- dockershim.sock (/var/run/dockershim.sock)
- containerd-cri
- containerd.sock (/var/run/docker/containerd/containerd.sock|/run/containerd/containerd.sock)
- containerd-shim
- Runc
- RunV
- Kata
- gVisor
containerd 在容器生态中的角色, 作为容器运行时的扛把子
3、containerd 架构
简单来说分为:
- containerd-shim
- runC
- LXC 调用封装
更细化为
grpc 模块向上层提供服务接口,metrics 则提供监控数据(cgroup 相关数据),两者均向上层提供服务。containerd 包含一个守护进程,该进程通过本地 UNIX 套接字暴露 grpc 接口。
storage 部分负责镜像的存储、管理、拉取等metadata 管理容器及镜像的元数据,通过bootio存储在磁盘上task -- 管理容器的逻辑结构,与 low-level 交互event -- 对容器操作的事件,上层通过订阅可以知道发生了什么事情Runtimes -- low-level runtime(对接 runC)
containerd-shim
containerd-shim 是 containerd 的一个组件,主要是用于剥离 containerd 守护进程与容器进程。containerd 通过 shim 调用 runc 的包函数来启动容器.
各个组件以插件的形式注册
cri /run/containerd/containerd.sock
- containers/tasks/event/snapshots/namespace/tasks/image /run/containerd/containerd.sock
- 低内存环境 /run/containerd/containerd.sock.ttrpc
- debug /run/containerd/debug.sock /debug/pprof
- metrics metrics.sock /v1/metrics
- bolt 元数据存储 和etcd底层使用相同结构
直接启动containerd和moby启动方式
- 检测 /run/containerd/containerd.sock是否存在,判断是否启动containerd
- 用supervisor启动containerd,直接二进制调用
容器的 namespace
容器 ns 主要目的用于在 Linux 层面做 namespace 划分, 故拆分为一下3种主流 namespace.
目前最常见的 namespace type 主要分下面两种:
io.kubernetes.cri.container-type
io.kubernetes.docker.type
containerd-shim 容器生命周期管理
containerd-shim 类似 docker-shim 也是一层 gRPC 交互层, 提供对下层运行时的标准化 api 接口.
其特性有
- 允许 runC 在创建&运行容器之后退出
- 用 shim 作为容器的父进程,而不是直接用 containerd 作为容器的父进程,是为了防止这种情况:当 containerd 挂掉的时候,shim 还在,因此可以保证容器打开的文件描述符不会被关掉
- 依靠 shim 来收集&报告容器的退出状态,这样就不需要 containerd 来wait 子进程
使用 shim 的主要作用,就是将 containerd 和真实的容器(里的进程)解耦。
回过来看特性第一点,为什么要允许 runc 退出呢? 因为,Go 编译出来的二进制文件,默认是静态链接,因此,如果一个机器上起N个容器,那么就会占用M*N的内存,其中M是一个 runc 所消耗的内存。 但是出于上面描述的原因又不想直接让 containerd 来做容器的父进程,因此,就需要一个比 runc 占内存更小的东西来作父进程,也就是 shim。
与 OCI 组件的交互
runC 是标准化的产物,为了防止一家商业公司主导容器化标准,因此又成立了 opencontainers 组织.
有了 runC 后, OCI 对容器 runtime 的标准所定义的指定容器的运行状态和 runtime 需要提供的命令得以打通.
runC 有以下特性:
- 构建出的二进制直接调用
- 用于更新 dockerd 配置文件 config.v2.json, hostconfig.json 文件
- 提供了 k8s runtime class
- 与 OCI 交互标准化
docker-init
UNIX系统中,1号进程是init进程,也是所有孤儿进程的父进程。而使用 docker 时,如果不加 --init 参数,容器中的1号进程 就是所给的ENTRYPOINT,例如下面例子中的 sh。而加上 --init 之后,1号进程就会是 tini:
故在 docker 中启动容器时默认会走 docker-init 进程,其作用为
- 避免僵尸进程
- 默认信号处理