這篇文章我們簡單聊聊 docker 的映象分層。
思考題
在第四篇文章容器與虛擬機器中,我們說到 docker 其實就是把可執行程式及其所有的依賴打包成映象檔案,然後呼叫宿主機核心介面執行容器。可以想象,像 ubuntu等基礎映象,體積必然不小。那麼,思考以下幾個問題:
- 我們基於同一個映象(ubuntu 18.4)啟動了兩個容器,會佔用兩倍磁碟空間嗎?
- 我們在容器內修改或者新建了某個檔案,要修改原映象嗎?
- 我們基於某映象(ubuntu 18.04)新建一個映象(myubuntu),需要將原映象檔案全部拷貝到新映象中嗎?
首先,讓我們嘗試思考下,如果我們去做,該如何高效的解決這些問題?
- 問題 1,只要將同一個映象檔案載入到記憶體不同位置就行了,沒必要在磁碟上儲存多份,可以節省大量儲存空間。
- 問題 2,我們可以參考 Linux 核心管理記憶體的 *Copy-On-Write 策略,也即讀時大家共用一份檔案,如果需要修改再複製一份進行修改,而大部分檔案是其實不會修改的,這樣可以最大限度節省空間,提升效能。
- 問題 3,我們可以將映象檔案分為多個獨立的層,然後新映象檔案只要引用基礎映象檔案就可以了,這樣可以節省大量空間。至於修改基礎映象檔案的情況,參考問題 2 。
如果你能想到以上思路,那麼恭喜你,因為 Docker 就是這麼做的,你已經具備為寫docker 寫一套檔案系統的實力了(哈哈哈哈,不要飄,還有大量技術細節需要思考)。
Docker的映象分層
接下來,我們來看看 Docker的映象分層機制。
Docker映象是分層構建的,Dockerfile 中每條指令都會新建一層。例如以下 Dockerfile:
FROM ubuntu:18.04
COPY . /app
RUN make /app
CMD python /app/app.py
以上四條指令會建立四層,分別對應基礎映象、複製檔案、編譯檔案以及入口檔案,每層只記錄本層所做的更改,而這些層都是隻讀層。當你啟動一個容器,Docker 會在最頂部新增讀寫層,你在容器內做的所有更改,如寫日誌、修改、刪除檔案等,都儲存到了讀寫層內,一般稱該層為容器層,如下圖所示:
事實上,容器(container)和映象(image)的最主要區別就是容器加上了頂層的讀寫層。所有對容器的修改都發生在此層,映象並不會被修改,也即前面說的 COW(copy-on-write)技術。容器需要讀取某個檔案時,直接從底部只讀層去讀即可,而如果需要修改某檔案,則將該檔案拷貝到頂部讀寫層進行修改,只讀層保持不變。
每個容器都有自己的讀寫層,因此多個容器可以使用同一個映象,另外容器被刪除時,其對應的讀寫層也會被刪除(如果你希望多個容器共享或者持久化資料,可以使用 Docker volume)。
最後,執行命令 docker ps -s,可以看到最後有兩列 size 和 virtual size。其中 size就是容器讀寫層佔用的磁碟空間,而 virtual size 就是讀寫層加上對應只讀層所佔用的磁碟空間。如果兩個容器是從同一個映象建立,那麼只讀層就是 100%共享,即使不是從同一映象建立,其映象仍然可能共享部分只讀層(如一個映象是基於另一個建立)。因此,docker 實際佔用的磁碟空間遠遠小於 virtual size 的總和。
以上就是 Docker 映象分層的主要內容,至於這些層的互動、管理就需要儲存驅動程式,也即聯合檔案系統(UnionFS)。Docker 可使用多種驅動,如目前已經合併入 Linux 核心、官方推薦的overlay, 曾在 Ubuntu、Debian等發行版中得到廣泛使用的 AUFS,以及devicemapper、zfs等等,需要根據 Docker以及宿主機系統的版本,進行合適的選擇。接下來以 AUFS 為例,簡單介紹 UnionFS 的使用。
AUFS
AUFS 是一種 UnionFS,所謂 UnionFS 就是把不同物理位置的目錄合併到同一個目錄中。例如把 CD和硬碟 mount 到一起,就可以對 CD上的檔案進行修改,再比如上面所講 docker 的使用場景。
AUFS是Another UnionFS的首字母縮略字,2006年由岡島順治郎開發,是之前的 UnionFS 的完全重寫,其穩定性和效能上確實好很多,但從第 2 版開始它代表advanced multi-layered unification filesystem。
這裡插播一個悲情的小故事,AUFS被 Linus 拒絕合併到主線 Linux,其程式碼被批評為“稠密,不可讀,無註釋”。然後作者不斷改進程式碼,不斷提交,不斷被 Linus 拒掉,最終放棄。而2014年,OverlayFS被合併到 Linux 核心 3.18版本,岡島順治郎再無希望。
言歸正傳,下面我們簡單試驗下 AUFS 的使用。(Ubuntu 18.04環境)
首先,建立兩個資料夾代表只讀層以及讀寫層。
mkdir readLayer
echo "original read file content" >> readLayer/readFile
mkdir writeLayer
echo "original write file content" >> writeLayer/writeFile
然後,將兩個資料夾進行聯合掛載。命令中預設dirs後第一個資料夾為讀寫許可權,之後的資料夾為只讀許可權。
mkdir unionFs
mount -t aufs -o dirs=./writeLayer:./readLayer none ./unionFs
接下來,我們可以檢視下 unionFs資料夾下的內容,發現已經有了readFile、writeFile兩個檔案,說明掛載成功。
然後,我們再來測試下對檔案的修改操作,我們分別修改讀寫層以及只讀層的檔案。
cd unionFs
echo "edit writeable file" >> writeFile
echo "edit readonly file" >> readFile
根據我們之前的分析,針對讀寫層writeLayer的 writeFile 的修改,應該是直接在原始檔上生效;而針對只讀層readLayer的 readFile 的修改,應該是原始檔保持不變,將檔案拷貝到 writeLayer,然後再修改,我們來檢查下。
首先,檢視下只讀層readLayer,檔案內容並沒有改變。
然後檢視下讀寫層,可以看到新增的檔案 readFile,且檔案內容為修改後內容。
可以看到,我們成功模擬了只讀層、讀寫層的聯合使用,Docker的映象分層也是此原理,只是實現更加複雜。
參考資料
以上即是 docker 映象分層技術的簡單介紹,通過這項技術,節省了大量的儲存空間,也提升了容器的下載、構建、啟動速度,推動了 Docker 技術的廣泛使用。
如果你想更進一步瞭解相關細節,可以參考以下文章:
閒言
前後拖了 7 個月,終於把六篇文章更完了,把自己學習時的困惑、自己的理解都儘量簡潔的寫出來,希望能幫大家更快的理解學習。在整理的過程中,我對 Docker 技術的認知也更加清晰完整,收穫很大。
每寫一篇文章都很痛苦,文采、技術都不好,糾結怎麼能以更清晰的方式,說更多的技術點。每寫完一篇也很充實,又前近了一步。看著專欄的讀者一個個增加,很開心,也很有成就感,接下來還會更新,不保證頻繁,但每一篇都會盡量寫好。千里之行,始於足下,加油!