容器執行時分析

2022-07-03 06:01:35

什麼是RunC

Docker、Google、CoreOS 和其他供應商建立了OCI 開放容器計劃。目前有兩個標準檔案:

  • 容器執行時標準(runtime spec)
  • 容器映象標準(image spec)

OCI 對容器runtime的標準主要是指定容器的執行狀態,如runtime需要提供的命令。下圖是容器狀態轉換圖:

  • init 狀態:這個狀態並不再標準中,僅表示沒有容器存在的初始狀態
  • creating:使用create 命令建立容器,狀態為建立中
  • created:容器建立出來,但是還沒執行,表示映象和設定沒有錯誤,容器可以執行在當前平臺
  • running: 容器的執行狀態,裡面的程序處理up狀態
  • stopped:容器執行完成、出錯或者stop命令之後,容器處於暫停狀態。

RunC就是一個命令列工具,可以操作宿主機的核心來管理Namespace和CGroups。使用runc,我們可以建立,啟動,停止,刪除容器。

Runc的由來

runc是從docker的libcontianer中遷移出來的。實現了容器啟動,停止,資源隔離等功能。Docker講runc捐贈給OCI作為OCI容器執行時標準的參考實現。當我們用Docker執行一個容器時,經歷瞭如下步驟:

圖片來源:《Kubernetes In Action》 2.1

  1. Docker會檢查busybox:latest映象是否己經存在於本機。如果沒有, Docker會從http://docker.io的Docker映象中心拉取映象。
  2. 將映象下載到本地並解壓為符合OCI標準的bundle檔案, 將一個檔案系統拆分成多層(overlay)
  3. Docker基於這個映象bundle檔案建立一個容器並在容器中執行命令。echo命令列印文字到標準輸出流, 然後程序終止,容器停止執行。

RunC標準化的僅僅是第三步,也是Docker 貢獻出來的部分。

怎麼使用Runc

 create the bundle
$ mkdir -p /mycontainer/rootfs

# [ab]use Docker to copy a root fs into the bundle
$ docker export $(docker create busybox) | tar -C /mycontainer/rootfs -xvf -

# create the specification, by default sh will be the entrypoint of the container
$ cd /mycontainer
$ runc spec

# launch the container
$ sudo -i
$ cd /mycontainer
$ runc run mycontainerid

# list containers
$ runc list

# stop the container
$ runc kill mycontainerid

# cleanup
$ runc delete mycontainerid

想象一下,我們用runc啟動一個容器後,我們要怎麼去跟蹤他們的狀態。我們要啟動其他幾個容器來跟蹤他們的狀態,其中一些需要在失敗時重新啟動,需要在終止時釋放資源,且要從遠端倉庫中拉去映象,並設定容器的網路和記憶體資源。如果我們要自動話這個過程,我們就需要一個容器管理器。如何用實現一個簡單的runc "Building a container from scratch in Go"

Low-level & High-level

當我們討論容器執行時的時候,我們可能會想到:runc、lxc、lmctfy、docker、rkt、cri-o。這些中的每一個都是為不同的情況構建的,實現了不同的特性功能。如 contianerd,cri-o,實際上時使用runc來執行容器,在High-level實現了映象管理和API層。如,映象推播,映象拉去,映象管理,映象解包和API。這些被視為High-level的功能。每一個High-level的實現都囊括了low-level。

從實際出發,通常只關注於正在執行的容器的runtime通常稱為「low-level」容器執行時,支援更多高階功能如(映象管理和gRPC/REST API)的執行時通常稱為「High-leve」容器執行時。二者在根本上是解決不同的問題。

Low-level容器執行時

