底層 Linux 容器執行時之發展史

Daniel Walsh發表於2018-06-14

“容器執行時”是一個被過度使用的名詞。

在 Red Hat,我們樂意這麼說,“容器即 Linux,Linux 即容器”。下面解釋一下這種說法。傳統的容器是作業系統中的程式,通常具有如下 3 個特性:

  1. 資源限制

    當你在系統中執行多個容器時,你肯定不希望某個容器獨佔系統資源,所以我們需要使用資源約束來控制 CPU、記憶體和網路頻寬等資源。Linux 核心提供了 cgroup 特性,可以通過配置控制容器程式的資源使用。

  2. 安全性配置

    一般而言,你不希望你的容器可以攻擊其它容器或甚至攻擊宿主機系統。我們使用了 Linux 核心的若干特性建立安全隔離,相關特性包括 SELinux、seccomp 和 capabilities。

    (LCTT 譯註:從 2.2 版本核心開始,Linux 將特權從超級使用者中分離,產生了一系列可以單獨啟用或關閉的 capabilities)

  3. 虛擬隔離

    容器外的任何程式對於容器而言都應該不可見。容器應該使用獨立的網路。不同的容器對應的程式應該都可以繫結 80 埠。每個容器的核心映像image根檔案系統rootfs(rootfs)都應該相互獨立。在 Linux 中,我們使用核心的名字空間namespace特性提供虛擬隔離virtual separation

那麼,具有安全性配置並且在 cgroup 和名字空間下執行的程式都可以稱為容器。檢視一下 Red Hat Enterprise Linux 7 作業系統中的 PID 1 的程式 systemd,你會發現 systemd 執行在一個 cgroup 下。

# tail -1 /proc/1/cgroup
1:name=systemd:/

ps 命令讓我們看到 systemd 程式具有 SELinux 標籤:

# ps -eZ | grep systemd
system_u:system_r:init_t:s0             1 ?     00:00:48 systemd

以及 capabilities:

# grep Cap /proc/1/status
...
CapEff: 0000001fffffffff
CapBnd: 0000001fffffffff
CapBnd:    0000003fffffffff

最後,檢視 /proc/1/ns 子目錄,你會發現 systemd 執行所在的名字空間。

ls -l /proc/1/ns
lrwxrwxrwx. 1 root root 0 Jan 11 11:46 mnt -> mnt:[4026531840]
lrwxrwxrwx. 1 root root 0 Jan 11 11:46 net -> net:[4026532009]
lrwxrwxrwx. 1 root root 0 Jan 11 11:46 pid -> pid:[4026531836]
...

如果 PID 1 程式(實際上每個系統程式)具有資源約束、安全性配置和名字空間,那麼我可以說系統上的每一個程式都執行在容器中。

容器執行時工具也不過是修改了資源約束、安全性配置和名字空間,然後 Linux 核心執行起程式。容器啟動後,容器執行時可以在容器內監控 PID 1 程式,也可以監控容器的標準輸入/輸出,從而進行容器程式的生命週期管理。

容器執行時

你可能自言自語道,“哦,systemd 看起來很像一個容器執行時”。經過若干次關於“為何容器執行時不使用 systemd-nspawn 工具來啟動容器”的郵件討論後,我認為值得討論一下容器執行時及其發展史。

Docker 通常被稱為容器執行時,但“容器執行時container runtime”是一個被過度使用的詞語。當使用者提到“容器執行時”,他們其實提到的是為開發人員提供便利的上層high-level工具,包括 Docker,CRI-ORKT。這些工具都是基於 API 的,涉及操作包括從容器倉庫拉取容器映象、配置儲存和啟動容器等。啟動容器通常涉及一個特殊工具,用於配置核心如何執行容器,這類工具也被稱為“容器執行時”,下文中我將稱其為“底層容器執行時”以作區分。像 Docker、CRI-O 這樣的守護程式及形如 PodmanBuildah 的命令列工具,似乎更應該被稱為“容器管理器”。

