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