一文搞定 Containerd 的使用
在學(xué)習(xí) Containerd 之前我們有必要對 Docker 的發(fā)展歷史做一個簡單的回顧,因?yàn)檫@里面牽涉到的組件實(shí)戰(zhàn)是有點(diǎn)多,有很多我們會經(jīng)常聽到,但是不清楚這些組件到底是干什么用的,比如 libcontainer、runc、containerd、CRI、OCI 等等。
Docker
從 Docker 1.11 版本開始,Docker 容器運(yùn)行就不是簡單通過 Docker Daemon 來啟動了,而是通過集成 containerd、runc 等多個組件來完成的。雖然 Docker Daemon 守護(hù)進(jìn)程模塊在不停的重構(gòu),但是基本功能和定位沒有太大的變化,一直都是 CS 架構(gòu),守護(hù)進(jìn)程負(fù)責(zé)和 Docker Client 端交互,并管理 Docker 鏡像和容器。現(xiàn)在的架構(gòu)中組件 containerd 就會負(fù)責(zé)集群節(jié)點(diǎn)上容器的生命周期管理,并向上為 Docker Daemon 提供 gRPC 接口。

當(dāng)我們要創(chuàng)建一個容器的時(shí)候,現(xiàn)在 Docker Daemon 并不能直接幫我們創(chuàng)建了,而是請求 containerd 來創(chuàng)建一個容器,containerd 收到請求后,也并不會直接去操作容器,而是創(chuàng)建一個叫做 containerd-shim 的進(jìn)程,讓這個進(jìn)程去操作容器,我們指定容器進(jìn)程是需要一個父進(jìn)程來做狀態(tài)收集、維持 stdin 等 fd 打開等工作的,假如這個父進(jìn)程就是 containerd,那如果 containerd 掛掉的話,整個宿主機(jī)上所有的容器都得退出了,而引入 containerd-shim 這個墊片就可以來規(guī)避這個問題了。
然后創(chuàng)建容器需要做一些 namespaces 和 cgroups 的配置,以及掛載 root 文件系統(tǒng)等操作,這些操作其實(shí)已經(jīng)有了標(biāo)準(zhǔn)的規(guī)范,那就是 OCI(開放容器標(biāo)準(zhǔn)),runc 就是它的一個參考實(shí)現(xiàn)(Docker 被逼無耐將 libcontainer 捐獻(xiàn)出來改名為 runc 的),這個標(biāo)準(zhǔn)其實(shí)就是一個文檔,主要規(guī)定了容器鏡像的結(jié)構(gòu)、以及容器需要接收哪些操作指令,比如 create、start、stop、delete 等這些命令。runc 就可以按照這個 OCI 文檔來創(chuàng)建一個符合規(guī)范的容器,既然是標(biāo)準(zhǔn)肯定就有其他 OCI 實(shí)現(xiàn),比如 Kata、gVisor 這些容器運(yùn)行時(shí)都是符合 OCI 標(biāo)準(zhǔn)的。
所以真正啟動容器是通過 containerd-shim 去調(diào)用 runc 來啟動容器的,runc 啟動完容器后本身會直接退出,containerd-shim 則會成為容器進(jìn)程的父進(jìn)程, 負(fù)責(zé)收集容器進(jìn)程的狀態(tài), 上報(bào)給 containerd, 并在容器中 pid 為 1 的進(jìn)程退出后接管容器中的子進(jìn)程進(jìn)行清理, 確保不會出現(xiàn)僵尸進(jìn)程。
而 Docker 將容器操作都遷移到 containerd 中去是因?yàn)楫?dāng)前做 Swarm,想要進(jìn)軍 PaaS 市場,做了這個架構(gòu)切分,讓 Docker Daemon 專門去負(fù)責(zé)上層的封裝編排,當(dāng)然后面的結(jié)果我們知道 Swarm 在 Kubernetes 面前是慘敗,然后 Docker 公司就把 containerd 項(xiàng)目捐獻(xiàn)給了 CNCF 基金會,這個也是現(xiàn)在的 Docker 架構(gòu)。
CRI
我們知道 Kubernetes 提供了一個 CRI 的容器運(yùn)行時(shí)接口,那么這個 CRI 到底是什么呢?這個其實(shí)也和 Docker 的發(fā)展密切相關(guān)的。
在 Kubernetes 早期的時(shí)候,當(dāng)時(shí) Docker 實(shí)在是太火了,Kubernetes 當(dāng)然會先選擇支持 Docker,而且是通過硬編碼的方式直接調(diào)用 Docker API,后面隨著 Docker 的不斷發(fā)展以及 Google 的主導(dǎo),出現(xiàn)了更多容器運(yùn)行時(shí),Kubernetes 為了支持更多更精簡的容器運(yùn)行時(shí),Google 就和紅帽主導(dǎo)推出了 CRI 標(biāo)準(zhǔn),用于將 Kubernetes 平臺和特定的容器運(yùn)行時(shí)(當(dāng)然主要是為了干掉 Docker)解耦。
CRI(Container Runtime Interface 容器運(yùn)行時(shí)接口)本質(zhì)上就是 Kubernetes 定義的一組與容器運(yùn)行時(shí)進(jìn)行交互的接口,所以只要實(shí)現(xiàn)了這套接口的容器運(yùn)行時(shí)都可以對接到 Kubernetes 平臺上來。不過 Kubernetes 推出 CRI 這套標(biāo)準(zhǔn)的時(shí)候還沒有現(xiàn)在的統(tǒng)治地位,所以有一些容器運(yùn)行時(shí)可能不會自身就去實(shí)現(xiàn) CRI 接口,于是就有了 shim(墊片), 一個 shim 的職責(zé)就是作為適配器將各種容器運(yùn)行時(shí)本身的接口適配到 Kubernetes 的 CRI 接口上,其中 dockershim 就是 Kubernetes 對接 Docker 到 CRI 接口上的一個墊片實(shí)現(xiàn)。

