Contents

Talk about Containerd

背景

最近看一些社区 issue 中正好接触到 containerd 源码, 早年大家基本都使用 docker/podman 这类容器上层包装, 包括我也是, 故对真正容器运行时的结构部分了解得不够深入, 最近 k8s 社区在 1.21 规划上计划从 kubelet 中移除 docker-shim 交互的逻辑, 大势上组件走上 containerd-shim, 故需要着手对容器运行时更深入了解.

带着问题作为引入,

容器底层还是对 Linux LXC 的交互, 那究竟 docker daemon 中是哪个组件来完成这一步的呢?

1、背景知识

首先我们需要了解 Docker Daemon 生产出容器的过程.

当前整个 Docker 调用链架构可以用下图来简单概括

https://miro.medium.com/max/1400/1*c3AiZFHuib7FUGyINzkEag.png

从 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、生态架构

http://cdn.zimug.com/Docker%E7%94%9F%E6%80%81%E7%B3%BB%E7%BB%9F%E5%85%A8%E8%A7%A3%E6%9E%90docker%E7%94%9F%E6%80%81%E7%B3%BB%E7%BB%9F.png

单看 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 在容器生态中的角色, 作为容器运行时的扛把子

https://yqfile.alicdn.com/6ff06eb7bacf309994df662138197cbadfd49344.png

3、containerd 架构

http://lexus-blog.test.upcdn.net/containerd/821618039890_.pic_hd.jpg

简单来说分为:

  • containerd-shim
  • runC
  • LXC 调用封装

更细化为

https://images2018.cnblogs.com/blog/952033/201805/952033-20180520115610144-588472749.png

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 的包函数来启动容器.

各个组件以插件的形式注册

https://oscimg.oschina.net/oscnet/34d6fddfefcff0eb7fef5542709e9006c4a.jpg

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

https://oscimg.oschina.net/oscnet/50bb0c5ab1e69aa56caf4fca07a05db3974.jpg

容器 ns 主要目的用于在 Linux 层面做 namespace 划分, 故拆分为一下3种主流 namespace.

目前最常见的 namespace type 主要分下面两种:

  • io.kubernetes.cri.container-type

  • io.kubernetes.docker.type

containerd-shim 容器生命周期管理

  • 允许 runc 在创建&运行容器之后退出
  • 用 shim 作为容器的父进程,而不是直接用 containerd 作为容器的父进程,是为了防止这种情况:当 containerd 挂掉的时候,shim 还在,因此可以保证容器打开的文件描述符不会被关掉
  • 依靠 shim 来收集&报告容器的退出状态,这样就不需要 containerd 来wait 子进程

因此,使用 shim 的主要作用,就是将 containerd 和真实的容器(里的进程)解耦。

而第一点,为什么要允许 runc 退出呢? 因为,Go 编译出来的二进制文件,默认是静态链接,因此,如果一个机器上起N个容器,那么就会占用M*N的内存,其中M是一个 runc 所消耗的内存。 但是出于上面描述的原因又不想直接让 containerd 来做容器的父进程,因此,就需要一个比 runc 占内存更小的东西来作父进程,也就是 shim。

shimv2 启动一个服务,复用

与OCI组件的交互

runC 是标准化的产物,为了防止一家商业公司主导容器化标准,因此又成立了opencontainers 组织

二进制直接调用

更新 config.v2.json, hostconfig.json 文件

k8s runtime class

docker-init

UNIX系统中,1号进程是init进程,也是所有孤儿进程的父进程。而使用 docker 时,如果不加 –init 参数,容器中的1号进程 就是所给的ENTRYPOINT,例如下面例子中的 sh。而加上 –init 之后,1号进程就会是 tini

docker-init 作用

  • 避免僵尸进程
  • 默认信号处理