Containerd 簡(jiǎn)介
我們可以把 docker 抽象為下圖所示的結(jié)構(gòu)(此圖來自互聯(lián)網(wǎng)):

從圖中可以看出,docker 對(duì)容器的管理和操作基本都是通過 containerd 完成的。 那么,containerd 是什么呢?
Containerd 是一個(gè)工業(yè)級(jí)標(biāo)準(zhǔn)的容器運(yùn)行時(shí),它強(qiáng)調(diào)簡(jiǎn)單性、健壯性和可移植性。Containerd 可以在宿主機(jī)中管理完整的容器生命周期:容器鏡像的傳輸和存儲(chǔ)、容器的執(zhí)行和管理、存儲(chǔ)和網(wǎng)絡(luò)等。詳細(xì)點(diǎn)說,Containerd 負(fù)責(zé)干下面這些事情:
- 管理容器的生命周期(從創(chuàng)建容器到銷毀容器)
- 拉取/推送容器鏡像
- 存儲(chǔ)管理(管理鏡像及容器數(shù)據(jù)的存儲(chǔ))
- 調(diào)用 runC 運(yùn)行容器(與 runC 等容器運(yùn)行時(shí)交互)
- 管理容器網(wǎng)絡(luò)接口及網(wǎng)絡(luò)
注意:Containerd 被設(shè)計(jì)成嵌入到一個(gè)更大的系統(tǒng)中,而不是直接由開發(fā)人員或終端用戶使用。
為什么需要 containerd
我們可以從下面幾點(diǎn)來理解為什么需要獨(dú)立的 containerd:
- 繼續(xù)從整體 docker 引擎中分離出的項(xiàng)目(開源項(xiàng)目的思路)
- 可以被 Kubernets CRI 等項(xiàng)目使用(通用化)
- 為廣泛的行業(yè)合作打下基礎(chǔ)(就像 runC 一樣)
重復(fù)一遍:Containerd 被設(shè)計(jì)成嵌入到一個(gè)更大的系統(tǒng)中,而不是直接由開發(fā)人員或終端用戶使用。所以 containerd 具有宏大的愿景(此圖來自互聯(lián)網(wǎng)):

當(dāng) containerd 和 runC 成為標(biāo)準(zhǔn)化容器服務(wù)的基石后,上層的應(yīng)用就可以直接建立在 containerd 和 runC 之上。上圖中展示的容器平臺(tái)都已經(jīng)支持 containerd 和 runC 的組合了,相信接下來會(huì)有更多類似的容器平臺(tái)出現(xiàn)。
Containerd 的技術(shù)方向和目標(biāo)
- 簡(jiǎn)潔的基于 gRPC 的 API 和 client library
- 完整的 OCI 支持(runtime 和 image spec)
- 同時(shí)具備穩(wěn)定性和高性能的定義良好的容器核心功能
- 一個(gè)解耦的系統(tǒng)(讓 image、filesystem、runtime 解耦合),實(shí)現(xiàn)插件式的擴(kuò)展和重用
下圖展示了 containerd 的架構(gòu)(此圖來自互聯(lián)網(wǎng)):