早期版本的 Docker 使用 lxc 工具集啟動容器,該工具出現在 systemd-nspawn 之前。Red Hat 最初試圖將 libvirtlibvirt-lxc)整合到 Docker 中替代 lxc 工具,因為 RHEL 並不支援 lxclibvirt-lxc 也沒有使用 systemd-nspawn,在那時 systemd 團隊僅將 systemd-nspawn 視為測試工具,不適用於生產環境。

與此同時,包括我的 Red Hat 團隊部分成員在內的上游upstream Docker 開發者,認為應該採用 golang 原生的方式啟動容器,而不是呼叫外部應用。他們的工作促成了 libcontainer 這個 golang 原生庫,用於啟動容器。Red Hat 工程師更看好該庫的發展前景,放棄了 libvirt-lxc

後來成立 開放容器組織Open Container Initiative(OCI)的部分原因就是人們希望用其它方式啟動容器。傳統的基於名字空間隔離的容器已經家喻戶曉,但人們也有虛擬機器級別隔離virtual machine-level isolation的需求。Intel 和 Hyper.sh 正致力於開發基於 KVM 隔離的容器,Microsoft 致力於開發基於 Windows 的容器。OCI 希望有一份定義容器的標準規範,因而產生了 OCI 執行時規範Runtime Specification

OCI 執行時規範定義了一個 JSON 檔案格式,用於描述要執行的二進位制,如何容器化以及容器根檔案系統的位置。一些工具用於生成符合標準規範的 JSON 檔案,另外的工具用於解析 JSON 檔案並在該根檔案系統(rootfs)上執行容器。Docker 的部分程式碼被抽取出來構成了 libcontainer 專案,該專案被貢獻給 OCI。上游 Docker 工程師及我們自己的工程師建立了一個新的前端工具,用於解析符合 OCI 執行時規範的 JSON 檔案,然後與 libcontainer 互動以便啟動容器。這個前端工具就是 runc,也被貢獻給 OCI。雖然 runc 可以解析 OCI JSON 檔案,但使用者需要自行生成這些檔案。此後,runc 也成為了最流行的底層容器執行時,基本所有的容器管理工具都支援 runc,包括 CRI-O、Docker、Buildah、Podman 和 Cloud Foundry Garden 等。此後,其它工具的實現也參照 OCI 執行時規範,以便可以執行 OCI 相容的容器。

Clear Containers 和 Hyper.sh 的 runV 工具都是參照 OCI 執行時規範執行基於 KVM 的容器,二者將其各自工作合併到一個名為 Kata 的新專案中。在去年,Oracle 建立了一個示例版本的 OCI 執行時工具,名為 RailCar,使用 Rust 語言編寫。但該 GitHub 專案已經兩個月沒有更新了,故無法判斷是否仍在開發。幾年前,Vincent Batts 試圖建立一個名為 nspawn-oci 的工具,用於解析 OCI 執行時規範檔案並啟動 systemd-nspawn;但似乎沒有引起大家的注意,而且也不是原生的實現。

如果有開發者希望實現一個原生的 systemd-nspawn --oci OCI-SPEC.json 並讓 systemd 團隊認可和提供支援,那麼CRI-O、Docker 和 Podman 等容器管理工具將可以像使用 runc 和 Clear Container/runV (Kata) 那樣使用這個新的底層執行時。(目前我的團隊沒有人蔘與這方面的工作。)

總結如下,在 3-4 年前,上游開發者打算編寫一個底層的 golang 工具用於啟動容器,最終這個工具就是 runc。那時開發者有一個使用 C 編寫的 lxc 工具,在 runc 開發後,他們很快轉向 runc。我很確信,當決定構建 libcontainer 時,他們對 systemd-nspawn 或其它非原生(即不使用 golang)的執行 namespaces 隔離的容器的方式都不感興趣。


via: https://opensource.com/article/18/1/history-low-level-container-runtimes

作者:Daniel Walsh 譯者:pinewall 校對:wxy

本文由 LCTT 原創編譯,Linux中國 榮譽推出

相關文章