Docker 引擎是用來運(yùn)行和管理容器的核心軟件。通常人們會簡單地將其代指為 Docker 或 Docker 平臺。
如果你對 VMware 略知一二,那么可以將 Docker 引擎理解為 ESXi 的角色。
基于開放容器計(jì)劃(OCI)相關(guān)標(biāo)準(zhǔn)的要求,Docker 引擎采用了模塊化的設(shè)計(jì)原則,其組件是可替換的。
從多個(gè)角度來看,Docker 引擎就像汽車引擎——二者都是模塊化的,并且由許多可交換的部件組成。
汽車引擎由許多專用的部件協(xié)同工作,從而使汽車可以行駛,例如進(jìn)氣管、節(jié)氣門、氣缸、火花塞、排氣管等。
Docker 引擎由許多專用的工具協(xié)同工作,從而可以創(chuàng)建和運(yùn)行容器,例如 API、執(zhí)行驅(qū)動(dòng)、運(yùn)行時(shí)、shim 進(jìn)程等。
Docker 引擎由如下主要的組件構(gòu)成:Docker 客戶端(Docker Client)、Docker 守護(hù)進(jìn)程(Docker daemon)、containerd 以及 runc。它們共同負(fù)責(zé)容器的創(chuàng)建和運(yùn)行。
總體邏輯如下圖所示。
Docker 首次發(fā)布時(shí),Docker 引擎由兩個(gè)核心組件構(gòu)成:LXC 和 Docker daemon。
Docker daemon 是單一的二進(jìn)制文件,包含諸如 Docker 客戶端、Docker API、容器運(yùn)行時(shí)、鏡像構(gòu)建等。
LXC 提供了對諸如命名空間(Namespace)和控制組(CGroup)等基礎(chǔ)工具的操作能力,它們是基于 Linux 內(nèi)核的容器虛擬化技術(shù)。
下圖闡釋了在 Docker 舊版本中,Docker daemon、LXC 和操作系統(tǒng)之間的交互關(guān)系。
對 LXC 的依賴自始至終都是個(gè)問題。
首先,LXC 是基于 Linux 的。這對于一個(gè)立志于跨平臺的項(xiàng)目來說是個(gè)問題。
其次,如此核心的組件依賴于外部工具,這會給項(xiàng)目帶來巨大風(fēng)險(xiǎn),甚至影響其發(fā)展。
因此,Docker 公司開發(fā)了名為 Libcontainer 的自研工具,用于替代 LXC。
Libcontainer 的目標(biāo)是成為與平臺無關(guān)的工具,可基于不同內(nèi)核為 Docker 上層提供必要的容器交互功能。
在 Docker 0.9 版本中,Libcontainer 取代 LXC 成為默認(rèn)的執(zhí)行驅(qū)動(dòng)。
隨著時(shí)間的推移,Docker daemon 的整體性帶來了越來越多的問題。難于變更、運(yùn)行越來越慢。這并非生態(tài)(或Docker公司)所期望的。
Docker 公司意識到了這些問題,開始努力著手拆解這個(gè)大而全的 Docker daemon 進(jìn)程,并將其模塊化。
這項(xiàng)任務(wù)的目標(biāo)是盡可能拆解出其中的功能特性,并用小而專的工具來實(shí)現(xiàn)它。這些小工具可以是可替換的,也可以被第三方拿去用于構(gòu)建其他工具。
這一計(jì)劃遵循了在 UNIX 中得以實(shí)踐并驗(yàn)證過的一種軟件哲學(xué):小而專的工具可以組裝為大型工具。
這項(xiàng)拆解和重構(gòu) Docker 引擎的工作仍在進(jìn)行中。不過,所有容器執(zhí)行和容器運(yùn)行時(shí)的代碼已經(jīng)完全從 daemon 中移除,并重構(gòu)為小而專的工具。
目前 Docker 引擎的架構(gòu)示意圖如下圖所示,圖中有簡要的描述。
當(dāng) Docker 公司正在進(jìn)行 Docker daemon 進(jìn)程的拆解和重構(gòu)的時(shí)候,OCI 也正在著手定義兩個(gè)容器相關(guān)的規(guī)范(或者說標(biāo)準(zhǔn))。
鏡像規(guī)范和容器運(yùn)行時(shí)規(guī)范,兩個(gè)規(guī)范均于 2017 年 7 月發(fā)布了 1.0 版。
Docker 公司參與了這些規(guī)范的制定工作,并貢獻(xiàn)了許多的代碼。
從 Docker 1.11 版本(2016 年初)開始,Docker 引擎盡可能實(shí)現(xiàn)了 OCI 的規(guī)范。例如,Docker daemon 不再包含任何容器運(yùn)行時(shí)的代碼——所有的容器運(yùn)行代碼在一個(gè)單獨(dú)的 OCI 兼容層中實(shí)現(xiàn)。
默認(rèn)情況下,Docker 使用 runc 來實(shí)現(xiàn)這一點(diǎn)。runc 是 OCI 容器運(yùn)行時(shí)標(biāo)準(zhǔn)的參考實(shí)現(xiàn)。
如上圖中的 runc 容器運(yùn)行時(shí)層。runc 項(xiàng)目的目標(biāo)之一就是與 OCI 規(guī)范保持一致。
目前 OCI 規(guī)范均為 1.0 版本,我們不希望它們頻繁地迭代,畢竟穩(wěn)定勝于一切。
除此之外,Docker 引擎中的 containerd 組件確保了 Docker 鏡像能夠以正確的 OCI Bundle 的格式傳遞給 runc。
其實(shí),在 OCI 規(guī)范以 1.0 版本正式發(fā)布之前,Docker 引擎就已經(jīng)遵循該規(guī)范實(shí)現(xiàn)了部分功能。
如前所述,runc 是 OCI 容器運(yùn)行時(shí)規(guī)范的參考實(shí)現(xiàn)。Docker 公司參與了規(guī)范的制定以及 runc 的開發(fā)。
去粗取精,會發(fā)現(xiàn) runc 實(shí)質(zhì)上是一個(gè)輕量級的、針對 Libcontainer 進(jìn)行了包裝的命令行交互工具(Libcontainer 取代了早期 Docker 架構(gòu)中的 LXC)。
runc 生來只有一個(gè)作用——創(chuàng)建容器,這一點(diǎn)它非常拿手,速度很快!不過它是一個(gè) CLI 包裝器,實(shí)質(zhì)上就是一個(gè)獨(dú)立的容器運(yùn)行時(shí)工具。
因此直接下載它或基于源碼編譯二進(jìn)制文件,即可擁有一個(gè)全功能的 runc。但它只是一個(gè)基礎(chǔ)工具,并不提供類似 Docker 引擎所擁有的豐富功能。
有時(shí)也將 runc 所在的那一層稱為“OCI 層”,如上圖所示。關(guān)于 runc 的發(fā)布信息見 GitHub 中 opencontainers/runc 庫的 release。
在對 Docker daemon 的功能進(jìn)行拆解后,所有的容器執(zhí)行邏輯被重構(gòu)到一個(gè)新的名為 containerd(發(fā)音為 container-dee)的工具中。
它的主要任務(wù)是容器的生命周期管理——start | stop | pause | rm....
containerd 在 Linux 和 Windows 中以 daemon 的方式運(yùn)行,從 1.11 版本之后 Docker 就開始在 Linux 上使用它。
Docker 引擎技術(shù)棧中,containerd 位于 daemon 和 runc 所在的 OCI 層之間。Kubernetes 也可以通過 cri-containerd 使用 containerd。
如前所述,containerd 最初被設(shè)計(jì)為輕量級的小型工具,僅用于容器的生命周期管理。然而,隨著時(shí)間的推移,它被賦予了更多的功能,比如鏡像管理。
其原因之一在于,這樣便于在其他項(xiàng)目中使用它。比如,在 Kubernetes 中,containerd 就是一個(gè)很受歡迎的容器運(yùn)行時(shí)。
然而在 Kubernetes 這樣的項(xiàng)目中,如果 containerd 能夠完成一些諸如 push 和 pull 鏡像這樣的操作就更好了。
因此,如今 containerd 還能夠完成一些除容器生命周期管理之外的操作。不過,所有的額外功能都是模塊化的、可選的,便于自行選擇所需功能。
所以,Kubernetes 這樣的項(xiàng)目在使用 containerd 時(shí),可以僅包含所需的功能。
containerd 是由 Docker 公司開發(fā)的,并捐獻(xiàn)給了云原生計(jì)算基金會(Cloud Native Computing Foundation, CNCF)。2017 年 12 月發(fā)布了 1.0 版本,具體的發(fā)布信息見 GitHub 中的 containerd/ containerd 庫的 releases。
現(xiàn)在我們對 Docker 引擎已經(jīng)有了一個(gè)總體認(rèn)識,也了解了一些歷史,下面介紹一下創(chuàng)建新容器的過程。
常用的啟動(dòng)容器的方法就是使用 Docker 命令行工具。下面的docker container run命令會基于 alpine:latest 鏡像啟動(dòng)一個(gè)新容器。
$ docker container run --name ctr1 -it alpine:latest sh
當(dāng)使用 Docker 命令行工具執(zhí)行如上命令時(shí),Docker 客戶端會將其轉(zhuǎn)換為合適的 API 格式,并發(fā)送到正確的 API 端點(diǎn)。
API 是在 daemon 中實(shí)現(xiàn)的。這套功能豐富、基于版本的 REST API 已經(jīng)成為 Docker 的標(biāo)志,并且被行業(yè)接受成為事實(shí)上的容器 API。
一旦 daemon 接收到創(chuàng)建新容器的命令,它就會向 containerd 發(fā)出調(diào)用。daemon 已經(jīng)不再包含任何創(chuàng)建容器的代碼了!
daemon 使用一種 CRUD 風(fēng)格的 API,通過 gRPC 與 containerd 進(jìn)行通信。
雖然名叫 containerd,但是它并不負(fù)責(zé)創(chuàng)建容器,而是指揮 runc 去做。
containerd 將 Docker 鏡像轉(zhuǎn)換為 OCI bundle,并讓 runc 基于此創(chuàng)建一個(gè)新的容器。
然后,runc 與操作系統(tǒng)內(nèi)核接口進(jìn)行通信,基于所有必要的工具(Namespace、CGroup等)來創(chuàng)建容器。容器進(jìn)程作為 runc 的子進(jìn)程啟動(dòng),啟動(dòng)完畢后,runc 將會退出。
至此,容器啟動(dòng)完畢。整個(gè)過程如下圖所示。
將所有的用于啟動(dòng)、管理容器的邏輯和代碼從 daemon 中移除,意味著容器運(yùn)行時(shí)與 Docker daemon 是解耦的,有時(shí)稱之為“無守護(hù)進(jìn)程的容器(daemonless container)”,如此,對 Docker daemon 的維護(hù)和升級工作不會影響到運(yùn)行中的容器。
在舊模型中,所有容器運(yùn)行時(shí)的邏輯都在 daemon 中實(shí)現(xiàn),啟動(dòng)和停止 daemon 會導(dǎo)致宿主機(jī)上所有運(yùn)行中的容器被殺掉。
這在生產(chǎn)環(huán)境中是一個(gè)大問題——想一想新版 Docker 的發(fā)布頻次吧!每次 daemon 的升級都會殺掉宿主機(jī)上所有的容器,這太糟了!
幸運(yùn)的是,這已經(jīng)不再是個(gè)問題。
shim 是實(shí)現(xiàn)無 daemon 的容器(用于將運(yùn)行中的容器與 daemon 解耦,以便進(jìn)行 daemon 升級等操作)不可或缺的工具。
前面提到,containerd 指揮 runc 來創(chuàng)建新容器。事實(shí)上,每次創(chuàng)建容器時(shí)它都會 fork 一個(gè)新的 runc 實(shí)例。
不過,一旦容器創(chuàng)建完畢,對應(yīng)的 runc 進(jìn)程就會退出。因此,即使運(yùn)行上百個(gè)容器,也無須保持上百個(gè)運(yùn)行中的 runc 實(shí)例。
一旦容器進(jìn)程的父進(jìn)程 runc 退出,相關(guān)聯(lián)的 containerd-shim 進(jìn)程就會成為容器的父進(jìn)程。作為容器的父進(jìn)程,shim 的部分職責(zé)如下。
保持所有 STDIN 和 STDOUT 流是開啟狀態(tài),從而當(dāng) daemon 重啟的時(shí)候,容器不會因?yàn)楣艿?pipe)的關(guān)閉而終止。
將容器的退出狀態(tài)反饋給 daemon。
在 Linux 系統(tǒng)中,前面談到的組件由單獨(dú)的二進(jìn)制來實(shí)現(xiàn),具體包括 dockerd(Docker daemon)、docker-containerd(containerd)、docker-containerd-shim (shim) 和 docker-runc (runc)。
通過在 Docker 宿主機(jī)的 Linux 系統(tǒng)中執(zhí)行 ps 命令可以看到以上組件的進(jìn)程。當(dāng)然,有些進(jìn)程只有在運(yùn)行容器的時(shí)候才可見。
當(dāng)所有的執(zhí)行邏輯和運(yùn)行時(shí)代碼都從 daemon 中剝離出來之后,問題出現(xiàn)了—— daemon 中還剩什么?
顯然,隨著越來越多的功能從 daemon 中拆解出來并被模塊化,這一問題的答案也會發(fā)生變化。
不過,daemon 的主要功能包括鏡像管理、鏡像構(gòu)建、REST API、身份驗(yàn)證、安全、核心網(wǎng)絡(luò)以及編排。