Kubelet 通過 gRPC 框架與容器運(yùn)行時(shí)或 shim 進(jìn)行通信,其中 kubelet 作為客戶端,CRI shim(也可能是容器運(yùn)行時(shí)本身)作為服務(wù)器。
CRI 定義的 API(https://github.com/kubernetes/kubernetes/blob/release-1.5/pkg/kubelet/api/v1alpha1/runtime/api.proto) 主要包括兩個 gRPC 服務(wù),ImageService 和 RuntimeService,ImageService 服務(wù)主要是拉取鏡像、查看和刪除鏡像等操作,RuntimeService 則是用來管理 Pod 和容器的生命周期,以及與容器交互的調(diào)用(exec/attach/port-forward)等操作,可以通過 kubelet 中的標(biāo)志 --container-runtime-endpoint 和 --image-service-endpoint 來配置這兩個服務(wù)的套接字。

不過這里同樣也有一個例外,那就是 Docker,由于 Docker 當(dāng)時(shí)的江湖地位很高,Kubernetes 是直接內(nèi)置了 dockershim 在 kubelet 中的,所以如果你使用的是 Docker 這種容器運(yùn)行時(shí)的話是不需要單獨(dú)去安裝配置適配器之類的,當(dāng)然這個舉動似乎也麻痹了 Docker 公司。

現(xiàn)在如果我們使用的是 Docker 的話,當(dāng)我們在 Kubernetes 中創(chuàng)建一個 Pod 的時(shí)候,首先就是 kubelet 通過 CRI 接口調(diào)用 dockershim,請求創(chuàng)建一個容器,kubelet 可以視作一個簡單的 CRI Client, 而 dockershim 就是接收請求的 Server,不過他們都是在 kubelet 內(nèi)置的。
dockershim 收到請求后, 轉(zhuǎn)化成 Docker Daemon 能識別的請求, 發(fā)到 Docker Daemon 上請求創(chuàng)建一個容器,請求到了 Docker Daemon 后續(xù)就是 Docker 創(chuàng)建容器的流程了,去調(diào)用 containerd,然后創(chuàng)建 containerd-shim 進(jìn)程,通過該進(jìn)程去調(diào)用 runc 去真正創(chuàng)建容器。
其實(shí)我們仔細(xì)觀察也不難發(fā)現(xiàn)使用 Docker 的話其實(shí)是調(diào)用鏈比較長的,真正容器相關(guān)的操作其實(shí) containerd 就完全足夠了,Docker 太過于復(fù)雜笨重了,當(dāng)然 Docker 深受歡迎的很大一個原因就是提供了很多對用戶操作比較友好的功能,但是對于 Kubernetes 來說壓根不需要這些功能,因?yàn)槎际峭ㄟ^接口去操作容器的,所以自然也就可以將容器運(yùn)行時(shí)切換到 containerd 來。

切換到 containerd 可以消除掉中間環(huán)節(jié),操作體驗(yàn)也和以前一樣,但是由于直接用容器運(yùn)行時(shí)調(diào)度容器,所以它們對 Docker 來說是不可見的。因此,你以前用來檢查這些容器的 Docker 工具就不能使用了。
你不能再使用 docker ps 或 docker inspect 命令來獲取容器信息。由于不能列出容器,因此也不能獲取日志、停止容器,甚至不能通過 docker exec 在容器中執(zhí)行命令。
當(dāng)然我們?nèi)匀豢梢韵螺d鏡像,或者用 docker build 命令構(gòu)建鏡像,但用 Docker 構(gòu)建、下載的鏡像,對于容器運(yùn)行時(shí)和 Kubernetes,均不可見。為了在 Kubernetes 中使用,需要把鏡像推送到鏡像倉庫中去。
從上圖可以看出在 containerd 1.0 中,對 CRI 的適配是通過一個單獨(dú)的 CRI-Containerd 進(jìn)程來完成的,這是因?yàn)樽铋_始 containerd 還會去適配其他的系統(tǒng)(比如 swarm),所以沒有直接實(shí)現(xiàn) CRI,所以這個對接工作就交給 CRI-Containerd 這個 shim 了。
然后到了 containerd 1.1 版本后就去掉了 CRI-Containerd 這個 shim,直接把適配邏輯作為插件的方式集成到了 containerd 主進(jìn)程中,現(xiàn)在這樣的調(diào)用就更加簡潔了。

與此同時(shí) Kubernetes 社區(qū)也做了一個專門用于 Kubernetes 的 CRI 運(yùn)行時(shí) CRI-O,直接兼容 CRI 和 OCI 規(guī)范。

