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

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

編譯自: https://opensource.com/article/18/1/history-low-level-container-runtimes

譯者: Andy Song

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

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

  1. 資源限制
  2. 當你在系統中運行多個容器時,你肯定不希望某個容器獨佔系統資源,所以我們需要使用資源約束來控制 CPU、內存和網絡帶寬等資源。Linux 內核提供了 cgroup 特性,可以通過配置控制容器進程的資源使用。
  3. 安全性配置
  4. 一般而言,你不希望你的容器可以攻擊其它容器或甚至攻擊宿主機系統。我們使用了 Linux 內核的若干特性建立安全隔離,相關特性包括 SELinux、seccomp 和 capabilities。
  5. (LCTT 譯註:從 2.2 版本內核開始,Linux 將特權從超級用戶中分離,產生了一系列可以單獨啟用或關閉的 capabilities)
  6. 虛擬隔離
  7. 容器外的任何進程對於容器而言都應該不可見。容器應該使用獨立的網絡。不同的容器對應的進程應該都可以綁定 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-O 和 RKT 。這些工具都是基於 API 的,涉及操作包括從容器倉庫拉取容器鏡像、配置存儲和啟動容器等。啟動容器通常涉及一個特殊工具,用於配置內核如何運行容器,這類工具也被稱為“容器運行時”,下文中我將稱其為“底層容器運行時”以作區分。像 Docker、CRI-O 這樣的守護進程及形如 Podman 、 Buildah 的命令行工具,似乎更應該被稱為“容器管理器”。

早期版本的 Docker 使用 lxc 工具集啟動容器,該工具出現在 systemd-nspawn 之前。Red Hat 最初試圖將 libvirt (libvirt-lxc)集成到 Docker 中替代 lxc 工具,因為 RHEL 並不支持 lxc。libvirt-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


分享到:


相關文章: