Docker 鏡像之進階篇

筆者在《Docker 基礎 : 鏡像》一文中介紹了 docker 鏡像的基本用法,本文咱們來介紹 docker 鏡像背後的技術原理。html

什麼是 docker 鏡像

docker 鏡像是一個只讀的 docker 容器模板,含有啓動 docker 容器所需的文件系統結構及其內容,所以是啓動一個 docker 容器的基礎。docker 鏡像的文件內容以及一些運行 docker 容器的配置文件組成了 docker 容器的靜態文件系統運行環境:rootfs。能夠這麼理解,docker 鏡像是 docker 容器的靜態視角,docker 容器是 docker 鏡像的運行狀態。咱們能夠經過下圖來理解 docker daemon、docker 鏡像以及 docker 容器三者的關係(此圖來自互聯網):mysql

從上圖中咱們能夠看到,當由 ubuntu:14.04 鏡像啓動容器時,ubuntu:14.04 鏡像的鏡像層內容將做爲容器的 rootfs;而 ubuntu:14.04 鏡像的 json 文件,會由 docker daemon 解析,並提取出其中的容器執行入口 CMD 信息,以及容器進程的環境變量 ENV 信息,最終初始化容器進程。固然,容器進程的執行入口來源於鏡像提供的 rootfs。sql

rootfsdocker

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 容器運行過程當中文件系統發生變化時,纔會把變化的文件內容寫到可讀寫層,並隱藏只讀層中的舊版本文件。json

Docker 鏡像的主要特色

爲了更好的理解 docker 鏡像的結構,下面介紹一下 docker 鏡像設計上的關鍵技術。ubuntu

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

寫時複製
docker 鏡像使用了寫時複製(copy-on-write)的策略,在多個容器之間共享鏡像,每一個容器在啓動的時候並不須要單獨複製一份鏡像文件,而是將全部鏡像層以只讀的方式掛載到一個掛載點,再在上面覆蓋一個可讀寫的容器層。在未更改文件內容時,全部容器共享同一份數據,只有在 docker 容器運行過程當中文件系統發生變化時,纔會把變化的文件內容寫到可讀寫層,並隱藏只讀層中的老版本文件。寫時複製配合分層機制減小了鏡像對磁盤空間的佔用和容器啓動時間。架構

內容尋址
在 docker 1.10 版本後,docker 鏡像改動較大,其中最重要的特性即是引入了內容尋址存儲(content-addressable storage) 的機制,根據文件的內容來索引鏡像和鏡像層。與以前版本對每一個鏡像層隨機生成一個 UUID 不一樣,新模型對鏡像層的內容計算校驗和,生成一個內容哈希值,並以此哈希值代替以前的 UUID 做爲鏡像層的惟一標識。該機制主要提升了鏡像的安全性,並在 pull、push、load 和 save 操做後檢測數據的完整性。另外,基於內容哈希來索引鏡像層,在必定程度上減小了 ID 的衝突而且加強了鏡像層的共享。對於來自不一樣構建的鏡像層,主要擁有相同的內容哈希,也能被不一樣的鏡像共享。app

聯合掛載
通俗地講,聯合掛載技術能夠在一個掛載點同時掛載多個文件系統,將掛載點的原目錄與被掛載內容進行整合,使得最終可見的文件系統將會包含整合以後的各層的文件和目錄。實現這種聯合掛載技術的文件系統一般被稱爲聯合文件系統(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.六、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 容器與容器雲》

相關文章
相關標籤/搜索