這個方案和 containerd 的方案顯然比默認(rèn)的 dockershim 簡潔很多,不過由于大部分用戶都比較習(xí)慣使用 Docker,所以大家還是更喜歡使用 dockershim 方案。
但是隨著 CRI 方案的發(fā)展,以及其他容器運(yùn)行時(shí)對 CRI 的支持越來越完善,Kubernetes 社區(qū)在2020年7月份就開始著手移除 dockershim 方案了:https://github.com/kubernetes/enhancements/tree/master/keps/sig-node/2221-remove-dockershim,現(xiàn)在的移除計(jì)劃是在 1.20 版本中將 kubelet 中內(nèi)置的 dockershim 代碼分離,將內(nèi)置的 dockershim 標(biāo)記為維護(hù)模式,當(dāng)然這個時(shí)候仍然還可以使用 dockershim,目標(biāo)是在 1.23/1.24 版本發(fā)布沒有 dockershim 的版本(代碼還在,但是要默認(rèn)支持開箱即用的 docker 需要自己構(gòu)建 kubelet,會在某個寬限期過后從 kubelet 中刪除內(nèi)置的 dockershim 代碼)。
那么這是否就意味著 Kubernetes 不再支持 Docker 了呢?當(dāng)然不是的,這只是廢棄了內(nèi)置的 dockershim 功能而已,Docker 和其他容器運(yùn)行時(shí)將一視同仁,不會單獨(dú)對待內(nèi)置支持,如果我們還想直接使用 Docker 這種容器運(yùn)行時(shí)應(yīng)該怎么辦呢?可以將 dockershim 的功能單獨(dú)提取出來獨(dú)立維護(hù)一個 cri-dockerd 即可,就類似于 containerd 1.0 版本中提供的 CRI-Containerd,當(dāng)然還有一種辦法就是 Docker 官方社區(qū)將 CRI 接口內(nèi)置到 Dockerd 中去實(shí)現(xiàn)。
但是我們也清楚 Dockerd 也是去直接調(diào)用的 Containerd,而 containerd 1.1 版本后就內(nèi)置實(shí)現(xiàn)了 CRI,所以 Docker 也沒必要再去單獨(dú)實(shí)現(xiàn) CRI 了,當(dāng) Kubernetes 不再內(nèi)置支持開箱即用的 Docker 的以后,最好的方式當(dāng)然也就是直接使用 Containerd 這種容器運(yùn)行時(shí),而且該容器運(yùn)行時(shí)也已經(jīng)經(jīng)過了生產(chǎn)環(huán)境實(shí)踐的,接下來我們就來學(xué)習(xí)下 Containerd 的使用。
Containerd
我們知道很早之前的 Docker Engine 中就有了 containerd,只不過現(xiàn)在是將 containerd 從 Docker Engine 里分離出來,作為一個獨(dú)立的開源項(xiàng)目,目標(biāo)是提供一個更加開放、穩(wěn)定的容器運(yùn)行基礎(chǔ)設(shè)施。分離出來的 containerd 將具有更多的功能,涵蓋整個容器運(yùn)行時(shí)管理的所有需求,提供更強(qiáng)大的支持。
containerd 是一個工業(yè)級標(biāo)準(zhǔn)的容器運(yùn)行時(shí),它強(qiáng)調(diào)簡單性、健壯性和可移植性,containerd 可以負(fù)責(zé)干下面這些事情:
管理容器的生命周期(從創(chuàng)建容器到銷毀容器) 拉取/推送容器鏡像 存儲管理(管理鏡像及容器數(shù)據(jù)的存儲) 調(diào)用 runc 運(yùn)行容器(與 runc 等容器運(yùn)行時(shí)交互) 管理容器網(wǎng)絡(luò)接口及網(wǎng)絡(luò)
架構(gòu)
containerd 可用作 Linux 和 Windows 的守護(hù)程序,它管理其主機(jī)系統(tǒng)完整的容器生命周期,從鏡像傳輸和存儲到容器執(zhí)行和監(jiān)測,再到底層存儲到網(wǎng)絡(luò)附件等等。

上圖是 containerd 官方提供的架構(gòu)圖,可以看出 containerd 采用的也是 C/S 架構(gòu),服務(wù)端通過 unix domain socket 暴露低層的 gRPC API 接口出去,客戶端通過這些 API 管理節(jié)點(diǎn)上的容器,每個 containerd 只負(fù)責(zé)一臺機(jī)器,Pull 鏡像,對容器的操作(啟動、停止等),網(wǎng)絡(luò),存儲都是由 containerd 完成。具體運(yùn)行容器由 runc 負(fù)責(zé),實(shí)際上只要是符合 OCI 規(guī)范的容器都可以支持。
為了解耦,containerd 將系統(tǒng)劃分成了不同的組件,每個組件都由一個或多個模塊協(xié)作完成(Core 部分),每一種類型的模塊都以插件的形式集成到 Containerd 中,而且插件之間是相互依賴的,例如,上圖中的每一個長虛線的方框都表示一種類型的插件,包括 Service Plugin、Metadata Plugin、GC Plugin、Runtime Plugin 等,其中 Service Plugin 又會依賴 Metadata Plugin、GC Plugin 和 Runtime Plugin。每一個小方框都表示一個細(xì)分的插件,例如 Metadata Plugin 依賴 Containers Plugin、Content Plugin 等。比如:
Content Plugin: 提供對鏡像中可尋址內(nèi)容的訪問,所有不可變的內(nèi)容都被存儲在這里。Snapshot Plugin: 用來管理容器鏡像的文件系統(tǒng)快照,鏡像中的每一層都會被解壓成文件系統(tǒng)快照,類似于 Docker 中的 graphdriver。
總體來看 containerd 可以分為三個大塊:Storage、Metadata 和 Runtime。

安裝
這里我使用的系統(tǒng)是 Linux Mint 20.2,首先需要安裝 seccomp 依賴:
? ~ apt-get update
? ~ apt-get install libseccomp2 -y
由于 containerd 需要調(diào)用 runc,所以我們也需要先安裝 runc,不過 containerd 提供了一個包含相關(guān)依賴的壓縮包 cri-containerd-cni-${VERSION}.${OS}-${ARCH}.tar.gz,可以直接使用這個包來進(jìn)行安裝。首先從 release 頁面下載最新版本的壓縮包,當(dāng)前為 1.5.5 版本:
? ~ wget https://github.com/containerd/containerd/releases/download/v1.5.5/cri-containerd-cni-1.5.5-linux-amd64.tar.gz
# 如果有限制,也可以替換成下面的 URL 加速下載
# wget https://download.fastgit.org/containerd/containerd/releases/download/v1.5.5/cri-containerd-cni-1.5.5-linux-amd64.tar.gz
可以通過 tar 的 -t 選項(xiàng)直接看到壓縮包中包含哪些文件:
? ~ tar -tf cri-containerd-cni-1.4.3-linux-amd64.tar.gz
etc/
etc/cni/
etc/cni/net.d/
etc/cni/net.d/10-containerd-net.conflist
etc/crictl.yaml
etc/systemd/
etc/systemd/system/
etc/systemd/system/containerd.service
usr/
usr/local/
usr/local/bin/
usr/local/bin/containerd-shim-runc-v2
usr/local/bin/ctr
usr/local/bin/containerd-shim
usr/local/bin/containerd-shim-runc-v1
usr/local/bin/crictl
usr/local/bin/critest
usr/local/bin/containerd
usr/local/sbin/
usr/local/sbin/runc
opt/
opt/cni/
opt/cni/bin/
opt/cni/bin/vlan
opt/cni/bin/host-local
opt/cni/bin/flannel
opt/cni/bin/bridge
opt/cni/bin/host-device
opt/cni/bin/tuning
opt/cni/bin/firewall
opt/cni/bin/bandwidth
opt/cni/bin/ipvlan
opt/cni/bin/sbr
opt/cni/bin/dhcp
opt/cni/bin/portmap
opt/cni/bin/ptp
opt/cni/bin/static
opt/cni/bin/macvlan
opt/cni/bin/loopback
opt/containerd/
opt/containerd/cluster/
opt/containerd/cluster/version
opt/containerd/cluster/gce/
opt/containerd/cluster/gce/cni.template
opt/containerd/cluster/gce/configure.sh
opt/containerd/cluster/gce/cloud-init/
opt/containerd/cluster/gce/cloud-init/master.yaml
opt/containerd/cluster/gce/cloud-init/node.yaml
opt/containerd/cluster/gce/env
直接將壓縮包解壓到系統(tǒng)的各個目錄中:
? ~ tar -C / -xzf cri-containerd-cni-1.5.5-linux-amd64.tar.gz
當(dāng)然要記得將 /usr/local/bin 和 /usr/local/sbin 追加到 ~/.bashrc 文件的 PATH 環(huán)境變量中:
export PATH=$PATH:/usr/local/bin:/usr/local/sbin
然后執(zhí)行下面的命令使其立即生效:
? ~ source ~/.bashrc
containerd 的默認(rèn)配置文件為 /etc/containerd/config.toml,我們可以通過如下所示的命令生成一個默認(rèn)的配置:
? ~ mkdir /etc/containerd
? ~ containerd config default > /etc/containerd/config.toml
由于上面我們下載的 containerd 壓縮包中包含一個 etc/systemd/system/containerd.service 的文件,這樣我們就可以通過 systemd 來配置 containerd 作為守護(hù)進(jìn)程運(yùn)行了,內(nèi)容如下所示:
? ~ cat /etc/systemd/system/containerd.service
[Unit]
Description=containerd container runtime
Documentation=https://containerd.io
After=network.target local-fs.target
[Service]
ExecStartPre=-/sbin/modprobe overlay
ExecStart=/usr/local/bin/containerd
Type=notify
Delegate=yes
KillMode=process
Restart=always
RestartSec=5
# 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
LimitNOFILE=1048576
# Comment TasksMax if your systemd version does not supports it.
# Only systemd 226 and above support this version.
TasksMax=infinity
OOMScoreAdjust=-999
[Install]
WantedBy=multi-user.target
這里有兩個重要的參數(shù):
Delegate: 這個選項(xiàng)允許 containerd 以及運(yùn)行時(shí)自己管理自己創(chuàng)建容器的 cgroups。如果不設(shè)置這個選項(xiàng),systemd 就會將進(jìn)程移到自己的 cgroups 中,從而導(dǎo)致 containerd 無法正確獲取容器的資源使用情況。KillMode: 這個選項(xiàng)用來處理 containerd 進(jìn)程被殺死的方式。默認(rèn)情況下,systemd 會在進(jìn)程的 cgroup 中查找并殺死 containerd 的所有子進(jìn)程。KillMode 字段可以設(shè)置的值如下。control-group(默認(rèn)值):當(dāng)前控制組里面的所有子進(jìn)程,都會被殺掉process:只殺主進(jìn)程mixed:主進(jìn)程將收到 SIGTERM 信號,子進(jìn)程收到 SIGKILL 信號none:沒有進(jìn)程會被殺掉,只是執(zhí)行服務(wù)的 stop 命令
我們需要將 KillMode 的值設(shè)置為 process,這樣可以確保升級或重啟 containerd 時(shí)不殺死現(xiàn)有的容器。
現(xiàn)在我們就可以啟動 containerd 了,直接執(zhí)行下面的命令即可:
? ~ systemctl enable containerd --now
啟動完成后就可以使用 containerd 的本地 CLI 工具 ctr 了,比如查看版本:

