containerd 全面攻略

containerd简介

Containerd是一个工业标准的容器运行时,重点是它简洁,健壮,便携,在Linux和window上可以作为一个守护进程运行,它可以管理主机系统上容器的完整的生命周期:镜像传输和存储,容器的执行和监控,低级别的存储和网络。
containerd和docker不同,containerd重点是继承在大规模的系统中,例如kubernetes,而不是面向开发者,让开发者使用,更多的是容器运行时的概念,承载容器运行。

1 Containerd 架构图

containerd 全面攻略_第1张图片

2 Containerd 作用
  • 管理容器的生命周期(从创建容器到销毁容器)
  • 拉取/推送容器镜像
  • 存储管理(管理镜像及容器数据的存储)
  • 调用 runC 运行容器(与 runC 等容器运行时交互)
  • 管理容器网络接口及网络
3 为什么需要 Containerd
  • 继续从整体 docker 引擎中分离出的项目(开源项目的思路)
  • 可以被 Kubernets CRI 等项目使用(通用化)
  • 为广泛的行业合作打下基础(就像 runC 一样)
4 Containerd 的技术方向
  • 简洁的基于 gRPC 的 API 和 client library
  • 完整的 OCI 支持(runtime 和 image spec)
  • 同时具备稳定性和高性能的定义良好的容器核心功能
  • 一个解耦的系统(让 image、filesystem、runtime 解耦合),实现插件式的扩展和重用

下图展示了 containerd 的技术架构
containerd 全面攻略_第2张图片
在架构设计和实现方面,核心开发人员在他们的博客里提到了通过反思 graphdriver 的实现,他们将 containerd 设计成了 snapshotter 的模式,这也使得 containerd 对于 overlay 文件系、snapshot 文件系统的支持比较好。
storage、metadata 和 runtime 的三大块划分非常清晰,通过抽象出 events 的设计,containerd 也得以将网络层面的复杂度交给了上层处理,仅提供 network namespace 相关的一些接口添加和配置 API。这样做的好处无疑是巨大的,保留最小功能集合的纯粹和高效,而将更多的复杂性及灵活性交给了插件及上层系统。

5 Containerd 的愿景目标

containerd 全面攻略_第3张图片

当 containerd 和 runC 成为标准化容器服务的基石后,上层的应用就可以直接建立在 containerd 和 runC 之上。上图中展示的容器平台都已经支持 containerd 和 runC 的组合了,相信接下来会有更多类似的容器平台出现。
注意: Containerd 被设计成嵌入到一个更大的系统中,而不是直接由开发人员或终端用户使用。所以 containerd 具有宏大的愿景

安装 containerd

注意:containerd 需要调用 runC,所以在安装 containerd 之前请先安装 runC。RunC 的安装请参考笔者博文《RunC 简介》。

1 下载并解压 containerd 程序
# 从 github 上下载 containerd 包,当前的最新版本为 v1.1.0,然后把下载到的压缩包解压到 /usr/local 目录下:
cd /home/work
wgt https://github.com/containerd/containerd/releases/download/v1.3.9/containerd-1.3.9-linux-amd64.tar.gz
sudo tar -C /usr/local -xf containerd-1.3.9-linux-amd64.tar.gz

containerd 全面攻略_第4张图片

  • containerd:即容器的运行时,以 gRPC 协议的形式提供满足 OCI 标准的 API
  • containerd-release 和 containerd-stress:分别是 containerd 项目的发行版发布工具以及压力测试工具
  • containerd-shim:这是每一个容器的运行时载体,我们在 docker 宿主机上看到的 shim 也正是代表着一个个通过调用 containerd 启动的 docker 容器。
  • ctr:它是一个简单的 CLI 接口,用作 containerd 本身的一些调试用途,投入生产使用时还是应该配合docker 或者 cri-containerd 部署
2 生成 containerd 配置文件

Containerd 的配置文件默认为 /etc/containerd/config.toml。这里我们可以通过命令来生成一个默认的配置文件:

sudo su
mkdir /etc/containerd
./containerd config default > /etc/containerd/config.toml
cat /etc/containerd/config.toml
3 配置 containerd 作为服务运行

创建文件 containerd.service:

sudo touch /lib/systemd/system/containerd.service
vi /lib/systemd/system/containerd.service

/lib/systemd/system/containerd.service 文件如下:

[Unit]
Description=containerd container runtime
Documentation=https://containerd.io
After=network.target

[Service]
ExecStartPre=/sbin/modprobe overlay
ExecStart=/usr/local/bin/containerd
Delegate=yes
KillMode=process
LimitNOFILE=1048576
# Having non-zero Limit*s causes performance problems due to accounting overhead
# in the kernel. We recommend using cgroups to do container-local accounting.
LimitNPROC=infinity
LimitCORE=infinity

