深入淺出Docker 讀書筆記(二)

                                                                             第五章:Docker引擎

Docker引擎是用來運行和管理容器的核心軟件。通常人們會簡單地將其代指爲 Docker 或 Docker 平臺。基於開放容器計劃(OCI)相關標準的要求,Docker 引擎採用了模塊化的設計原則,其組件是可替換的。從多個角度來看,Docker 引擎就像汽車引擎—二者都是模塊化的,並且由許多可交換的部件組成。汽車引擎由許多專用的部件協同工作,從而使汽車可以行駛,例如進氣管、節氣門、氣缸、排氣管等。Docker 引擎由許多專用的工具協同工作,從而可以創建和運行容器,例如 API、執行驅動、運行時、shim 進程等。Docker 引擎由如下主要的組件構成:Docker 客戶端(Docker Client)、Docker 守護進程(Docker daemon)、containerd 以及 runc。它們共同負責容器的創建和運行。總體邏輯如下圖所示。
 

Docker總體邏輯

Docker 首次發佈時,Docker 引擎由兩個核心組件構成:LXC ((LinuXContainers Linux容器)和 Docker daemon。Docker daemon 是單一的二進制文件,包含諸如 Docker 客戶端、Docker API、容器運行時、鏡像構建等。LXC 提供了對諸如命名空間(Namespace)和控制組(CGroup)等基礎工具的操作能力,它們是基於Linux內核的容器虛擬化技術。下圖闡釋了在 Docker 舊版本中,Docker daemon、LXC 和操作系統之間的交互關係。

先前的Docker架構

對 LXC 的依賴自始至終都是個問題,首先,LXC 是基於 Linux 的。這對於一個立志於跨平臺的項目來說是個問題。其次,如此核心的組件依賴於外部工具,這會給項目帶來巨大風險,甚至影響其發展。因此,Docker 公司開發了名爲 Libcontainer 的自研工具,用於替代 LXC。Libcontainer 的目標是成爲與平臺無關的工具,可基於不同內核爲 Docker 上層提供必要的容器交互功能。隨着時間的推移,Docker daemon 的整體性帶來了越來越多的問題。難於變更、運行越來越慢。這並非生態(或Docker公司)所期望的。拆解這個大而全的 Docker daemon 進程,並將其模塊化。這項任務的目標是儘可能拆解出其中的功能特性,並用小而專的工具來實現它。這些小工具可以是可替換的,也可以被第三方拿去用於構建其他工具。這一計劃遵循了在 UNIX 中得以實踐並驗證過的一種軟件哲學:小而專的工具可以組裝爲大型工具。目前 Docker 引擎的架構示意圖如下圖所示,圖中有簡要的描述。

Docker引擎的架構

開放容器計劃(OCI)的影響定義兩個容器相關的規範(或者說標準)。例如Docker daemon 不再包含任何容器運行時的代碼—所有的容器運行代碼在一個單獨的 OCI 兼容層中實現。默認情況下,Docker 使用 runc 來實現這一點。runc 是 OCI 容器運行時標準的參考實現。如上圖中的 runc 容器運行時層。runc 項目的目標之一就是與 OCI 規範保持一致。除此之外,Docker 引擎中的 containerd 組件確保了 Docker 鏡像能夠以正確的 OCI Bundle 的格式傳遞給 runc。關於runc如前所述,runc 是 OCI 容器運行時規範的參考實現。Docker 公司參與了規範的制定以及 runc 的開發。runc 實質上是一個輕量級的、針對 Libcontainer 進行了包裝的命令行交互工具(Libcontainer 取代了早期 Docker 架構中的 LXC)。runc 生來只有一個作用——創建容器,這一點它非常拿手,速度很快!不過它是一個 CLI 包裝器,實質上就是一個獨立的容器運行時工具。因此直接下載它或基於源碼編譯二進制文件,即可擁有一個全功能的 runc。但它只是一個基礎工具,並不提供類似 Docker 引擎所擁有的豐富功能。有時也將 runc 所在的那一層稱爲“OCI 層”,如上圖所示。關於 runc 的發佈信息見 GitHub 中 opencontainers/runc 庫的 release。

      在對 Docker daemon 的功能進行拆解後,所有的容器執行邏輯被重構到一個新的名爲 containerd的工具中。它的主要任務是容器的生命週期管理——start | stop | pause | rm....。containerd 在 Linux 和 Windows 中以 daemon 的方式運行,Docker 引擎技術棧中,containerd 位於 daemon 和 runc 所在的 OCI 層之間。Kubernetes 也可以通過 cri-containerd 使用 containerd ,它最初被設計爲輕量級的小型工具,僅用於容器的生命週期管理。然而隨着時間的推移,它被賦予了更多的功能,比如鏡像管理。

       啓動一個容器:常用的啓動容器的方法就是使用 Docker 命令行工具。下面的docker container run命令會基於 alpine:latest 鏡像啓動一個新容器。     $ docker container run --name ctr1 -it alpine:latest sh      當使用 Docker 命令行工具執行如上命令時,Docker 客戶端會將其轉換爲合適的 API 格式,併發送到正確的 API 端點。API 是在 daemon 中實現的。一旦 daemon 接收到創建新容器的命令,它就會向 containerd 發出調用。daemon 已經不再包含任何創建容器的代碼了!daemon 使用一種 CRUD 風格的 API,通過 gRPC 與 containerd 進行通信。雖然名叫 containerd,但是它並不負責創建容器,而是指揮 runc 去做。containerd 將 Docker 鏡像轉換爲 OCI bundle,並讓 runc 基於此創建一個新的容器。然後,runc 與操作系統內核接口進行通信,基於所有必要的工具(Namespace、CGroup等)來創建容器。容器進程作爲 runc 的子進程啓動,啓動完畢後,runc 將會退出。至此,容器啓動完畢。整個過程如下圖所示。

啓動新容器的過程

該模型的優勢在於將所有的用於啓動、管理容器的邏輯和代碼從 daemon 中移除,意味着容器運行時與 Docker daemon 是解耦的,有時稱之爲“無守護進程的容器(daemonless container)”,如此,對 Docker daemon 的維護和升級工作不會影響到運行中的容器。在舊模型中,所有容器運行時的邏輯都在 daemon 中實現,啓動和停止 daemon 會導致宿主機上所有運行中的容器被殺掉。

     shim 是實現無 daemon 的容器(用於將運行中的容器與 daemon 解耦,以便進行 daemon 升級等操作)不可或缺的工具。
containerd 指揮 runc 來創建新容器。事實上,每次創建容器時它都會 fork 一個新的 runc 實例。一旦容器創建完畢,對應的 runc 進程就會退出。因此即使運行上百個容器,也無須保持上百個運行中的 runc 實例。一旦容器進程的父進程 runc 退出,相關聯的 containerd-shim 進程就會成爲容器的父進程。作爲容器的父進程,shim 的部分職責如下。

  • 保持所有 STDIN 和 STDOUT 流是開啓狀態,從而當 daemon 重啓的時候,容器不會因爲管道(pipe)的關閉而終止。
  • 將容器的退出狀態反饋給 daemon。

daemon 的作用:當所有的執行邏輯和運行時代碼都從 daemon 中剝離出來之後, daemon 中還剩什麼?顯然,隨着越來越多的功能從 daemon 中拆解出來並被模塊化,這一問題的答案也會發生變化。不過,daemon 的主要功能包括鏡像管理、鏡像構建、REST API、身份驗證、安全、核心網絡以及編排。

                                                                             第6章:Docker鏡像

       Docker鏡像可以理解爲 VM裏的模板,VM 模板就像停止運行的 VM,而 Docker 鏡像就像停止運行的容器;而作爲一名研發人員,則可以將鏡像理解爲類(Class)。拉取鏡像下載到本地 Docker 主機,可以使用該鏡像啓動一個或者多個容器。鏡像由多個層組成,每層疊加之後,從外部看來就如一個獨立的對象。鏡像內部是一個精簡的操作系統(OS),同時還包含應用運行所必須的文件和依賴包。因爲容器的設計初衷就是快速和小巧,所以鏡像通常都比較小。在該前提下,鏡像可以理解爲一種構建時(build-time)結構,而容器可以理解爲一種運行時(run-time)結構,如下圖所示。
 

鏡像與容器

      鏡像和容器:上圖從頂層設計層面展示了鏡像和容器間的關係。通常使用docker container run和docker service create命令從某個鏡像啓動一個或多個容器。一旦容器從鏡像啓動後,二者之間就變成了互相依賴的關係,並且在鏡像上啓動的容器全部停止之前,鏡像是無法被刪除的。嘗試刪除鏡像而不停止或銷燬使用它的容器,會導致出錯。容器目的就是運行應用或者服務,這意味着容器的鏡像中必須包含應用/服務運行所必需的操作系統和應用文件。容器追求快速和小巧,這意味着構建鏡像的時候通常需要裁剪掉不必要的部分,保持較小的體積。例如,Docker 鏡像通常不會包含 6 個不同的 Shell 讓讀者選擇——通常 Docker 鏡像中只有一個精簡的Shell,甚至沒有 Shell。鏡像中還不包含內核——容器都是共享所在 Docker 主機的內核。所以有時會說容器僅包含必要的操作系統(通常只有操作系統文件和文件系統對象)。

       鏡像拉取:Docker 主機安裝之後,本地並沒有鏡像。docker image pull 是下載鏡像的命令。鏡像從遠程鏡像倉庫服務的倉庫中下載。默認情況下,鏡像會從 Docker Hub 的倉庫中拉取。docker image pull alpine:latest 命令會從 Docker Hub 的 alpine 倉庫中拉取標籤爲 latest 的鏡像。Linux Docker 主機本地鏡像倉庫通常位於 /var/lib/docker/<storage-driver>,Windows Docker 主機則是 C:\ProgramData\docker\windowsfilter。可以使用以下命令檢查 Docker 主機的本地倉庫中是否包含鏡像。$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE

Windows示例如下:> docker image pull microsoft/powershell:nanoserver

鏡像倉庫:Docker 鏡像存儲在鏡像倉庫服務(Image Registry)當中。Docker 客戶端的鏡像倉庫服務是可配置的,默認使用 Docker Hub。鏡像倉庫服務包含多個鏡像倉庫(Image Repository)。同樣,一個鏡像倉庫中可以包含多個鏡像。所以下圖展示了包含 3 個鏡像倉庫的鏡像倉庫服務,其中每個鏡像倉庫都包含一個或多個鏡像。
 

包含3個鏡像倉庫的鏡像倉庫服務

官方和非官方鏡像倉庫:Docker Hub 也分爲官方倉庫(Official Repository)和非官方倉庫(Unofficial Repository)。官方倉庫中的鏡像是由 Docker 公司審查的。這意味着其中的鏡像會及時更新,由高質量的代碼構成,這些代碼是安全的,有完善的文檔和最佳實踐。非官方倉庫更像江湖俠客,其中的鏡像不一定具備官方倉庫的優點,但這並不意味着所有非官方倉庫都是不好的!非官方倉庫中也有一些很優秀的鏡像。

       鏡像命名和標籤:只需要給出鏡像的名字和標籤,就能在官方倉庫中定位一個鏡像(採用“:”分隔)。從官方倉庫拉取鏡像時,docker image pull 命令的格式如下。docker image pull <repository>:<tag>  如:$ docker image pull mongo:3.3.11  //該命令會從官方Mongo庫拉取標籤爲3.3.11的鏡像。如果沒有在倉庫名稱後指定具體的鏡像標籤,則 Docker 會假設用戶希望拉取標籤爲 latest 的鏡像。標有 latest 標籤的鏡像不保證這是倉庫中最新的鏡像!所以使用 latest 標籤時需要謹慎!鏡像也以根據需求設置多個標籤。這是因爲標籤是存放在鏡像元數據中的任意數字或字符串。

過濾 docker image ls 的輸出內容:Docker 提供 --filter 參數來過濾 docker image ls 命令返回的鏡像列表內容。
下面的示例只會返回懸虛(dangling)鏡像。那些沒有標籤的鏡像被稱爲懸虛鏡像,在列表中展示爲<none>:<none>。$ docker image ls --filter dangling=true   通常出現這種情況,是因爲構建了一個新鏡像,然後爲該鏡像打了一個已經存在的標籤。
當此情況出現,Docker 會構建新的鏡像,然後發現已經有鏡像包含相同的標籤,接着 Docker 會移除舊鏡像上面的標籤,將該標籤標在新的鏡像之上。可以通過 docker image prune 命令移除全部的懸虛鏡像。如果添加了 -a 參數,Docker 會額外移除沒有被使用的鏡像(那些沒有被任何容器使用的鏡像)。
Docker 目前支持如下的過濾器。

  • dangling:可以指定 true 或者 false,僅返回懸虛鏡像(true),或者非懸虛鏡像(false)。
  • before:需要鏡像名稱或者 ID 作爲參數,返回在之前被創建的全部鏡像。
  • since:與 before 類似,不過返回的是指定鏡像之後創建的全部鏡像。
  • label:根據標註(label)的名稱或者值,對鏡像進行過濾。docker image ls命令輸出中不顯示標註內容。

docker search 命令允許通過 CLI 的方式搜索 Docker Hub。可以通過“NAME”字段的內容進行匹配,並且基於返回內容中任意列的值進行過濾。$ docker search nigelpoulton, “NAME”字段是倉庫名稱,包含了 Docker ID,或者非官方倉庫的組織名稱。Docker 鏡像由一些松耦合的只讀鏡像層組成。Docker 負責堆疊這些鏡像層,並且將它們表示爲單個統一的對象。查看鏡像分層的方式可以通過 docker image inspect 命令。所有的 Docker 鏡像都起始於一個基礎鏡像層,當進行修改或增加新的內容時,就會在當前鏡像層之上,創建新的鏡像層。舉一個簡單的例子,假如基於 Ubuntu Linux 16.04 創建一個新的鏡像,這就是新鏡像的第一層;如果在該鏡像中添加python包,就會在基礎鏡像層之上創建第二個鏡像層;如果繼續添加一個安全補丁,就會創建第三個鏡像層。該鏡像當前已經包含 3 個鏡像層。在添加額外的鏡像層的同時,鏡像始終保持是當前所有鏡像的組合,理解這一點非常重要。下圖中舉了一個簡單的例子,每個鏡像層包含 3 個文件,而鏡像包含了來自兩個鏡像層的 6 個文件。
 

添加額外的鏡像層後的鏡像

Docker 通過存儲引擎(新版本採用快照機制)的方式來實現鏡像層堆棧,並保證多鏡像層對外展示爲統一的文件系統。Linux 上可用的存儲引擎有 AUFS、Overlay2、Device Mapper、Btrfs 以及 ZFS。顧名思義,每種存儲引擎都基於 Linux 中對應的文件系統或者塊設備技術,並且每種存儲引擎都有其獨有的性能特點。Docker 在 Windows 上僅支持 windowsfilter 一種存儲引擎,該引擎基於 NTFS 文件系統之上實現了分層和 CoW[1]。多個鏡像之間可以並且確實會共享鏡像層。這樣可以有效節省空間並提升性能。Docker拉取鏡像時它可以識別出要拉取的鏡像中,哪幾層已經在本地存在 就會以Already exists 結尾。Docker 在 Linux 上支持很多存儲引擎(Snapshotter)。每個存儲引擎都有自己的鏡像分層、鏡像層共享以及寫時複製(CoW)技術的具體實現。
不僅可以通過標籤拉取鏡像還可以通過摘要(鏡像散列值)(而不是標籤)來再次拉取該鏡像。鏡像本身就是一個配置對象,其中包含了鏡像層的列表以及一些元數據信息。鏡像層纔是實際數據存儲的地方(比如文件等,鏡像層之間是完全獨立的,並沒有從屬於某個鏡像集合的概念)。鏡像的唯一標識是一個加密 ID,即配置對象本身的散列值。每個鏡像層也由一個加密 ID 區分,其值爲鏡像層本身內容的散列值。這意味着修改鏡像的內容或其中任意的鏡像層,都會導致加密散列值的變化。所以,鏡像和其鏡像層都是不可變的,任何改動都能很輕鬆地被辨別。這就是所謂的內容散列(Content Hash)。每個鏡像層同時會包含一個分發散列值(Distribution Hash)。這是一個壓縮版鏡像的散列值,當從鏡像倉庫服務拉取或者推送鏡像的時候,其中就包含了分發散列值,該散列值會用於校驗拉取的鏡像是否被篡改過。這個內容尋址存儲模型極大地提升了鏡像的安全性,因爲在拉取和推送操作後提供了一種方式來確保鏡像和鏡像層數據是一致的。該模型也解決了隨機生成鏡像和鏡像層 ID 這種方式可能導致的 ID 衝突問題。

在拉取鏡像並運行之前,需要考慮鏡像是否與當前運行環境的架構匹配,這破壞了 Docker 的流暢體驗。多架構鏡像(Multi-architecture Image)的出現解決了這個問題!Docker(鏡像和鏡像倉庫服務)規範目前支持多架構鏡像。這意味着某個鏡像倉庫標籤(repository:tag)下的鏡像可以同時支持 64 位 Linux、PowerPC Linux、64 位 Windows 和 ARM 等多種架構。簡單地說,就是一個鏡像標籤之下可以支持多個平臺和架構。下面通過實操演示該特性。爲了實現這個特性,鏡像倉庫服務 API 支持兩種重要的結構:Manifest 列表(新)和 Manifest。Manifest 列表是指某個鏡像標籤支持的架構列表。其支持的每種架構,都有自己的 Mainfest 定義,其中列舉了該鏡像的構成。

刪除鏡像:可以通過 docker image rm 命令從 Docker 主機刪除該鏡像。其中,rm 是 remove 的縮寫。刪除操作會在當前主機上刪除該鏡像以及相關的鏡像層。這意味着無法通過 docker image ls 命令看到刪除後的鏡像,並且對應的包含鏡像層數據的目錄會被刪除。但是,如果某個鏡像層被多個鏡像共享,那只有當全部依賴該鏡像層的鏡像都被刪除後,該鏡像層纔會被刪除。
如通過鏡像 ID 來刪除鏡像,可能跟讀者機器上鏡像 ID 有所不同。$ docker image rm 02674b9cb179   如果被刪除的鏡像上存在運行狀態的容器,那麼刪除操作不會被允許。再次執行刪除鏡像命令之前,需要停止並刪除該鏡像相關的全部容器。一種刪除某 Docker 主機上全部鏡像的快捷方式是在 docker image rm 命令中傳入當前系統的全部鏡像 ID,可以通過 docker image ls 獲取全部鏡像 ID(使用 -q 參數)。如果是在 Windows 環境中,那麼只有在 PowerShell 終端中執行纔會生效。在 CMD 中執行並不會生效。$ docker image rm $(docker image ls -q) -f

爲了理解具體工作原理,首先下載一組鏡像,然後通過運行 docker image ls -q

$ docker image rm $(docker image ls -q) -f

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章