一文搞定 Containerd 的使用

在學習 Containerd 之前我們有必要對 Docker 的發展歷史做一個簡單的回顧,因爲這裏面牽涉到的組件實戰是有點多,有很多我們會經常聽到,但是不清楚這些組件到底是幹什麼用的,比如 libcontainerrunccontainerdCRIOCI 等等。

Docker

從 Docker 1.11 版本開始,Docker 容器運行就不是簡單通過 Docker Daemon 來啓動了,而是通過集成 containerd、runc 等多個組件來完成的。雖然 Docker Daemon 守護進程模塊在不停的重構,但是基本功能和定位沒有太大的變化,一直都是 CS 架構,守護進程負責和 Docker Client 端交互,並管理 Docker 鏡像和容器。現在的架構中組件 containerd 就會負責集羣節點上容器的生命週期管理,並向上爲 Docker Daemon 提供 gRPC 接口。

docker 架構

當我們要創建一個容器的時候,現在 Docker Daemon 並不能直接幫我們創建了,而是請求 containerd 來創建一個容器,containerd 收到請求後,也並不會直接去操作容器,而是創建一個叫做 containerd-shim 的進程,讓這個進程去操作容器,我們指定容器進程是需要一個父進程來做狀態收集、維持 stdin 等 fd 打開等工作的,假如這個父進程就是 containerd,那如果 containerd 掛掉的話,整個宿主機上所有的容器都得退出了,而引入 containerd-shim 這個墊片就可以來規避這個問題了。

然後創建容器需要做一些 namespaces 和 cgroups 的配置,以及掛載 root 文件系統等操作,這些操作其實已經有了標準的規範,那就是 OCI(開放容器標準),runc 就是它的一個參考實現(Docker 被逼無耐將 libcontainer 捐獻出來改名爲 runc 的),這個標準其實就是一個文檔,主要規定了容器鏡像的結構、以及容器需要接收哪些操作指令,比如 create、start、stop、delete 等這些命令。runc 就可以按照這個 OCI 文檔來創建一個符合規範的容器,既然是標準肯定就有其他 OCI 實現,比如 Kata、gVisor 這些容器運行時都是符合 OCI 標準的。

所以真正啓動容器是通過 containerd-shim 去調用 runc 來啓動容器的,runc 啓動完容器後本身會直接退出,containerd-shim 則會成爲容器進程的父進程, 負責收集容器進程的狀態, 上報給 containerd, 並在容器中 pid 爲 1 的進程退出後接管容器中的子進程進行清理, 確保不會出現殭屍進程。

而 Docker 將容器操作都遷移到 containerd 中去是因爲當前做 Swarm,想要進軍 PaaS 市場,做了這個架構切分,讓 Docker Daemon 專門去負責上層的封裝編排,當然後面的結果我們知道 Swarm 在 Kubernetes 面前是慘敗,然後 Docker 公司就把 containerd 項目捐獻給了 CNCF 基金會,這個也是現在的 Docker 架構。

CRI

我們知道 Kubernetes 提供了一個 CRI 的容器運行時接口,那麼這個 CRI 到底是什麼呢?這個其實也和 Docker 的發展密切相關的。

在 Kubernetes 早期的時候,當時 Docker 實在是太火了,Kubernetes 當然會先選擇支持 Docker,而且是通過硬編碼的方式直接調用 Docker API,後面隨着 Docker 的不斷髮展以及 Google 的主導,出現了更多容器運行時,Kubernetes 爲了支持更多更精簡的容器運行時,Google 就和紅帽主導推出了 CRI 標準,用於將 Kubernetes 平臺和特定的容器運行時(當然主要是爲了幹掉 Docker)解耦。

CRI(Container Runtime Interface 容器運行時接口)本質上就是 Kubernetes 定義的一組與容器運行時進行交互的接口,所以只要實現了這套接口的容器運行時都可以對接到 Kubernetes 平臺上來。不過 Kubernetes 推出 CRI 這套標準的時候還沒有現在的統治地位,所以有一些容器運行時可能不會自身就去實現 CRI 接口,於是就有了 shim(墊片), 一個 shim 的職責就是作爲適配器將各種容器運行時本身的接口適配到 Kubernetes 的 CRI 接口上,其中 dockershim 就是 Kubernetes 對接 Docker 到 CRI 接口上的一個墊片實現。

cri shim

Kubelet 通過 gRPC 框架與容器運行時或 shim 進行通信,其中 kubelet 作爲客戶端,CRI shim(也可能是容器運行時本身)作爲服務器。

CRI 定義的 API(https://github.com/kubernetes/kubernetes/blob/release-1.5/pkg/kubelet/api/v1alpha1/runtime/api.proto) 主要包括兩個 gRPC 服務,ImageServiceRuntimeServiceImageService 服務主要是拉取鏡像、查看和刪除鏡像等操作,RuntimeService 則是用來管理 Pod 和容器的生命週期,以及與容器交互的調用(exec/attach/port-forward)等操作,可以通過 kubelet 中的標誌 --container-runtime-endpoint--image-service-endpoint 來配置這兩個服務的套接字。

kubelet cri

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

dockershim

現在如果我們使用的是 Docker 的話,當我們在 Kubernetes 中創建一個 Pod 的時候,首先就是 kubelet 通過 CRI 接口調用 dockershim,請求創建一個容器,kubelet 可以視作一個簡單的 CRI Client, 而 dockershim 就是接收請求的 Server,不過他們都是在 kubelet 內置的。

dockershim 收到請求後, 轉化成 Docker Daemon 能識別的請求, 發到 Docker Daemon 上請求創建一個容器,請求到了 Docker Daemon 後續就是 Docker 創建容器的流程了,去調用 containerd,然後創建 containerd-shim 進程,通過該進程去調用 runc 去真正創建容器。

其實我們仔細觀察也不難發現使用 Docker 的話其實是調用鏈比較長的,真正容器相關的操作其實 containerd 就完全足夠了,Docker 太過於複雜笨重了,當然 Docker 深受歡迎的很大一個原因就是提供了很多對用戶操作比較友好的功能,但是對於 Kubernetes 來說壓根不需要這些功能,因爲都是通過接口去操作容器的,所以自然也就可以將容器運行時切換到 containerd 來。

切換到 containerd

切換到 containerd 可以消除掉中間環節,操作體驗也和以前一樣,但是由於直接用容器運行時調度容器,所以它們對 Docker 來說是不可見的。因此,你以前用來檢查這些容器的 Docker 工具就不能使用了。

你不能再使用 docker psdocker inspect 命令來獲取容器信息。由於不能列出容器,因此也不能獲取日誌、停止容器,甚至不能通過 docker exec 在容器中執行命令。

當然我們仍然可以下載鏡像,或者用 docker build 命令構建鏡像,但用 Docker 構建、下載的鏡像,對於容器運行時和 Kubernetes,均不可見。爲了在 Kubernetes 中使用,需要把鏡像推送到鏡像倉庫中去。

從上圖可以看出在 containerd 1.0 中,對 CRI 的適配是通過一個單獨的 CRI-Containerd 進程來完成的,這是因爲最開始 containerd 還會去適配其他的系統(比如 swarm),所以沒有直接實現 CRI,所以這個對接工作就交給 CRI-Containerd 這個 shim 了。

然後到了 containerd 1.1 版本後就去掉了 CRI-Containerd 這個 shim,直接把適配邏輯作爲插件的方式集成到了 containerd 主進程中,現在這樣的調用就更加簡潔了。

containerd cri

與此同時 Kubernetes 社區也做了一個專門用於 Kubernetes 的 CRI 運行時 CRI-O,直接兼容 CRI 和 OCI 規範。

cri-o

這個方案和 containerd 的方案顯然比默認的 dockershim 簡潔很多,不過由於大部分用戶都比較習慣使用 Docker,所以大家還是更喜歡使用 dockershim 方案。

但是隨着 CRI 方案的發展,以及其他容器運行時對 CRI 的支持越來越完善,Kubernetes 社區在 2020 年 7 月份就開始着手移除 dockershim 方案了:https://github.com/kubernetes/enhancements/tree/master/keps/sig-node/2221-remove-dockershim,現在的移除計劃是在 1.20 版本中將 kubelet 中內置的 dockershim 代碼分離,將內置的 dockershim 標記爲維護模式,當然這個時候仍然還可以使用 dockershim,目標是在 1.23/1.24 版本發佈沒有 dockershim 的版本(代碼還在,但是要默認支持開箱即用的 docker 需要自己構建 kubelet,會在某個寬限期過後從 kubelet 中刪除內置的 dockershim 代碼)。

那麼這是否就意味着 Kubernetes 不再支持 Docker 了呢?當然不是的,這只是廢棄了內置的 dockershim 功能而已,Docker 和其他容器運行時將一視同仁,不會單獨對待內置支持,如果我們還想直接使用 Docker 這種容器運行時應該怎麼辦呢?可以將 dockershim 的功能單獨提取出來獨立維護一個 cri-dockerd 即可,就類似於 containerd 1.0 版本中提供的 CRI-Containerd,當然還有一種辦法就是 Docker 官方社區將 CRI 接口內置到 Dockerd 中去實現。

但是我們也清楚 Dockerd 也是去直接調用的 Containerd,而 containerd 1.1 版本後就內置實現了 CRI,所以 Docker 也沒必要再去單獨實現 CRI 了,當 Kubernetes 不再內置支持開箱即用的 Docker 的以後,最好的方式當然也就是直接使用 Containerd 這種容器運行時,而且該容器運行時也已經經過了生產環境實踐的,接下來我們就來學習下 Containerd 的使用。

Containerd

我們知道很早之前的 Docker Engine 中就有了 containerd,只不過現在是將 containerd 從 Docker Engine 裏分離出來,作爲一個獨立的開源項目,目標是提供一個更加開放、穩定的容器運行基礎設施。分離出來的 containerd 將具有更多的功能,涵蓋整個容器運行時管理的所有需求,提供更強大的支持。

containerd 是一個工業級標準的容器運行時,它強調簡單性健壯性可移植性,containerd 可以負責幹下面這些事情:

架構

containerd 可用作 Linux 和 Windows 的守護程序,它管理其主機系統完整的容器生命週期,從鏡像傳輸和存儲到容器執行和監測,再到底層存儲到網絡附件等等。

containerd 架構

上圖是 containerd 官方提供的架構圖,可以看出 containerd 採用的也是 C/S 架構,服務端通過 unix domain socket 暴露低層的 gRPC API 接口出去,客戶端通過這些 API 管理節點上的容器,每個 containerd 只負責一臺機器,Pull 鏡像,對容器的操作(啓動、停止等),網絡,存儲都是由 containerd 完成。具體運行容器由 runc 負責,實際上只要是符合 OCI 規範的容器都可以支持。

爲了解耦,containerd 將系統劃分成了不同的組件,每個組件都由一個或多個模塊協作完成(Core 部分),每一種類型的模塊都以插件的形式集成到 Containerd 中,而且插件之間是相互依賴的,例如,上圖中的每一個長虛線的方框都表示一種類型的插件,包括 Service Plugin、Metadata Plugin、GC Plugin、Runtime Plugin 等,其中 Service Plugin 又會依賴 Metadata Plugin、GC Plugin 和 Runtime Plugin。每一個小方框都表示一個細分的插件,例如 Metadata Plugin 依賴 Containers Plugin、Content Plugin 等。比如:

總體來看 containerd 可以分爲三個大塊:Storage、Metadata 和 Runtime。

containerd 架構 2

安裝

這裏我使用的系統是 Linux Mint 20.2,首先需要安裝 seccomp 依賴:

➜  ~ apt-get update
➜  ~ apt-get install libseccomp2 -y

由於 containerd 需要調用 runc,所以我們也需要先安裝 runc,不過 containerd 提供了一個包含相關依賴的壓縮包 cri-containerd-cni-${VERSION}.${OS}-${ARCH}.tar.gz,可以直接使用這個包來進行安裝。首先從 release 頁面下載最新版本的壓縮包,當前爲 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 選項直接看到壓縮包中包含哪些文件:

➜  ~ 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

直接將壓縮包解壓到系統的各個目錄中:

➜  ~ tar -C / -xzf cri-containerd-cni-1.5.5-linux-amd64.tar.gz

當然要記得將 /usr/local/bin/usr/local/sbin 追加到 ~/.bashrc 文件的 PATH 環境變量中:

export PATH=$PATH:/usr/local/bin:/usr/local/sbin

然後執行下面的命令使其立即生效:

➜  ~ source ~/.bashrc

containerd 的默認配置文件爲 /etc/containerd/config.toml,我們可以通過如下所示的命令生成一個默認的配置:

➜  ~ mkdir /etc/containerd
➜  ~ containerd config default > /etc/containerd/config.toml

由於上面我們下載的 containerd 壓縮包中包含一個 etc/systemd/system/containerd.service 的文件,這樣我們就可以通過 systemd 來配置 containerd 作爲守護進程運行了,內容如下所示:

➜  ~ 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

這裏有兩個重要的參數:

我們需要將 KillMode 的值設置爲 process,這樣可以確保升級或重啓 containerd 時不殺死現有的容器。

現在我們就可以啓動 containerd 了,直接執行下面的命令即可:

➜  ~ systemctl enable containerd --now

啓動完成後就可以使用 containerd 的本地 CLI 工具 ctr 了,比如查看版本:

ctr version

配置

我們首先來查看下上面默認生成的配置文件 /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

這個配置文件比較複雜,我們可以將重點放在其中的 plugins 配置上面,仔細觀察我們可以發現每一個頂級配置塊的命名都是 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,還可以配置默認的 runtime。比如現在我們要爲鏡像配置一個加速器,那麼就需要在 cri 配置塊下面的 registry 配置塊下面進行配置 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"]

另外在默認配置中還有兩個關於存儲的配置路徑:

root = "/var/lib/containerd"
state = "/run/containerd"

其中 root 是用來保存持久化數據,包括 Snapshots, Content, Metadata 以及各種插件的數據,每一個插件都有自己單獨的目錄,Containerd 本身不存儲任何數據,它的所有功能都來自於已加載的插件。

而另外的 state 是用來保存運行時的臨時數據的,包括 sockets、pid、掛載點、運行時狀態以及不需要持久化的插件數據。

使用

我們知道 Docker CLI 工具提供了需要增強用戶體驗的功能,containerd 同樣也提供一個對應的 CLI 工具:ctr,不過 ctr 的功能沒有 docker 完善,但是關於鏡像和容器的基本功能都是有的。接下來我們就先簡單介紹下 ctr 的使用。

幫助

直接輸入 ctr 命令即可獲得所有相關的操作命令使用方式:

➜  ~ 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 選項指定對應平臺的鏡像。當然對應的也有推送鏡像的命令 ctr image push,如果是私有鏡像則在推送的時候可以通過 --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) 選項可以只打印鏡像名稱。

檢測本地鏡像

➜  ~ 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

主要查看其中的 STATUScomplete 表示鏡像是完整可用的狀態。

重新打標籤

同樣的我們也可以重新給指定的鏡像打一個 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 進行刪除:

➜  ~ 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 選項可以同步刪除鏡像和所有相關的資源。

將鏡像掛載到主機目錄

➜  ~ 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

將鏡像從主機目錄上卸載

➜  ~ ctr image unmount /mnt
/mnt

將鏡像導出爲壓縮包

➜  ~ ctr image export nginx.tar.gz docker.io/library/nginx:alpine

從壓縮包導入鏡像

➜  ~ ctr image import nginx.tar.gz

容器操作

容器相關操作可以通過 ctr container 獲取。

創建容器

➜  ~ 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 選項精簡列表內容:

➜  ~ ctr container ls -q
nginx

查看容器詳細配置

類似於 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 刪除容器。

任務

上面我們通過 container create 命令創建的容器,並沒有處於運行狀態,只是一個靜態的容器。一個 container 對象只是包含了運行一個容器所需的資源及相關配置數據,表示 namespaces、rootfs 和容器的配置都已經初始化成功了,只是用戶進程還沒有啓動。

一個容器真正運行起來是由 Task 任務實現的,Task 可以爲容器設置網卡,還可以配置工具來對容器進行監控等。

Task 相關操作可以通過 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 查看正在運行的容器:

➜  ~ ctr task ls
TASK     PID     STATUS
nginx    3630    RUNNING

同樣也可以使用 exec 命令進入容器進行操作:

➜  ~ ctr task exec --exec-id 0 -t nginx sh
/ #

不過這裏需要注意必須要指定 --exec-id 參數,這個 id 可以隨便寫,只要唯一就行。

暫停容器,和 docker pause 類似的功能:

➜  ~ ctr task pause nginx

暫停後容器狀態變成了 PAUSED

➜  ~ ctr task ls
TASK     PID     STATUS
nginx    3630    PAUSED

同樣也可以使用 resume 命令來恢復容器:

➜  ~ 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

殺掉容器後可以看到容器的狀態變成了 STOPPED。同樣也可以通過 task rm 命令刪除 Task:

➜  ~ ctr task rm nginx
➜  ~ ctr task ls
TASK    PID    STATUS

除此之外我們還可以獲取容器的 cgroup 相關信息,可以使用 task metrics 命令用來獲取容器的內存、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 命令查看容器中所有進程在宿主機中的 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 就是我們容器中的 1 號進程。

命名空間

另外 Containerd 中也支持命名空間的概念,比如查看命名空間:

➜  ~ ctr ns ls
NAME    LABELS
default

如果不指定,ctr 默認使用的是 default 空間。同樣也可以使用 ns create 命令創建一個命名空間:

➜  ~ 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

有了命名空間後就可以在操作資源的時候指定 namespace,比如查看 test 命名空間的鏡像,可以在操作命令後面加上 -n test 選項:

➜  ~ ctr -n test image ls
REF TYPE DIGEST SIZE PLATFORMS LABELS

我們知道 Docker 其實也是默認調用的 containerd,事實上 Docker 使用的 containerd 下面的命名空間默認是 moby,而不是 default,所以假如我們有用 docker 啓動容器,那麼我們也可以通過 ctr -n moby 來定位下面的容器:

➜  ~ ctr -n moby container ls

同樣 Kubernetes 下使用的 containerd 默認命名空間是 k8s.io,所以我們可以使用 ctr -n k8s.io 來查看 Kubernetes 下面創建的容器。後續我們再介紹如何將 Kubernetes 集羣的容器運行時切換到 containerd

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/--t74RuFGMmTGl2IT-TFrg