Docker鏡像與容器存儲結構分析
Docker是一個開源的應用容器引擎,主要利用linux內核namespace實現沙盒隔離,用cgroup實現資源限制。
Docker 支持三種不同的鏡像層次存儲的drivers: aufs、devicemapper、btrfs ;
Aufs:
AUFS (AnotherUnionFS) 是一種 Union FS, 簡單來說就是支持將不同目錄掛載到同一個虛擬文件系統下(unite several directories into a single virtual filesystem)的文件系統。 Aufs driver是docker 最早支持的driver,但是aufs只是linux內核的一個補丁集而且不太可以會被合并加入到linux內核中。但是由于aufs是唯一一個 storage driver可以實現容器間共享可執行及可共享的運行庫, 所以當你跑成千上百個擁有相同程序代碼或者運行庫時時候,aufs是個相當不錯的選擇。
Device Mapper:
Device mapper 是 Linux 2.6 內核中提供的一種從邏輯設備到物理設備的映射框架機制,在該機制下,用戶可以很方便的根據自己的需要制定實現存儲資源的管理策略(詳見:http://www.ibm.com/developerworks/cn/linux/l-devmapper/index.html) 。
Device mapper driver 會創建一個100G的簡單文件包含你的鏡像和容器。每一個容器被限制在10G大小的卷內。(如果想要調整,參考:http://jpetazzo.github.io/2014/01/29/docker-device-mapper-resize/ 。中文譯文: http://zhumeng8337797.blog.163.com/blog/static/100768914201452405120107/ )
你可以在啟動docker daemon時用參數-s 指定driver: docker -d -s devicemapper ;
Btrfs:
Btufs driver 在docker build 可以很高效。但是跟 devicemapper 一樣不支持設備間共享存儲(文檔里是does not share executable memory between devices)。
下面筆者就已有的條件去分析下docker的鏡像與容器的存儲結構。
環境:
opensuse 13.10 + Docker version 1.2.0, build fa7b24f
Ubuntu 14.10 + Docker version 1.0.1, build 990021a
在沒有aufs支持的linux發行版本上(CentOS,opensuse等)安裝docker可能就使用了devicemapper driver。
查看你的linux發行版有沒有aufs支持:lsmod | grep aufs
筆者opensuse 13.10里是沒有加載這個模塊的:
而虛擬機里的ubuntu 14.10 是加載了這個模塊的:
而我們列出/var/lib/docker 這個目錄的內容也可以看出你那個docker是使用了哪個storage driver:
opensuse 13.10 上的/var/lib/docker
這里應該看出是使用了device mapper這個driver ;
然后再來看看虛擬機ubuntu 14.10上/var/lib/docker 目錄:
這里也可以看出筆者ubuntu里docker 是使用了aufs 這個driver : 下文就這兩個不同的driver作對比。
請注意分析的是哪一個。
那么鏡像文件是本地存放在哪里呢?
筆者在opensuse和ubuntu里把docker徹底重新安裝了一遍刪除了所有鏡像,并只Pull下來一個ubuntu:14.10的鏡像,這樣分析起來會比較簡單明了: 現在兩個系統都只有一個ubuntu:14.10的鏡像:
opensuse:
Ubuntu :
好了。首先現在我們來看看/var/lib/docker里都是什么文件。
1、首先用Python 的json.tool工具查看下repositories-* 里的內容。
opensuse:
里面的json數據記錄的正是本地上存放的鏡像的名稱及其64位長度的ID.這個ID可以有其12位的簡短模式。 Ubuntu上也是一樣的:
而且我們可以發現這兩個ID是一樣。這時我們其實可以猜想到:這個ID是全局性的,就是說你這個鏡像在鏡像倉庫上的ID也是這個。被其它機器上ID也是這個。這樣的好處無疑是方便管理鏡像。
#p#
2、/var/lib/docker/graph 目錄里的內容:
opensuse:
Ubuntu:
Graph目錄里有7個長ID命名的目錄,其中第二個長ID是我們所pull下來的ubuntu14.10鏡像的對應的長ID..那么其它6個是怎么來的呢?
這里我們用docker images -tree列出鏡像樹形結構:
可以看到最下層的鏡像是我們的ubuntu14.10。那么上面對應的是6個layer。就是說在這個樹中第n+1個層是基于第n個層上改動的。而第個層在graph目錄里都對應著一個長ID目錄。
我們來看看虛擬機里ubuntu14.10 里的docker images -tree:
大小數量一致。但是到了***一個層的大小不一樣(這里原因可能會是系統問題,也可能是docker版本問題。具體原因需要另外考察)
再分析一下各個層的大小,***個為0B, 第二個層就應該為198.9MB,第三個層大小為0.2MB(199.1-198.9)…如此類推下去。
上層的image依賴下層的image(注:這里的邏輯上層是上圖樹形結構的下層),因此docker中把下層的image稱作父image,沒有父image的image稱作base image ;
例如我要用這里的ubuntu:14.10為模板啟動一個容器時,docker會加載樹形結構中的最下層( 2185fd5…),然后加載其父層(f180ea…),這樣一直加載到***層(511136…)才算加載這個rootfs。那么一個層在哪里保存它的父 層信息呢?在下面長ID目錄里的json文件其實也可以看到這個信息。
graph長ID目錄內容:(對于ubuntu里是一樣的,這里以opensuse為例)
我們進入長ID目錄里看看里面的內容:
opensuse :
我們進入***一個層長ID目錄里。里面有一個json文件及一個名為layersize的文件。 用cat查看layersize里的內容,里面記錄的數字是指這個層的大小。這里(綠色前頭)是0。而我們從上面的目錄樹可以算出***一個層確實是0。如 果還不相信。我們再算算倒數第二個層的大小(opensuse里的樹形圖里短id為f180ea115597的層)應該為37.8M?,F在進入對應長ID 目錄:
可以看到是是37816084(B),約37.8M,與我們計算的剛剛吻合。
而另一個文件json又是什么呢?用python工具看看:(內容有點多,沒有截完)
可以看到json這個文件保存的是這個鏡像的元數據。
拉到底部可以看到有個parent:的值:
這個就是保存了其父層長ID的值。對照樹形結構看f180ea115597 的父層是不是0f154c52e965 。
但是注意在graph這個目錄里并沒有找到我們想找到的鏡像內容存放地。只是一些鏡像相關的信息數據。
鏡像里的內容存放在哪里
opensuse :
在opensuse下的/var/lib/docker/devicemapper/devicemapper/這個目錄下找到兩個文件,并列出其大小。
其中一個data的文件大小為100G(非真實占用)。真實占用的情況如下:
100G的只占用了590M。
上面我們講到:Device mapper driver 會創建一個100G的簡單文件包含你的鏡像和容器。每一個容器被限制在10G大小的卷內。那么看來這個100G的簡單文件正是這個名為data 的文件,那么鏡像和容器下是存放在這里的。
好了。這時我在opensuse上再pull下一個ubuntu:12.10 鏡像看看這個文件大小有什么變化: 這次一下子截了三個命令的信息:
Pull下來的ubuntu是172.1M。樹形結構可以看到各個層的關系。而data的大小變成了787M. 沒pull ubuntu:12.10之前是590M.增加了197M,跟pull下來的172.1M有點差距。這里可認為是存儲了額外的某些信息。
那么容器是不是也存放在這里呢?
我們用ubuntu14.10啟動一個模板看看情況如何:
這次我也是一下子截了幾個命令:
可以看到了一個基于ubuntu:14.10鏡像的容器在運行中,簡短ID是a9b35d72fcd4,
第二個命令du列出了data的大小為789M,增加了2M。
第三個命令列出了container目錄內出現一個長ID的目錄,ID就是運行的容器的ID。但是里面的文件應該都是些配置文件。并沒有我們想要的內容目錄。
這樣的話我們進一步做測試:在運行的容器內使用dd if=/dev/zero of=test.txt bs=1M count=8000 創建一個8G大小的文件后:
這里data變成了8.6G,增長了接近8G,這樣也證實了容器里的內容是保存在data這個簡單文件內的。
這樣的話證實了devicemapper driver是把鏡像和容器的文件都存儲在data這個文件內。
#p#
Ubuntu 的aufs driver 又如何呢:
Ubuntu上由于是aufs driver 所以/var/lib/docker 目錄下有aufs目錄而不是devicemapper 目錄:
這里的aufs 目錄有三個目錄,diff 、layers 、mnt 三個目錄。
這里layers目錄是保存了layers層次信息,并不是layers里面的內容。
而diff 目錄時有數個長ID目錄:
列出這幾個目錄的大小可以看出基本與上面樹形結構的所能計算的大小相對應(相關部分可能是由于壓縮或者其它原因造成,這里純屬猜測)。
那我們進入f180ea115597這個ID對應的目錄看看里面是什么:
里面是一些文件夾,但是只有幾個,并不像我們平時常規linux發行版里的那么齊全。
這里的話其實我們可以想到了因為一個層是基于另一個層之上的。Aufs文件系統可以做到增量修改,所以這里的幾個文件夾是基于上一個層做的修改內容增量地保存在這里,因為上一個層對于這個層來說不可寫:
在這里我需要先引用一張網上的圖片:
這里我們可以看到一個我們想象中的運行中的container是包含了若干個readonly的image層,然后最上面的writable層才是我們可寫的層。***個readonly的層會加載其父層。直到最下面的base image層。
我們所做的改動會被保存在最上面的那個writable層里。當我們用commit 把容器固化成鏡像時那個層就會變成我們上面看到的“目錄不齊全的”長ID目錄。
為了證實這一點,我們在運行一個基于ubuntu:14.10鏡像的容器:
可以看到運行的容器簡短ID為7b3c13323d8c 。
這時再列出diff目錄的內容:
多了兩個長ID目錄,正是我們運行的容器的ID,列出內容:
然后我們在運行的容器中創建一個/test 目錄,并在里面用dd命令創建一個8G的test.txt文件:完成這些后再列出這兩個目錄內容:
可以看到其中一個目錄(沒有init后綴)變成了7.5G,而另一個目錄還是24K。
在長ID目錄里還多了一個test文件夾,正是我們在容器里創建的,這樣的話里面無疑問就是test.txt文件了。容器通過這種方法在writable層里記錄了修改過的內容(增量記錄) (這里有個小問題筆者也還不清楚:怎么記錄刪除了東西呢?這個問題以后再考察)
從上面我們可以知道容器的writable 層是保存在以容器ID為名的長ID目錄里的,而ID+init后綴目錄是保存容器的初始信息的。
好了,現在我們進行***一個實驗:把容器固化成鏡像。
(這里要做個小小調整。把上面8G的文件刪除了再建一個3G大小的文件test_3G.txt代替)
Commit 后把容器固化成了test_image的鏡像。得到那個鏡像的長ID。
現在看看變化:
那個窗口目錄還在,原因是我們還沒用rm 命令刪除那個容器。而多出來的鏡像目錄正是我們固化所得到的,其大小與上面容器writable層大小一致為3GB。現在看看里面是什么內容:
里面有一個test目錄,目錄下對應我們創建的3GB大小的test_3G.txt文件。
這就是我們改動過的內容保存了在這個目錄內。
現在我們用rm命令刪除容器看看結果:
容器被刪除了,其對應的長目錄ID也被刪除了。而那個固化的得到的鏡像( c7560af30 )被保存了下來。
通過上面的小實驗基本可以看清docker 在devicemapping 和 aufs這兩個driver 的存儲結構,但是這些目錄是怎樣靈活地在運行容器時被加載到一起就需要讀者去了解更深層的關于aufs及devicemapping相關的知識。