在架構(gòu)設(shè)計(jì)和實(shí)現(xiàn)方面,核心開發(fā)人員在他們的博客里提到了通過反思 graphdriver 的實(shí)現(xiàn),他們將 containerd 設(shè)計(jì)成了 snapshotter 的模式,這也使得 containerd 對(duì)于 overlay 文件系、snapshot 文件系統(tǒng)的支持比較好。
storage、metadata 和 runtime 的三大塊劃分非常清晰,通過抽象出 events 的設(shè)計(jì),containerd 也得以將網(wǎng)絡(luò)層面的復(fù)雜度交給了上層處理,僅提供 network namespace 相關(guān)的一些接口添加和配置 API。這樣做的好處無疑是巨大的,保留最小功能集合的純粹和高效,而將更多的復(fù)雜性及靈活性交給了插件及上層系統(tǒng)。
安裝并運(yùn)行 containerd
在從概念上對(duì) containerd 有所了解之后,讓我們安裝最新版的 containerd 并實(shí)際把玩一下。本文的演示環(huán)境為 Ubuntu 16.04。
注意:containerd 需要調(diào)用 runC,所以在安裝 containerd 之前請(qǐng)先安裝 runC。RunC 的安裝請(qǐng)參考筆者博文《RunC 簡(jiǎn)介》。
下載并解壓 containerd 程序
從 github 上下載 containerd 包,當(dāng)前的最新版本為 v1.1.0。
然后把下載到的壓縮包解壓到 /usr/local 目錄下:
$ sudo tar -C /usr/local -xf containerd-1.1.0.linux-amd64.tar.gz
當(dāng)前 containerd 的安裝包中一共有五個(gè)文件,通過上面的命令它們被安裝到了 /usr/local/bin 目錄中:

- containerd:即容器的運(yùn)行時(shí),以 gRPC 協(xié)議的形式提供滿足 OCI 標(biāo)準(zhǔn)的 API
- containerd-release 和 containerd-stress:分別是 containerd 項(xiàng)目的發(fā)行版發(fā)布工具以及壓力測(cè)試工具
- containerd-shim:這是每一個(gè)容器的運(yùn)行時(shí)載體,我們?cè)?docker 宿主機(jī)上看到的 shim 也正是代表著一個(gè)個(gè)通過調(diào)用 containerd 啟動(dòng)的 docker 容器。
- ctr:它是一個(gè)簡(jiǎn)單的 CLI 接口,用作 containerd 本身的一些調(diào)試用途,投入生產(chǎn)使用時(shí)還是應(yīng)該配合docker 或者 cri-containerd 部署
生成 containerd 配置文件
Containerd 的配置文件默認(rèn)為 /etc/containerd/config.toml。這里我們可以通過命令來生成一個(gè)默認(rèn)的配置文件:
$ sudo su $ mkdir /etc/containerd $ containerd config default > /etc/containerd/config.toml
對(duì)于演示來說,這個(gè)默認(rèn)的配置已經(jīng)足夠了。
配置 containerd 作為服務(wù)運(yùn)行
創(chuàng)建文件 containerd.service:
$ sudo touch /lib/systemd/system/containerd.service
編輯其內(nèi)容如下:
[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
執(zhí)行下面的命令啟動(dòng) containerd 服務(wù)并查看服務(wù)的狀態(tài):
$ sudo systemctl daemon-reload $ sudo systemctl enable containerd.service $ sudo systemctl start containerd.service $ sudo systemctl status containerd.service

至此 containerd 已經(jīng)安裝成功!
演示 demo
可以使用類似 runC 的方式運(yùn)行容器,也就是使用現(xiàn)成的客戶端工具 ctr,由于用法與 runC 非常相似,所以這里不再贅述。Containerd 還提供了 client package 用于在代碼中集成 containerd 客戶端,下面的 demo 就采用 golang 和 client package 在代碼中訪問 containerd 服務(wù)來創(chuàng)建并運(yùn)行容器!
連接 containerd 服務(wù)
創(chuàng)建 main.go 文件,內(nèi)容如下:
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 }
上面代碼中使用默認(rèn)的 containerd 套接字創(chuàng)建了一個(gè)客戶端對(duì)象。因?yàn)?containerd daemon 通過 gRPC 協(xié)議提供服務(wù),所以我們需要?jiǎng)?chuàng)建一個(gè)用于調(diào)用客戶端方法的上下文。在創(chuàng)建上下文之后,我們還應(yīng)該為我們的 demo 設(shè)置一個(gè) namespace,創(chuàng)建單獨(dú)的 namespace 可以與用戶的資源進(jìn)行隔離以免發(fā)生沖突:
ctx := namespaces.WithNamespace(context.Background(), "demo")
拉取 redis 鏡像
在創(chuàng)建客戶端對(duì)象后我們就可以從 dockerhub 上拉取容器鏡像了,這里我們拉取一個(gè) redis 鏡像:
image, err := client.Pull(ctx, "docker.io/library/redis:alpine", containerd.WithPullUnpack) if err != nil { return err }
使用客戶端的 Pull 方法從 dockerhub 上拉取 redis 鏡像,這個(gè)方法支持 Opts 模式,所以我們可以指定 containerd.WithPullUnpackso 讓下載完成后直接把鏡像解壓縮為一個(gè) snapshotter 作為即將運(yùn)行的容器的 rootfs。
創(chuàng)建 OCI Spec 和容器
有了 rootfs 還需要運(yùn)行 OCI 容器所需的 OCI runtime spec,我們通過 NewContainer 方法可以使用默認(rèn)的 OCI runtime spec 直接創(chuàng)建容器對(duì)象。當(dāng)然,也可以通過 Opts 模式的參數(shù)修改默認(rèn)值:
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)
當(dāng)我們?yōu)槿萜鲃?chuàng)建一個(gè) snapshot 時(shí)需要提供 snapshot 的 ID及其父鏡像。通過提供一個(gè)單獨(dú)的 snapshot ID,而不是容器 ID,我們可以輕松地在不同的容器中重用現(xiàn)有的 snapshot。在完成這個(gè)示例之后,我們還添加了 defer container.Delete 調(diào)用來刪除容器以及它的快照。
創(chuàng)建運(yùn)行容器的 task
一個(gè) container 對(duì)象只是包含了運(yùn)行一個(gè)容器所需的資源及配置的數(shù)據(jù)結(jié)構(gòu),一個(gè)容器真正的運(yùn)行起來是由 Task 對(duì)象實(shí)現(xiàn)的:
task, err := container.NewTask(ctx, cio.NewCreator(cio.WithStdio)) if err != nil { return err } defer task.Delete(ctx)
此時(shí)容器的狀態(tài)相當(dāng)于我們?cè)凇?a href="http://www.rzrgm.cn/sparkdev/p/9032209.html" target="_blank">RunC 簡(jiǎn)介》一文中介紹的 "created"。這意味著 namespaces、rootfs 和容器的配置都已經(jīng)初始化成功了,只是用戶進(jìn)程(這里是 redis-server)還沒有啟動(dòng)。在這個(gè)時(shí)機(jī),我們可以為容器設(shè)置網(wǎng)卡,還可以配置工具來對(duì)容器進(jìn)行監(jiān)控等。
讓運(yùn)行中的 task 退出
當(dāng)要結(jié)束容器的運(yùn)行時(shí),可以調(diào)用 task.Kill 方法。其實(shí)就是向容器中運(yùn)行的進(jìn)程發(fā)送信號(hào):
// 讓容器先運(yùn)行一會(huì)兒 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)
向容器發(fā)送結(jié)束的信號(hào)后,代碼等待容器結(jié)束,并輸出返回碼。最后我們刪除 task 對(duì)象:
status, err := task.Delete(ctx)
完整的 demo 代碼請(qǐng)參考這里。下面編譯 demo 代碼并運(yùn)行:
$ go build main.go
$ sudo ./main

總結(jié)
可以看出,在容器技術(shù)逐步標(biāo)準(zhǔn)化后,containerd 在相關(guān)的技術(shù)棧中將占據(jù)非常重要的地位,containerd 提供的核心服務(wù)很可能成為底層管理容器的標(biāo)準(zhǔn)。屆時(shí),更上層的容器化應(yīng)用平臺(tái)將直接使用 containerd 提供的基礎(chǔ)服務(wù)。
參考:
Containerd
Getting started with containerd
containerd.io
小嘗containerd
A tour of containerd 1.0

浙公網(wǎng)安備 33010602011771號(hào)