[Install]
WantedBy=multi-user.target

执行下面的命令启动 containerd 服务并查看服务的状态:

sudo systemctl daemon-reload

sudo systemctl enable containerd.service
sudo systemctl start containerd.service
sudo systemctl status containerd.service

containerd 全面攻略_第5张图片
至此 containerd 已经安装成功!

Containerd 开发

可以使用类似 runC 的方式运行容器,也就是使用现成的客户端工具 ctr,由于用法与 runC 非常相似,所以这里不再赘述。Containerd 还提供了 client package 用于在代码中集成 containerd 客户端,下面的 demo 就采用 golang 和 client package 在代码中访问 containerd 服务来创建并运行容器!

1 连接 containerd 服务

创建 main.go 文件,内容如下:

package main

import (
    "log"
    "github.com/containerd/containerd"
)

func main() {
    if err := redisExample(); err != nil {
        log.Fatal(err)
    }
}

func redisExample() error {
    client, err := containerd.New("/run/containerd/containerd.sock")
    if err != nil {
        return err
    }
    defer client.Close()
    return nil
}

上文代码中使用默认的 containerd 套接字创建了一个客户端对象。因为 containerd daemon 通过 gRPC 协议提供服务,所以我们需要创建一个用于调用客户端方法的上下文。在创建上下文之后,我们还应该为我们的 demo 设置一个 namespace,创建单独的 namespace 可以与用户的资源进行隔离以免发生冲突:

ctx := namespaces.WithNamespace(context.Background(), "demo")
2 拉取 redis 镜像

在创建客户端对象后我们就可以从 dockerhub 上拉取容器镜像了,这里我们拉取一个 redis 镜像:

image, err := client.Pull(ctx, "docker.io/library/redis:alpine", containerd.WithPullUnpack)

if err != nil {
    return err
}

使用客户端的 Pull 方法从 dockerhub 上拉取 redis 镜像,这个方法支持 Opts 模式,所以我们可以指定 containerd.WithPullUnpackso 让下载完成后直接把镜像解压缩为一个 snapshotter 作为即将运行的容器的 rootfs。

3 创建 OCI Spec 和容器

有了 rootfs 还需要运行 OCI 容器所需的 OCI runtime spec,我们通过 NewContainer 方法可以使用默认的 OCI runtime spec 直接创建容器对象。当然,也可以通过 Opts 模式的参数修改默认值:

container, err := client.NewContainer(
    ctx,
    "redis-server",
    containerd.WithImage(image),
    containerd.WithNewSnapshot("redis-server-snapshot", image),
    containerd.WithNewSpec(oci.WithImageConfig(image)),
)
if err != nil {
    return err
}
defer container.Delete(ctx, containerd.WithSnapshotCleanup)

当我们为容器创建一个 snapshot 时需要提供 snapshot 的 ID及其父镜像。通过提供一个单独的 snapshot ID,而不是容器 ID,我们可以轻松地在不同的容器中重用现有的 snapshot。在完成这个示例之后,我们还添加了 defer container.Delete 调用来删除容器以及它的快照。

#####- 4 创建运行容器的 task
一个 container 对象只是包含了运行一个容器所需的资源及配置的数据结构,一个容器真正的运行起来是由 Task 对象实现的:

task, err := container.NewTask(ctx, cio.NewCreator(cio.WithStdio))

if err != nil {
    return err
}
defer task.Delete(ctx)

此时容器的状态相当于我们在《RunC 简介》一文中介绍的 “created”。这意味着 namespaces、rootfs 和容器的配置都已经初始化成功了,只是用户进程(这里是 redis-server)还没有启动。在这个时机,我们可以为容器设置网卡,还可以配置工具来对容器进行监控等。

#####5 运行中的 task 退出
当要结束容器的运行时,可以调用 task.Kill 方法。其实就是向容器中运行的进程发送信号:

// 让容器先运行一会儿
time.Sleep(3 * time.Second)

if err := task.Kill(ctx, syscall.SIGTERM); err != nil {
    return err
}

status := <-exitStatusC
code, exitedAt, err := status.Result()
if err != nil {
    return err
}
fmt.Printf("redis-server exited with status: %d\n", code)

向容器发送结束的信号后,代码等待容器结束,并输出返回码。最后我们删除 task 对象:

status, err := task.Delete(ctx)

完整的 demo 代码请参考这里。下面编译 demo 代码并运行

go build main.go
sudo ./main

containerd 全面攻略_第6张图片

可以看出,在容器技术逐步标准化后,containerd 在相关的技术栈中将占据非常重要的地位,containerd 提供的核心服务很可能成为底层管理容器的标准。届时,更上层的容器化应用平台将直接使用 containerd 提供的基础服务。

拓展阅读

弃用 Docker 后会有什么不同?

Don’t Panic!随后又重点解释了几个大家最关心的问题,我们来分析下官方提到的这些方面:

  • 正常的 K8s 用户不会有任何影响

  • 是的,生产环境中高版本的集群只需要把运行时从 docker 切换到其他的 runtime(如 containerd)即可。containerd 是 docker 中的一个底层组件,主要负责维护容器的生命周期,跟随 docker 经历了长期考验。同时 2019年初就从 CNCF 毕业,可以单独作为容器运行时用在集群中。TKE 也早在 2019 年就已经提供了 containerd 作为运行时选项,因此把 runtime 从 docker 转换到 containerd 是一个基本无痛的过程。CRI-O 是另一个常被提及的运行时组件,由 redhat 提供,比 containerd 更加轻量级,不过和 docker 的区别较大,可能转换时会有一些不同之处。

  • 开发环境中通过docker build构建出来的镜像依然可以在集群中使用

  • 镜像一直是容器生态的一大优势,虽然人们总是把镜像称之为“docker镜像”,但镜像早就成为了一种规范了。具体规范可以参考image-spec。在任何地方只要构建出符合 Image Spec 的镜像,就可以拿到其他符合 Image Spec 的容器运行时上运行。

  • 在 Pod 中使用 DinD(Docker in Docker)的用户会受到影响

  • 有些使用者会把 docker 的 socket (/run/docker.sock)挂载到 Pod 中,并在 Pod 中调用 docker 的 api 构建镜像或创建编译容器等,官方在这里的建议是使用 Kaniko、Img 或 Buildah。我们可以通过把 docker daemon 作为 DaemonSet 或者给想要使用 docker 的 Pod 添加一个 docker daemon 的 sidecar 的方式在任意运行时中使用 DinD 的方案。TKE 也专门为在 containerd 集群中使用 DinD 提供了方案,详见 在containerd中使用DinD。

containerd 的今生前世

所以 containerd 到底是个啥?和 docker 又是什么关系?可能有些同学看到博客后会发出这样的疑问,接下来就给同学们讲解下 containerd 和 docker 的渊源。

  • docker 与 containerd
    2016年,docker 把负责容器生命周期的模块拆分出来,并将其捐赠给了社区,也就是现在的 containerd。docker 拆分后结构如下图所示(当然 docker 公司还在 docker 中添加了部分编排的代码)。
    containerd 全面攻略_第7张图片

在我们调用 docker 命令创建容器后,docker daemon 会通过 Image 模块下载镜像并保存到 Graph Driver 模块中,之后通过 client 调用containerd 创建并运行容器。我们在使用 docker 创建容器时可能需要使用–volume给容器添加持久化存储;还有可能通过–network连接我们用 docker 命令创建的几个容器,当然,这些功能是 docker 中的 Storage 模块和 Networking 模块提供给我们的。但 K8s 提供了更强的卷挂载能力和集群级别的网络能力,在集群中 kubelet 只会使用到 docker 提供的镜像下载和容器管理功能,而编排、网络、存储等功能都不会用到。下图中可以看出当前的模式下各模块的调用链,同时图中被红框标注出的几个模块就是 kubelet 创建 Pod 时所依赖的几个运行时的模块。
containerd 全面攻略_第8张图片

containerd 被捐赠给CNCF社区后,社区给其添加了镜像管理模块和 CRI 模块,这样 containerd 不只可以管理容器的生命周期,还可以直接作为 K8s 的运行时使用。于是 containerd 在 2019年2月从 CNCF 社区毕业,正式进入生产环境。下图中能看出以 containerd 作为容器运行时,可以给 kubelet 带来创建 Pod 所需的全部功能,同时还得到了更纯粹的功能模块以及更短的调用链。
containerd 全面攻略_第9张图片
从上面的对比可以看出从 containerd 被捐赠给社区开始,就一直以成为简单、稳定且可靠的容器运行时为目标;而 docker 则是希望能成为一个完整的产品。官方文档中也提到了这一点,docker 为了给用户更好的交互和使用体验以及更多的功能,提供了很多开发人员所需要的特性,同时为了给 swarm 做基础,提供了网络和卷的功能。而这些功能其实都是是 K8s 用不上的;containerd 则相反,仅提供了 kubelet 创建 Pod 所需要的基础功能,当然这换来的就是更高的鲁棒性以及更好的性能。在一定程度上讲,即使在 kubelet 1.23 版本之后 docker 提供了 CRI 接口,containerd 仍然是更好的选择。

你可能感兴趣的:(云原生,container,docker)