配置
我們首先來查看下上面默認(rèn)生成的配置文件 /etc/containerd/config.toml:
disabled_plugins = []
imports = []
oom_score = 0
plugin_dir = ""
required_plugins = []
root = "/var/lib/containerd"
state = "/run/containerd"
version = 2
[cgroup]
path = ""
[debug]
address = ""
format = ""
gid = 0
level = ""
uid = 0
[grpc]
address = "/run/containerd/containerd.sock"
gid = 0
max_recv_message_size = 16777216
max_send_message_size = 16777216
tcp_address = ""
tcp_tls_cert = ""
tcp_tls_key = ""
uid = 0
[metrics]
address = ""
grpc_histogram = false
[plugins]
[plugins."io.containerd.gc.v1.scheduler"]
deletion_threshold = 0
mutation_threshold = 100
pause_threshold = 0.02
schedule_delay = "0s"
startup_delay = "100ms"
[plugins."io.containerd.grpc.v1.cri"]
disable_apparmor = false
disable_cgroup = false
disable_hugetlb_controller = true
disable_proc_mount = false
disable_tcp_service = true
enable_selinux = false
enable_tls_streaming = false
ignore_image_defined_volumes = false
max_concurrent_downloads = 3
max_container_log_line_size = 16384
netns_mounts_under_state_dir = false
restrict_oom_score_adj = false
sandbox_image = "k8s.gcr.io/pause:3.5"
selinux_category_range = 1024
stats_collect_period = 10
stream_idle_timeout = "4h0m0s"
stream_server_address = "127.0.0.1"
stream_server_port = "0"
systemd_cgroup = false
tolerate_missing_hugetlb_controller = true
unset_seccomp_profile = ""
[plugins."io.containerd.grpc.v1.cri".cni]
bin_dir = "/opt/cni/bin"
conf_dir = "/etc/cni/net.d"
conf_template = ""
max_conf_num = 1
[plugins."io.containerd.grpc.v1.cri".containerd]
default_runtime_name = "runc"
disable_snapshot_annotations = true
discard_unpacked_layers = false
no_pivot = false
snapshotter = "overlayfs"
[plugins."io.containerd.grpc.v1.cri".containerd.default_runtime]
base_runtime_spec = ""
container_annotations = []
pod_annotations = []
privileged_without_host_devices = false
runtime_engine = ""
runtime_root = ""
runtime_type = ""
[plugins."io.containerd.grpc.v1.cri".containerd.default_runtime.options]
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes]
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
base_runtime_spec = ""
container_annotations = []
pod_annotations = []
privileged_without_host_devices = false
runtime_engine = ""
runtime_root = ""
runtime_type = "io.containerd.runc.v2"
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
BinaryName = ""
CriuImagePath = ""
CriuPath = ""
CriuWorkPath = ""
IoGid = 0
IoUid = 0
NoNewKeyring = false
NoPivotRoot = false
Root = ""
ShimCgroup = ""
SystemdCgroup = false
[plugins."io.containerd.grpc.v1.cri".containerd.untrusted_workload_runtime]
base_runtime_spec = ""
container_annotations = []
pod_annotations = []
privileged_without_host_devices = false
runtime_engine = ""
runtime_root = ""
runtime_type = ""
[plugins."io.containerd.grpc.v1.cri".containerd.untrusted_workload_runtime.options]
[plugins."io.containerd.grpc.v1.cri".image_decryption]
key_model = "node"
[plugins."io.containerd.grpc.v1.cri".registry]
config_path = ""
[plugins."io.containerd.grpc.v1.cri".registry.auths]
[plugins."io.containerd.grpc.v1.cri".registry.configs]
[plugins."io.containerd.grpc.v1.cri".registry.headers]
[plugins."io.containerd.grpc.v1.cri".registry.mirrors]
[plugins."io.containerd.grpc.v1.cri".x509_key_pair_streaming]
tls_cert_file = ""
tls_key_file = ""
[plugins."io.containerd.internal.v1.opt"]
path = "/opt/containerd"
[plugins."io.containerd.internal.v1.restart"]
interval = "10s"
[plugins."io.containerd.metadata.v1.bolt"]
content_sharing_policy = "shared"
[plugins."io.containerd.monitor.v1.cgroups"]
no_prometheus = false
[plugins."io.containerd.runtime.v1.linux"]
no_shim = false
runtime = "runc"
runtime_root = ""
shim = "containerd-shim"
shim_debug = false
[plugins."io.containerd.runtime.v2.task"]
platforms = ["linux/amd64"]
[plugins."io.containerd.service.v1.diff-service"]
default = ["walking"]
[plugins."io.containerd.snapshotter.v1.aufs"]
root_path = ""
[plugins."io.containerd.snapshotter.v1.btrfs"]
root_path = ""
[plugins."io.containerd.snapshotter.v1.devmapper"]
async_remove = false
base_image_size = ""
pool_name = ""
root_path = ""
[plugins."io.containerd.snapshotter.v1.native"]
root_path = ""
[plugins."io.containerd.snapshotter.v1.overlayfs"]
root_path = ""
[plugins."io.containerd.snapshotter.v1.zfs"]
root_path = ""
[proxy_plugins]
[stream_processors]
[stream_processors."io.containerd.ocicrypt.decoder.v1.tar"]
accepts = ["application/vnd.oci.image.layer.v1.tar+encrypted"]
args = ["--decryption-keys-path", "/etc/containerd/ocicrypt/keys"]
env = ["OCICRYPT_KEYPROVIDER_CONFIG=/etc/containerd/ocicrypt/ocicrypt_keyprovider.conf"]
path = "ctd-decoder"
returns = "application/vnd.oci.image.layer.v1.tar"
[stream_processors."io.containerd.ocicrypt.decoder.v1.tar.gzip"]
accepts = ["application/vnd.oci.image.layer.v1.tar+gzip+encrypted"]
args = ["--decryption-keys-path", "/etc/containerd/ocicrypt/keys"]
env = ["OCICRYPT_KEYPROVIDER_CONFIG=/etc/containerd/ocicrypt/ocicrypt_keyprovider.conf"]
path = "ctd-decoder"
returns = "application/vnd.oci.image.layer.v1.tar+gzip"
[timeouts]
"io.containerd.timeout.shim.cleanup" = "5s"
"io.containerd.timeout.shim.load" = "5s"
"io.containerd.timeout.shim.shutdown" = "3s"
"io.containerd.timeout.task.state" = "2s"
[ttrpc]
address = ""
gid = 0
uid = 0
這個配置文件比較復(fù)雜,我們可以將重點(diǎn)放在其中的 plugins 配置上面,仔細(xì)觀察我們可以發(fā)現(xiàn)每一個頂級配置塊的命名都是 plugins."io.containerd.xxx.vx.xxx" 這種形式,每一個頂級配置塊都表示一個插件,其中 io.containerd.xxx.vx 表示插件的類型,vx 后面的 xxx 表示插件的 ID,我們可以通過 ctr 查看插件列表:
? ~ ctr plugin ls
ctr plugin ls
TYPE ID PLATFORMS STATUS
io.containerd.content.v1 content - ok
io.containerd.snapshotter.v1 aufs linux/amd64 ok
io.containerd.snapshotter.v1 btrfs linux/amd64 skip
io.containerd.snapshotter.v1 devmapper linux/amd64 error
io.containerd.snapshotter.v1 native linux/amd64 ok
io.containerd.snapshotter.v1 overlayfs linux/amd64 ok
io.containerd.snapshotter.v1 zfs linux/amd64 skip
io.containerd.metadata.v1 bolt - ok
io.containerd.differ.v1 walking linux/amd64 ok
io.containerd.gc.v1 scheduler - ok
io.containerd.service.v1 introspection-service - ok
io.containerd.service.v1 containers-service - ok
io.containerd.service.v1 content-service - ok
io.containerd.service.v1 diff-service - ok
io.containerd.service.v1 images-service - ok
io.containerd.service.v1 leases-service - ok
io.containerd.service.v1 namespaces-service - ok
io.containerd.service.v1 snapshots-service - ok
io.containerd.runtime.v1 linux linux/amd64 ok
io.containerd.runtime.v2 task linux/amd64 ok
io.containerd.monitor.v1 cgroups linux/amd64 ok
io.containerd.service.v1 tasks-service - ok
io.containerd.internal.v1 restart - ok
io.containerd.grpc.v1 containers - ok
io.containerd.grpc.v1 content - ok
io.containerd.grpc.v1 diff - ok
io.containerd.grpc.v1 events - ok
io.containerd.grpc.v1 healthcheck - ok
io.containerd.grpc.v1 images - ok
io.containerd.grpc.v1 leases - ok
io.containerd.grpc.v1 namespaces - ok
io.containerd.internal.v1 opt - ok
io.containerd.grpc.v1 snapshots - ok
io.containerd.grpc.v1 tasks - ok
io.containerd.grpc.v1 version - ok
io.containerd.grpc.v1 cri linux/amd64 ok
頂級配置塊下面的子配置塊表示該插件的各種配置,比如 cri 插件下面就分為 containerd、cni 和 registry 的配置,而 containerd 下面又可以配置各種 runtime,還可以配置默認(rèn)的 runtime。比如現(xiàn)在我們要為鏡像配置一個加速器,那么就需要在 cri 配置塊下面的 registry 配置塊下面進(jìn)行配置 registry.mirrors:
[plugins."io.containerd.grpc.v1.cri".registry]
[plugins."io.containerd.grpc.v1.cri".registry.mirrors]
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"]
endpoint = ["https://bqr1dr1n.mirror.aliyuncs.com"]
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."k8s.gcr.io"]
endpoint = ["https://registry.aliyuncs.com/k8sxio"]
registry.mirrors."xxx": 表示需要配置 mirror 的鏡像倉庫,例如registry.mirrors."docker.io"表示配置 docker.io 的 mirror。endpoint: 表示提供 mirror 的鏡像加速服務(wù),比如我們可以注冊一個阿里云的鏡像服務(wù)來作為 docker.io 的 mirror。
另外在默認(rèn)配置中還有兩個關(guān)于存儲的配置路徑:
root = "/var/lib/containerd"
state = "/run/containerd"
其中 root 是用來保存持久化數(shù)據(jù),包括 Snapshots, Content, Metadata 以及各種插件的數(shù)據(jù),每一個插件都有自己單獨(dú)的目錄,Containerd 本身不存儲任何數(shù)據(jù),它的所有功能都來自于已加載的插件。
而另外的 state 是用來保存運(yùn)行時(shí)的臨時(shí)數(shù)據(jù)的,包括 sockets、pid、掛載點(diǎn)、運(yùn)行時(shí)狀態(tài)以及不需要持久化的插件數(shù)據(jù)。
使用
我們知道 Docker CLI 工具提供了需要增強(qiáng)用戶體驗(yàn)的功能,containerd 同樣也提供一個對應(yīng)的 CLI 工具:ctr,不過 ctr 的功能沒有 docker 完善,但是關(guān)于鏡像和容器的基本功能都是有的。接下來我們就先簡單介紹下 ctr 的使用。
幫助
直接輸入 ctr 命令即可獲得所有相關(guān)的操作命令使用方式:
? ~ ctr
NAME:
ctr -
__
_____/ /______
/ ___/ __/ ___/
/ /__/ /_/ /
\___/\__/_/
containerd CLI
USAGE:
ctr [global options] command [command options] [arguments...]
VERSION:
v1.5.5
DESCRIPTION:
ctr is an unsupported debug and administrative client for interacting
with the containerd daemon. Because it is unsupported, the commands,
options, and operations are not guaranteed to be backward compatible or
stable from release to release of the containerd project.
COMMANDS:
plugins, plugin provides information about containerd plugins
version print the client and server versions
containers, c, container manage containers
content manage content
events, event display containerd events
images, image, i manage images
leases manage leases
namespaces, namespace, ns manage namespaces
pprof provide golang pprof outputs for containerd
run run a container
snapshots, snapshot manage snapshots
tasks, t, task manage tasks
install install a new package
oci OCI tools
shim interact with a shim directly
help, h Shows a list of commands or help for one command
GLOBAL OPTIONS:
--debug enable debug output in logs
--address value, -a value address for containerd's GRPC server (default: "/run/containerd/containerd.sock") [$CONTAINERD_ADDRESS]
--timeout value total timeout for ctr commands (default: 0s)
--connect-timeout value timeout for connecting to containerd (default: 0s)
--namespace value, -n value namespace to use with commands (default: "default") [$CONTAINERD_NAMESPACE]
--help, -h show help
--version, -v print the version
鏡像操作
拉取鏡像
拉取鏡像可以使用 ctr image pull 來完成,比如拉取 Docker Hub 官方鏡像 nginx:alpine,需要注意的是鏡像地址需要加上 docker.io Host 地址:
? ~ ctr image pull docker.io/library/nginx:alpine
docker.io/library/nginx:alpine: resolved |++++++++++++++++++++++++++++++++++++++|
index-sha256:bead42240255ae1485653a956ef41c9e458eb077fcb6dc664cbc3aa9701a05ce: exists |++++++++++++++++++++++++++++++++++++++|
manifest-sha256:ce6ca11a3fa7e0e6b44813901e3289212fc2f327ee8b1366176666e8fb470f24: done |++++++++++++++++++++++++++++++++++++++|
layer-sha256:9a6ac07b84eb50935293bb185d0a8696d03247f74fd7d43ea6161dc0f293f81f: done |++++++++++++++++++++++++++++++++++++++|
layer-sha256:e82f830de071ebcda58148003698f32205b7970b01c58a197ac60d6bb79241b0: done |++++++++++++++++++++++++++++++++++++++|
layer-sha256:d7c9fa7589ae28cd3306b204d5dd9a539612593e35df70f7a1d69ff7548e74cf: done |++++++++++++++++++++++++++++++++++++++|
layer-sha256:bf2b3ee132db5b4c65432e53aca69da4e609c6cb154e0d0e14b2b02259e9c1e3: done |++++++++++++++++++++++++++++++++++++++|
config-sha256:7ce0143dee376bfd2937b499a46fb110bda3c629c195b84b1cf6e19be1a9e23b: done |++++++++++++++++++++++++++++++++++++++|
layer-sha256:3c1eaf69ff492177c34bdbf1735b6f2e5400e417f8f11b98b0da878f4ecad5fb: done |++++++++++++++++++++++++++++++++++++++|
layer-sha256:29291e31a76a7e560b9b7ad3cada56e8c18d50a96cca8a2573e4f4689d7aca77: done |++++++++++++++++++++++++++++++++++++++|
elapsed: 11.9s total: 8.7 Mi (748.1 KiB/s)
unpacking linux/amd64 sha256:bead42240255ae1485653a956ef41c9e458eb077fcb6dc664cbc3aa9701a05ce...
done: 410.86624ms
也可以使用 --platform 選項(xiàng)指定對應(yīng)平臺的鏡像。當(dāng)然對應(yīng)的也有推送鏡像的命令 ctr image push,如果是私有鏡像則在推送的時(shí)候可以通過 --user 來自定義倉庫的用戶名和密碼。
列出本地鏡像
? ~ ctr image ls
REF TYPE DIGEST SIZE PLATFORMS LABELS
docker.io/library/nginx:alpine application/vnd.docker.distribution.manifest.list.v2+json sha256:bead42240255ae1485653a956ef41c9e458eb077fcb6dc664cbc3aa9701a05ce 9.5 MiB linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8,linux/ppc64le,linux/s390x -
? ~ ctr image ls -q
docker.io/library/nginx:alpine
使用 -q(--quiet) 選項(xiàng)可以只打印鏡像名稱。
檢測本地鏡像
? ~ ctr image check
REF TYPE DIGEST STATUS SIZE UNPACKED
docker.io/library/nginx:alpine application/vnd.docker.distribution.manifest.list.v2+json sha256:bead42240255ae1485653a956ef41c9e458eb077fcb6dc664cbc3aa9701a05ce complete (7/7) 9.5 MiB/9.5 MiB true
主要查看其中的 STATUS,complete 表示鏡像是完整可用的狀態(tài)。
重新打標(biāo)簽
同樣的我們也可以重新給指定的鏡像打一個 Tag:
? ~ ctr image tag docker.io/library/nginx:alpine harbor.k8s.local/course/nginx:alpine
harbor.k8s.local/course/nginx:alpine
? ~ ctr image ls -q
docker.io/library/nginx:alpine
harbor.k8s.local/course/nginx:alpine
刪除鏡像
不需要使用的鏡像也可以使用 ctr image rm 進(jìn)行刪除:
? ~ ctr image rm harbor.k8s.local/course/nginx:alpine
harbor.k8s.local/course/nginx:alpine
? ~ ctr image ls -q
docker.io/library/nginx:alpine
加上 --sync 選項(xiàng)可以同步刪除鏡像和所有相關(guān)的資源。
將鏡像掛載到主機(jī)目錄
? ~ ctr image mount docker.io/library/nginx:alpine /mnt
sha256:c3554b2d61e3c1cffcaba4b4fa7651c644a3354efaafa2f22cb53542f6c600dc
/mnt
? ~ tree -L 1 /mnt
/mnt
├── bin
├── dev
├── docker-entrypoint.d
├── docker-entrypoint.sh
├── etc
├── home
├── lib
├── media
├── mnt
├── opt
├── proc
├── root
├── run
├── sbin
├── srv
├── sys
├── tmp
├── usr
└── var
18 directories, 1 file
將鏡像從主機(jī)目錄上卸載
? ~ ctr image unmount /mnt
/mnt
將鏡像導(dǎo)出為壓縮包
? ~ ctr image export nginx.tar.gz docker.io/library/nginx:alpine
從壓縮包導(dǎo)入鏡像
? ~ ctr image import nginx.tar.gz
容器操作
容器相關(guān)操作可以通過 ctr container 獲取。
創(chuàng)建容器
? ~ ctr container create docker.io/library/nginx:alpine nginx
列出容器
? ~ ctr container ls
CONTAINER IMAGE RUNTIME
nginx docker.io/library/nginx:alpine io.containerd.runc.v2
同樣可以加上 -q 選項(xiàng)精簡列表內(nèi)容:
? ~ ctr container ls -q
nginx
查看容器詳細(xì)配置
類似于 docker inspect 功能。
? ~ ctr container info nginx
{
"ID": "nginx",
"Labels": {
"io.containerd.image.config.stop-signal": "SIGQUIT"
},
"Image": "docker.io/library/nginx:alpine",
"Runtime": {
"Name": "io.containerd.runc.v2",
"Options": {
"type_url": "containerd.runc.v1.Options"
}
},
"SnapshotKey": "nginx",
"Snapshotter": "overlayfs",
"CreatedAt": "2021-08-12T08:23:13.792871558Z",
"UpdatedAt": "2021-08-12T08:23:13.792871558Z",
"Extensions": null,
"Spec": {
......
刪除容器
? ~ ctr container rm nginx
? ~ ctr container ls
CONTAINER IMAGE RUNTIME
除了使用 rm 子命令之外也可以使用 delete 或者 del 刪除容器。
任務(wù)
上面我們通過 container create 命令創(chuàng)建的容器,并沒有處于運(yùn)行狀態(tài),只是一個靜態(tài)的容器。一個 container 對象只是包含了運(yùn)行一個容器所需的資源及相關(guān)配置數(shù)據(jù),表示 namespaces、rootfs 和容器的配置都已經(jīng)初始化成功了,只是用戶進(jìn)程還沒有啟動。
一個容器真正運(yùn)行起來是由 Task 任務(wù)實(shí)現(xiàn)的,Task 可以為容器設(shè)置網(wǎng)卡,還可以配置工具來對容器進(jìn)行監(jiān)控等。
Task 相關(guān)操作可以通過 ctr task 獲取,如下我們通過 Task 來啟動容器:
? ~ ctr task start -d nginx
/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
啟動容器后可以通過 task ls 查看正在運(yùn)行的容器:
? ~ ctr task ls
TASK PID STATUS
nginx 3630 RUNNING
同樣也可以使用 exec 命令進(jìn)入容器進(jìn)行操作:
? ~ ctr task exec --exec-id 0 -t nginx sh
/ #
不過這里需要注意必須要指定 --exec-id 參數(shù),這個 id 可以隨便寫,只要唯一就行。
暫停容器,和 docker pause 類似的功能:
? ~ ctr task pause nginx
暫停后容器狀態(tài)變成了 PAUSED:
? ~ ctr task ls
TASK PID STATUS
nginx 3630 PAUSED
同樣也可以使用 resume 命令來恢復(fù)容器:
? ~ ctr task resume nginx
? ~ ctr task ls
TASK PID STATUS
nginx 3630 RUNNING
不過需要注意 ctr 沒有 stop 容器的功能,只能暫?;蛘邭⑺廊萜?。殺死容器可以使用 task kill 命令:
? ~ ctr task kill nginx
? ~ ctr task ls
TASK PID STATUS
nginx 3630 STOPPED
殺掉容器后可以看到容器的狀態(tài)變成了 STOPPED。同樣也可以通過 task rm 命令刪除 Task:
? ~ ctr task rm nginx
? ~ ctr task ls
TASK PID STATUS
除此之外我們還可以獲取容器的 cgroup 相關(guān)信息,可以使用 task metrics 命令用來獲取容器的內(nèi)存、CPU 和 PID 的限額與使用量。
# 重新啟動容器
? ~ ctr task metrics nginx
ID TIMESTAMP
nginx 2021-08-12 08:50:46.952769941 +0000 UTC
METRIC VALUE
memory.usage_in_bytes 8855552
memory.limit_in_bytes 9223372036854771712
memory.stat.cache 0
cpuacct.usage 22467106
cpuacct.usage_percpu [2962708 860891 1163413 1915748 1058868 2888139 6159277 5458062]
pids.current 9
pids.limit 0
還可以使用 task ps 命令查看容器中所有進(jìn)程在宿主機(jī)中的 PID:
? ~ ctr task ps nginx
PID INFO
3984 -
4029 -
4030 -
4031 -
4032 -
4033 -
4034 -
4035 -
4036 -
? ~ ctr task ls
TASK PID STATUS
nginx 3984 RUNNING
其中第一個 PID 3984 就是我們?nèi)萜髦械?號進(jìn)程。
命名空間
另外 Containerd 中也支持命名空間的概念,比如查看命名空間:
? ~ ctr ns ls
NAME LABELS
default
如果不指定,ctr 默認(rèn)使用的是 default 空間。同樣也可以使用 ns create 命令創(chuàng)建一個命名空間:
? ~ ctr ns create test
? ~ ctr ns ls
NAME LABELS
default
test
使用 remove 或者 rm 可以刪除 namespace:
? ~ ctr ns rm test
test
? ~ ctr ns ls
NAME LABELS
default
有了命名空間后就可以在操作資源的時(shí)候指定 namespace,比如查看 test 命名空間的鏡像,可以在操作命令后面加上 -n test 選項(xiàng):
? ~ ctr -n test image ls
REF TYPE DIGEST SIZE PLATFORMS LABELS
我們知道 Docker 其實(shí)也是默認(rèn)調(diào)用的 containerd,事實(shí)上 Docker 使用的 containerd 下面的命名空間默認(rèn)是 moby,而不是 default,所以假如我們有用 docker 啟動容器,那么我們也可以通過 ctr -n moby 來定位下面的容器:
? ~ ctr -n moby container ls
同樣 Kubernetes 下使用的 containerd 默認(rèn)命名空間是 k8s.io,所以我們可以使用 ctr -n k8s.io 來查看 Kubernetes 下面創(chuàng)建的容器。后續(xù)我們再介紹如何將 Kubernetes 集群的容器運(yùn)行時(shí)切換到 containerd。
K8S 進(jìn)階訓(xùn)練營
點(diǎn)擊屏末 | 閱讀原文 | 即刻學(xué)習(xí)

掃描二維碼獲取
更多云原生知識
k8s 技術(shù)圈

