1. 程式人生 > >Docker映象進階:瞭解其背後的技術原理

Docker映象進階:瞭解其背後的技術原理

什麼是 docker 映象

docker 映象是一個只讀的 docker 容器模板,含有啟動 docker 容器所需的檔案系統結構及其內容,因此是啟動一個 docker 容器的基礎。docker 映象的檔案內容以及一些執行 docker 容器的配置檔案組成了 docker 容器的靜態檔案系統執行環境:rootfs。可以這麼理解,docker 映象是 docker 容器的靜態視角,docker 容器是 docker 映象的執行狀態。我們可以通過下圖來理解 docker daemon、docker 映象以及 docker 容器三者的關係(此圖來自網際網路):

從上圖中我們可以看到,當由 ubuntu:14.04 映象啟動容器時,ubuntu:14.04 映象的映象層內容將作為容器的 rootfs;而 ubuntu:14.04 映象的 json 檔案,會由 docker daemon 解析,並提取出其中的容器執行入口 CMD 資訊,以及容器程序的環境變數 ENV 資訊,最終初始化容器程序。當然,容器程序的執行入口來源於映象提供的 rootfs。

rootfs

rootfs 是 docker 容器在啟動時內部程序可見的檔案系統,即 docker 容器的根目錄。rootfs 通常包含一個作業系統執行所需的檔案系統,例如可能包含典型的類 Unix 作業系統中的目錄系統,如 /dev、/proc、/bin、/etc、/lib、/usr、/tmp 及執行 docker 容器所需的配置檔案、工具等。


在傳統的 Linux 作業系統核心啟動時,首先掛載一個只讀的 rootfs,當系統檢測其完整性之後,再將其切換為讀寫模式。而在 docker 架構中,當 docker daemon 為 docker 容器掛載 rootfs 時,沿用了 Linux 核心啟動時的做法,即將 rootfs 設為只讀模式。在掛載完畢之後,利用聯合掛載(union mount)技術在已有的只讀 rootfs 上再掛載一個讀寫層。這樣,可讀寫的層處於 docker 容器檔案系統的最頂層,其下可能聯合掛載了多個只讀的層,只有在 docker 容器執行過程中檔案系統發生變化時,才會把變化的檔案內容寫到可讀寫層,並隱藏只讀層中的舊版本檔案。

Docker 映象的主要特點

為了更好的理解 docker 映象的結構,下面介紹一下 docker 映象設計上的關鍵技術。

分層
docker 映象是採用分層的方式構建的,每個映象都由一系列的 "映象層" 組成。分層結構是 docker 映象如此輕量的重要原因。當需要修改容器映象內的某個檔案時,只對處於最上方的讀寫層進行變動,不覆寫下層已有檔案系統的內容,已有檔案在只讀層中的原始版本仍然存在,但會被讀寫層中的新版本所隱藏。當使用 docker commit 提交這個修改過的容器檔案系統為一個新的映象時,儲存的內容僅為最上層讀寫檔案系統中被更新過的檔案。分層達到了在不的容器同映象之間共享映象層的效果。

寫時複製
docker 映象使用了寫時複製(copy-on-write)的策略,在多個容器之間共享映象,每個容器在啟動的時候並不需要單獨複製一份映象檔案,而是將所有映象層以只讀的方式掛載到一個掛載點,再在上面覆蓋一個可讀寫的容器層。在未更改檔案內容時,所有容器共享同一份資料,只有在 docker 容器執行過程中檔案系統發生變化時,才會把變化的檔案內容寫到可讀寫層,並隱藏只讀層中的老版本檔案。寫時複製配合分層機制減少了映象對磁碟空間的佔用和容器啟動時間。

內容定址
在 docker 1.10 版本後,docker 映象改動較大,其中最重要的特性便是引入了內容定址儲存(content-addressable storage) 的機制,根據檔案的內容來索引映象和映象層。與之前版本對每個映象層隨機生成一個 UUID 不同,新模型對映象層的內容計算校驗和,生成一個內容雜湊值,並以此雜湊值代替之前的 UUID 作為映象層的唯一標識。該機制主要提高了映象的安全性,並在 pull、push、load 和 save 操作後檢測資料的完整性。另外,基於內容雜湊來索引映象層,在一定程度上減少了 ID 的衝突並且增強了映象層的共享。對於來自不同構建的映象層,主要擁有相同的內容雜湊,也能被不同的映象共享。

聯合掛載
通俗地講,聯合掛載技術可以在一個掛載點同時掛載多個檔案系統,將掛載點的原目錄與被掛載內容進行整合,使得最終可見的檔案系統將會包含整合之後的各層的檔案和目錄。實現這種聯合掛載技術的檔案系統通常被稱為聯合檔案系統(union filesystem)。以下圖所示的執行 Ubuntu:14.04 映象後的容器中的 aufs 檔案系統為例:

由於初始掛載時讀寫層為空,所以從使用者的角度看,該容器的檔案系統與底層的 rootfs 沒有差別;然而從核心的角度看,則是顯式區分開來的兩個層次。當需要修改映象內的某個檔案時,只對處於最上方的讀寫層進行了變動,不復寫下層已有檔案系統的內容,已有檔案在只讀層中的原始版本仍然存在,但會被讀寫層中的新版本檔案所隱藏,當 docker commit 這個修改過的容器檔案系統為一個新的映象時,儲存的內容僅為最上層讀寫檔案系統中被更新過的檔案。


聯合掛載是用於將多個映象層的檔案系統掛載到一個掛載點來實現一個統一檔案系統檢視的途徑,是下層儲存驅動(aufs、overlay等) 實現分層合併的方式。所以嚴格來說,聯合掛載並不是 docker 映象的必需技術,比如在使用 device mapper 儲存驅動時,其實是使用了快照技術來達到分層的效果。

Docker 映象的儲存組織方式

綜合考慮映象的層級結構,以及 volume、init-layer、可讀寫層這些概念,一個完整的、在執行的容器的所有檔案系統結構可以用下圖來描述:

從圖中我們不難看到,除了 echo hello 程序所在的 cgroups 和 namespace 環境之外,容器檔案系統其實是一個相對獨立的組織。可讀寫部分(read-write layer 以及 volumes)、init-layer、只讀層(read-only layer) 這 3 部分結構共同組成了一個容器所需的下層檔案系統,它們通過聯合掛載的方式巧妙地表現為一層,使得容器程序對這些層的存在一無所知。

Docker 映象中的關鍵概念

registry
我們知道,每個 docker 容器都要依賴 docker 映象。那麼當我們第一次使用 docker run 命令啟動一個容器時,是從哪裡獲取所需的映象呢?答案是,如果是第一次基於某個映象啟動容器,且宿主機上並不存在所需的映象,那麼 docker 將從 registry 中下載該映象並儲存到宿主機。如果宿主機上存在該映象,則直接使用宿主機上的映象完成容器的啟動。那麼 registry 是什麼呢?


registry 用以儲存 docker 映象,其中還包括映象層次結構和關於映象的元資料。可以將 registry 簡單的想象成類似於 Git 倉庫之類的實體。


使用者可以在自己的資料中心搭建私有的 registry,也可以使用 docker 官方的公用 registry 服務,即 Docker Hub。它是由 Docker 公司維護的一個公共映象庫。Docker Hub 中有兩種型別的倉庫,即使用者倉庫(user repository) 與頂層倉庫(top-level repository)。使用者倉庫由普通的 Docker Hub 使用者建立,頂層倉庫則由 Docker 公司負責維護,提供官方版本映象。理論上,頂層倉庫中的映象經過 Docker 公司驗證,被認為是架構良好且安全的。

repository
repository 由具有某個功能的 docker 映象的所有迭代版本構成的映象組。Registry 由一系列經過命名的 repository 組成,repository 通過命名規範對使用者倉庫和頂層倉庫進行組織。所謂的頂層倉庫,其名稱只包含倉庫名。使用者倉庫的名稱多了 "使用者名稱/" 部分。

比較容易讓人困惑的地方在於,我們經常把 mysql 視為映象的名稱,其實 mysql 是 repository 的名稱。repository 是一個映象的集合,其中包含了多個不同版本的映象,這些映象之間使用標籤進行版本區分,如 mysql:5.6、mysql:5.7 等,它們均屬於 mysql 這個 repository。


簡單來說,registry 是 repository 的集合,repository 是映象的集合。

manifest
manifest(描述檔案)主要存在於 registry 中作為 docker 映象的元資料檔案,在 pull、push、save 和 load 過程中作為映象結構和基礎資訊的描述檔案。在映象被 pull 或者 load 到 docker 宿主機時,manifest 被轉化為本地的映象配置檔案 config。在我們拉取映象時顯示的摘要(Digest):

就是對映象的 manifest 內容計算 sha256sum 得到的。

image 和 layer
docker 內部的 image 概念是用來儲存一組映象相關的元資料資訊,主要包括映象的架構(如 amd64)、映象預設配置資訊、構建映象的容器配置資訊、包含所有映象層資訊的 rootfs。docker 利用 rootfs 中的 diff_id 計算出內容定址的索引(chainID) 來獲取 layer 相關資訊,進而獲取每一個映象層的檔案內容。
layer(映象層) 是 docker 用來管理映象層的一箇中間概念。我們前面提到,映象是由映象層組成的,而單個映象層可能被多個映象共享,所以 docker 將 layer 與 image 的概念分離。docker 映象管理中的 layer 主要存放了映象層的 diff_id、size、cache-id 和 parent 等內容,實際的檔案內容則是由儲存驅動來管理,並可以通過 cache-id 在本地索引到。

Dockerfile
Dockerfile 是通過 docker build 命令構建 docker 映象時用到的配置檔案。它允許使用者使用基本的 DSL 語法來定義 docker 映象,其中的每一條指令描述一個構建映象的步驟。更多關於 Dockerfile 資訊請參考筆者的文章《 Docker 基礎 : Dockerfile》。

總結

本文我們介紹了實現 docker 映象的技術原理,希望可以加深大家對 docker 映象的理解。

參考:
《docker 容器與容器雲》