摘要:近日, DaoCloud 軟件工程師孫宏亮在 CSDN Container 微信群為大家帶來了 Docker 鏡像原理的深度分享,本次分享的重點是 Docker 鏡像,分享的內容主要包含兩個部分:1)Docker 鏡像的基本知識;2)Dockerfile,Docker 鏡像與 Docker 容器的關系。
嘉賓介紹:孫宏亮,碩士,浙江大學畢業,現為 DaoCloud 軟件工程師,出版有《Docker 源碼分析》,目前主要負責企業級容器雲平台的研發工作。數年來一直從事雲計算、PaaS 領域的研究與實踐,是國內較早一批接觸 Docker 的先行者,同時也是 Docker 技術的推廣者。
第一部分:Docker 鏡像的基本知識
1.1 什么是 Docker 鏡像
從整體的角度來講,一個完整的 Docker 鏡像可以支撐一個 Docker 容器的運行,在 Docker 容器運行過程中主要提供文件系統視角。例如一個 ubuntu:14.04 的鏡像,提供了一個基本的 ubuntu:14.04 的發行版,當然此鏡像是不包含操作系統 Linux 內核的。
說到此,可能就需要注意一下,linux 內核和 ubuntu:14.04 Docker 鏡像的區別了。傳統虛擬機安裝 ubuntu:14.04 會包含兩部分,第一,某一個 Linux 內核的發行版本,比如 Linux 3.8 版本的內核;第二,第一個特定的 Ubuntu 發行版,這部分內容不包含 Linux 內核,但是包含 Linux 之外的軟件管理方式,軟件驅動,如 apt-get 軟件管理包等。
理解以上內容之后,就可以理解,為什么在一個 Linux 內核版本為 3.8 的 ubuntu:14.04 基礎上,可以把 Linux 內核版本升級到 3.18,而 ubuntu 的版本依然是 14.04。最主要的就是:Linux 內核版本與 ubuntu 操作系統發行版之間的區別。
Linux 內核+ubuntu 操作系統發行版,組成一台工作的機器讓用戶體驗。那么靈活替換 ubuntu 操作系統發行版,那是不是也可以實現呢。那么 Docker 很方便的利用了這一點,技術手段就是 Docker 鏡像。
Docker 的架構中,Docker 鏡像就是類似於 “ubuntu 操作系統發行版”,可以在任何滿足要求的 Linux 內核之上運行。簡單一點有 “Debian 操作系統發行版” Docker 鏡像、“Ubuntu 操作系統發行版” Docker鏡像;如果在 Debian 鏡像中安裝 MySQL 5.6,那我們可以將其命名為 Mysql:5.6 鏡像;如果在 Debian 鏡像中安裝有 Golang 1.3,那我們可以將其命名為 golang:1.3 鏡像;以此類推,大家可以根據自己安裝的軟件,得到任何自己想要的鏡像。
那么鏡像最后的作用是什么呢?很好理解,回到 Linux 內核上來運行,通過鏡像來運行時我們常常將提供的環境稱為容器。
以上內容是從宏觀的角度看看 Docker 鏡像是什么,我們再從微觀的角度進一步深入 Docker 鏡像。剛才提到了“Debian 鏡像中安裝 MySQL 5.6,就成了 mysql:5.6 鏡像”,其實在此時 Docker 鏡像的層級概念就體現出來了。底層一個 Debian 操作系統鏡像,上面疊加一個 mysql 層,就完成了一個 mysql 鏡像的構建。層級概念就不難理解,此時我們一般 debian 操作系統鏡像稱為 mysql 鏡像層的父鏡像。
層級管理的方式大大便捷了 Docker 鏡像的分發與存儲。說到分發,大家自然會聯想到 Docker 鏡像的靈活性,傳輸的便捷性,以及高超的移植性。Docker Hub,作為全球的鏡像倉庫,作為 Docker 生態中的數據倉庫,將全世界的 Docker 數據匯聚在一起,是 Docker 生態的命脈。
Docker 有兩方面的技術非常重要,第一是 Linux 容器方面的技術,第二是 Docker 鏡像的技術。從技術本身來講,兩者的可復制性很強,不存在絕對的技術難點,然而 Docker Hub 由於存在大量的數據的原因,導致 Docker Hub 的可復制性幾乎不存在,這需要一個生態的營造。
1.2 Docker 鏡像的內容
大致介紹了 Docker 鏡像是什么,我們來看看 Docker 鏡像中有哪些內容?
介紹之前,我先分享一下,我個人在接觸 Docker 的兩年時間中,對 Docker 鏡像內容認識的變化。
第一階段:初步接觸 Docker。相信很多愛好者都會和我一樣,有這樣一個認識: Docker 鏡像代表一個容器的文件系統內容。
第二階段:初步接觸聯合文件系統。聯合文件系統的概念,讓我意識到鏡像層級管理的技術,每一層鏡像都是容器文件系統內容的一部分。
第三階段:研究鏡像與容器的關系:容器是一個動態的環境,每一層鏡像中的文件屬於靜態內容,然而 Dockerfile 中的 ENV、VOLUME、CMD 等內容最終都需要落實到容器的運行環境中,而這些內容均不可能直接坐落到每一層鏡像所包含的文件系統內容中,那此時每一個 Docker 鏡像還會包含 json 文件記錄與容器之間的關系。
因此,Docker 鏡像的內容主要包含兩個部分:第一,鏡像層文件內容;第二,鏡像 json 文件。
1.3 Docker 鏡像存儲位置
既然是說鏡像存儲的位置,那么應該包含:鏡像層文件和鏡像 json 文件。如一個 ubuntu:14.04 鏡像,包含 4 個鏡像層,在 aufs 存儲驅動的情況下,在磁盤上的情況可以如以下圖所示:
1.3.1 查看鏡像層組成:
我們可以通過命令 docker history ubuntu:14.04 查看 ubuntu:14.04,結果如下:
1.3 Docker 鏡像存儲位置
既然是說鏡像存儲的位置,那么應該包含:鏡像層文件和鏡像 json 文件。如一個 ubuntu:14.04 鏡像,包含 4 個鏡像層,在 aufs 存儲驅動的情況下,在磁盤上的情況可以如以下圖所示:
1.3.1 查看鏡像層組成:
我們可以通過命令 docker history ubuntu:14.04 查看 ubuntu:14.04,結果如下:
1.3.2 鏡像層文件內容存儲
Docker 鏡像層的內容一般在 Docker 根目錄的 aufs 路徑下,為 /var/lib/docker/aufs/diff/,具體情況如下:
圖中顯示了鏡像 ubuntu:14.04 的 4 個鏡像層內容,以及每個鏡像層內的一級目錄情況。需要額外注意的是:鏡像層 d2a0ecffe6fa 中沒有任何內容,也就是所謂的空鏡像。
1.3.3 鏡像 json 文件存儲
對於每一個鏡像層,Docker 都會保存一份相應的 json 文件,json 文件的存儲路徑為 /var/lib/docker/graph,ubuntu:14.04 所有鏡像層的 json 文件存儲路徑展示如下:
除了 json 文件,大家還看到每一個鏡像層還包含一個 layersize 文件,該文件主要記錄鏡像層內部文件內容的總大小。既然談到了鏡像 json 文件,為了給下文鋪墊,以下貼出 ubuntu:14.04 中空鏡像層 d2a0ecffe6fa 的 json 文件:
Docker 鏡像存儲,就和大家一起先看到這。同時介紹 Docker 鏡像的基本知識也告一段落。以下我們進入此次分享的第二部分。
第二部分 Dockerfile、Docker 鏡像和 Docker 容器的關系
Dockerfile 是軟件的原材料,Docker 鏡像是軟件的交付品,而 Docker 容器則可以認為是軟件的運行態。從應用軟件的角度來看,Dockerfile、Docker 鏡像與 Docker 容器分別代表軟件的三個不同階段,Dockerfile 面向開發,Docker 鏡像成為交付標准,Docker 容器則涉及部署與運維,三者缺一不可,合力充當 Docker 體系的基石。
簡單來講,Dockerfile構建出Docker鏡像,通過Docker鏡像運行Docker容器。
我們可以從Docker容器的角度,來反推三者的關系。首先可以來看下圖:
我們假設這個容器的鏡像通過以下 Dockerfile 構建而得:
FROM ubuntu:14.04
ADD run.sh /
VOLUME /data
CMD ["./run.sh"]
2.1 Dockerfile 與 Docker 鏡像
首先,我們結合上圖來看看 Dockerfile 與 Docker 鏡像之間的關系。
FROM ubuntu:14.04:設置基礎鏡像,此時會使用基礎鏡像 ubuntu:14.04 的所有鏡像層,為簡單起見,圖中將其作為一個整體展示。
ADD run.sh /:將 Dockerfile 所在目錄的文件 run.sh 加至鏡像的根目錄,此時新一層的鏡像只有一項內容,即根目錄下的 run.sh。
VOLUME /data:設定鏡像的 VOLUME,此 VOLUME 在容器內部的路徑為 /data。需要注意的是,此時並未在新一層的鏡像中添加任何文件,即構建出的磁層鏡像中文件為空,但更新了鏡像的 json 文件,以便通過此鏡像啟動容器時獲取這方面的信息。
CMD [“./run.sh”]:設置鏡像的默認執行入口,此命令同樣不會在新建鏡像中添加任何文件,僅僅在上一層鏡像 json 文件的基礎上更新新建鏡像的 json 文件。
因此,通過以上分析,以上的Dockerfile可以構建出一個新的鏡像,包含4個鏡像層,每一條命令會和一個鏡像層對應,鏡像之間會存在父子關系。
圖中很清楚的表明了這些關系。
2.2 Docker 鏡像與 Docker 容器的關系
Docker 鏡像是 Docker 容器運行的基礎,沒有 Docker 鏡像,就不可能有 Docker 容器,這也是 Docker 的設計原則之一。
可以理解的是:Docker 鏡像畢竟是鏡像,屬於靜態的內容;而 Docker 容器就不一樣了,容器屬於動態的內容。動態的內容,大家很容易聯想到進程,內存,CPU 等之類的東西。的確,Docker 容器作為動態的內容,都會包含這些。
為了便於理解,大家可以把 Docker 容器,理解為一個或多個運行進程,而這些運行進程將占有相應的內存,相應的 CPU 計算資源,相應的虛擬網絡設備以及相應的文件系統資源。而 Docker 容器所占用的文件系統資源,則通過 Docker 鏡像的鏡像層文件來提供。
那么作為靜態的鏡像,如何才有能力轉化為一個動態的 Docker 容器呢?此時,我們可以想象:第一,轉化的依據是什么;第二,由誰來執行這個轉化操作。
其實,轉化的依據是每個鏡像的 json 文件,Docker 可以通過解析 Docker 鏡像的 json 的文件,獲知應該在這個鏡像之上運行什樣的進程,應該為進程配置怎么樣的環境變量,此時也就實現了靜態向動態的轉變。
誰來執行這個轉化工作?答案是 Docker 守護進程。也許大家早就理解這樣一句話: Docker 容器實質上就是一個或者多個進程,而容器的父進程就是 Docker 守護進程。這樣的,轉化工作的執行就不難理解了:Docker 守護進程手握 Docker 鏡像的 json 文件,為容器配置相應的環境,並真正運行 Docker 鏡像所指定的進程,完成 Docker 容器的真正創建。
Docker 容器運行起來之后,Docker 鏡像 json 文件就失去作用了。此時 Docker 鏡像的絕大部分作用就是:為 Docker 容器提供一個文件系統的視角,供容器內部的進程訪問文件資源。
再次回到上圖,我們再來看看容器和鏡像之間的一些特殊關系。
首先,之前已經提及 Docker 鏡像是分層管理的,管理 Docker 容器的時候,Docker 鏡像仍然是分層管理的。由於此時動態的容器中已經存在進程,進程就會對文件系統視角內的文件進行讀寫操作,因此,就會涉及一個問題:容器是否會篡改 Docker 鏡像的內容?
答案自然是不會的。統一來講,正如上圖,所有的Docker鏡像層對於容器來說,都是只讀的,容器對於文件的寫操作絕對不會作用在鏡像中。
既然如此,實現的原理就很重要,究其根本:Docker 守護進程會在 Docker 鏡像的最上層之上,再添加一個可讀寫層,容器所有的寫操作都會作用到這一層中。而如果 Docker 容器需要寫底層 Docker 鏡像中的文件,那么此時就會涉及一個叫 Copy-on-Write 的機制,即 aufs 等聯合文件系統保證:首先將此文件從 Docker 鏡像層中拷貝至最上層的可讀寫層,然后容器進程再對讀寫層中的副本進行寫操縱。對於容器進程來講,它只能看到最上層的文件。
那最后我們再來說說:Docker 容器的文件系統視角中,到底是不是存在一些內容,不是存儲於 Docker 鏡像中的?
這次的答案依舊是肯定的。
再次重申一點,Docker 鏡像中存儲的都是一些靜態文件。這些文件原則上應該和容器具體信息以及主機信息完全解藕。那么 Docker 容器中不存在 Docker 鏡像中的內容主要有以下幾點:
- /proc 以及 /sys 等虛擬文件系統的內容
- 容器的 hosts 文件,hostname 文件以及 resolv.conf 文件,這些事具體環境的信息,原則上的確不應該被打入鏡像。
- 容器的 Volume 路徑,這部分的視角來源於從宿主機上掛載到容器內部的路徑
- 部分的設備文件
更多閱讀:
Docker鏡像結構原理:https://blog.51cto.com/liuleis/2070461
Docker 創建鏡像、修改、上傳鏡像:https://www.cnblogs.com/lsgxeva/p/8746644.html