containerd 全面攻略

2021-05-11  本文已影响0人  流雨声

containerd简介

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

1 Containerd 架构图
image
2 Containerd 作用
3 为什么需要 Containerd
4 Containerd 的技术方向

下图展示了 containerd 的技术架构


image

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

5 Containerd 的愿景目标
image

当 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
image
image
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
image

至此 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
image

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

拓展阅读

弃用 Docker 后会有什么不同?

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

containerd 的今生前世

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

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


image

containerd 被捐赠给CNCF社区后,社区给其添加了镜像管理模块和 CRI 模块,这样 containerd 不只可以管理容器的生命周期,还可以直接作为 K8s 的运行时使用。于是 containerd 在 2019年2月从 CNCF 社区毕业,正式进入生产环境。下图中能看出以 containerd 作为容器运行时,可以给 kubelet 带来创建 Pod 所需的全部功能,同时还得到了更纯粹的功能模块以及更短的调用链。


image

从上面的对比可以看出从 containerd 被捐赠给社区开始,就一直以成为简单、稳定且可靠的容器运行时为目标;而 docker 则是希望能成为一个完整的产品。官方文档中也提到了这一点,docker 为了给用户更好的交互和使用体验以及更多的功能,提供了很多开发人员所需要的特性,同时为了给 swarm 做基础,提供了网络和卷的功能。而这些功能其实都是是 K8s 用不上的;containerd 则相反,仅提供了 kubelet 创建 Pod 所需要的基础功能,当然这换来的就是更高的鲁棒性以及更好的性能。在一定程度上讲,即使在 kubelet 1.23 版本之后 docker 提供了 CRI 接口,containerd 仍然是更好的选择。

上一篇 下一篇

猜你喜欢

热点阅读