容器是通過Linux namespace和Cgroups實現的。Namespace能讓你為每個容器提供虛擬化系統資源,如檔案系統,網路;Cgroups提供了限制每個容器所能使用的資源的方法,如CPU和記憶體。在低階別容器執行時中,其主要負責為容器建立ns和cgroups,然後在其中執行命令。一個健壯的低階容器執行時會做更多的事情,比如允許在 cgroup 上設定資源限制,設定根檔案系統,以及將容器的程序 chroot 到根檔案系統。
以下為幾種Low-level容器執行時實現

  • runc: 最常見且被廣泛使用容器執行時,代表實現就是Docker runc
  • runv: runv是一個基於虛擬機器器管理程式的執行時,它通過虛擬化guest kernel,將容器和主機隔離開,使其邊界更加清晰,這種方式很容器就能幫助加強主機和容器的安全性,代表實是kataFirecracker
  • runsc: runc+safety 典型就是google的gvisor,通過攔截應用程式的所有系統呼叫,提供安全隔離的輕量級容器執行時沙箱。截止目前,貌似沒有生產環境使用案例
  • wasm: Wasm的沙箱機制帶來的隔離型和安全性都比Docker做的更好。但是wasm處於草案階段。

High-level容器執行時


通常情況下,開發人員想要執行一個容器不僅僅需要Low-Level容器執行時提供的這些特性,同時也需要與映象格式、映象管理和共用映象相關的API介面和特性,而這些特性一般由High-Level容器執行時提供。就日常使用來說,Low-Level容器執行時提供的這些特性可能滿足不了日常所需,因為這個緣故,唯一會使用Low-Level容器執行時的人是那些實現High-Level容器執行時以及容器工具的開發人員。那些實現Low-Level容器執行時的開發者會說High-Level容器執行時比如containerd和cri-o不像真正的容器執行時,因為從他們的角度來看,他們將容器執行的實現外包給了runc。但是從使用者的角度來看,它們只是提供容器功能的單個元件,可以被另一個的實現替換,因此從這個角度將其稱為runtime仍然是有意義的。即使containerd和cri-o都使用runc,但是它們是截然不同的專案,支援的特性也是非常不同的。dockershim, containerd 和cri-o都是遵循CRI的容器執行時,我們稱他們為高層級執行時(High-level Runtime)。
以下為幾種High-level容器執行時實現

dockerd


Docker 是一個容器執行時,它包含生成、打包、共用和執行容器。Docker 是C/S架構,dockerd為伺服器端是一個守護行程,docker client使用者端負責接收命令傳送給dockerd。守護程式提供了構建容器、管理映像和執行容器的大部分邏輯,以及 API。可以執行命令列使用者端來傳送命令並從守護程式獲取資訊。
現在建立一個docker容器的時候,Docker Daemon 並不能直接幫我們建立了,而是請求 containerd 來建立一個容器。當containerd 收到請求後,也不會直接去操作容器,而是建立一個叫做 containerd-shim 的程序。讓這個程序去操作容器,我們指定容器程序是需要一個父程序來做狀態收集、維持 stdin 等 fd 開啟等工作的,假如這個父程序就是 containerd,那如果 containerd 掛掉的話,整個宿主機上所有的容器都得退出了,而引入 containerd-shim 這個墊片就可以來規避這個問題了,就是提供的live-restore的功能。這裡需要注意systemd的 MountFlags=slave。
然後建立容器需要做一些 namespaces 和 cgroups 的設定,以及掛載 root 檔案系統等操作。runc 就可以按照這個 OCI 檔案來建立一個符合規範的容器。

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

containerd


containerd也是從docker中分離出來的專案。與docker-runc不同的是,containerd是一個常駐守護行程,負責管理由runc建立的容器。監聽上層請求,來啟動、停止或者上報容器的狀態。負責管理容器的生命週期。除此之外,還負責映象的推拉和映象的本地儲存,跨容器網路管理等其他功能。containerd的底層Low-level是runc,但並不侷限於runc。可以用其他Low-level實現來替代runc。contianerd是一個工業級標準容器執行時,強調簡單性、健壯性和可移植性。主要負責如下事情:

  • 管理容器的生命週期
  • 拉去和推播容器映象
  • 儲存管理
  • 呼叫runc執行容器及其他互動
  • 管理容器網路介面和請求

contianerd與docker的區別是contianerd專注容器的管理,而docker關注於使用者端的使用。支援編譯構建映象並定義了映象的格式。
從k8s的角度看,選擇containerd作為執行時組建,它的呼叫鏈更短,組建更少,更穩定,佔用節點資源更少。

參考