docker在實際使用中,讓運維人員詬病的,除了安全問題外,大概就是數據的問題了
?
很多人在初用docker的時候,很多時候都忘記或不知道docker中需要保留的數據需要掛載到宿主機文件夾到容器內部對應目錄(當然除了掛載宿主機目錄,還有其他解決方案,我們后面會有文章介紹)
?
當容器因為某些原因掛掉、無法重新啟動的時候,他們就認為數據丟失了,找不回了,這也是很多人對docker的一個認識誤區,網上沒有一篇文章說docker數據的問題,今天詳細解釋下,docker數據在哪里
?
首先說一下這邊以docker-ce 19.03.4版本舉例,GraphDriver是overlay2
?
這里說明下,overlay技術是一種虛擬網絡技術,這里說的overlay是overlayfs,是一種聯合文件系統
?
我們通過docker info簡單查看Storage Driver
也可以通過docker inspect 查看鏡像或容器的詳細信息
在GraphDriver部分可以看到使用的是什么文件系統,之前舊內核的系統中的docker的GraphDriver是使用DeviceMapper,由于overlay2性能比devicemapper好,而且新版本docker-ce默認采用,所以這里只研究overlay2
?
從上面的截圖可以看到GraphDriver的data部分有四部分,分別是LowerDir、MergerDir、UpperDir、WorkDir,解釋這幾個概念之前,先來回顧下docker的鏡像的分層原理
?
docker鏡像是一種輕量可執行的獨立軟件包,?用來打包軟件運行的環境和基于運行環境開發的軟件?,它包含運行某個軟件所需要的所有內容,包括代碼、運行時的庫、環境變量和配置文件
?
docker的鏡像實際上由一層一層的文件系統組成,這種層級的文件系統稱為聯合文件系統(unionFS)
?
UnionFS(聯合文件系統):union文件系統(unionFS)是一種分層、輕量級并且高性能的文件系統,它支持?對文件系統的修改作為一次提交來一層層的疊加?,同時可以將不同的目錄掛載到同一個虛擬文件系統下。union文件系統是docker鏡像的基礎。鏡像可以通過分層來進行繼承。基于基礎鏡像(沒有父鏡像),可以制作各種的應用鏡像
?
這種分層最大的好處就是資源共享
?
很多時候,你pull鏡像的時候,如果是來源于相同的base鏡像,你可以看到,pull的時候,底下的層是已經pulled,而且base64加密id是一樣的,這就是因為base鏡像資源已經下載,可以共享使用,不需要重復下載
?
在容器中,伴隨聯合文件系統的一個技術就是寫時復制(CoW),該技術是linux內核的一個技術,為了避免不必要的進程間復制操作,在父進程fork子進程后,父子進程共享同一副本,當子進程需要調用exec寫入的時候,數據才會被復制,從而父子進程各自有自己的副本
?
結合容器技術來看,當處于鏡像態的時候,所有的層級都是只讀的,但是當docker run啟動為容器態的時候,在基礎鏡像上添加了一層可讀寫層,這時肯定會在最上層,可讀寫層進行文件寫入,就需要將要寫入的文件從它存在的層復制到可讀寫層,然后進行讀寫,并隱藏只讀層的舊文件,這個就是利用了寫時復制技術
?
回顧了這些基礎之后,接著看下overlay2的基礎概念及原理
overlayfs在linux主機上只有兩層,一個目錄在下層,用來保存鏡像(docker),另外一個目錄在上層,用來存儲容器信息。在overlayfs中,底層的目錄叫做lowerdir,頂層的目錄稱之為upperdir,對外提供統一的文件系統為merged。當需要修改一個文件時,使用CoW將文件從只讀的Lower復制到可寫的Upper進行修改,這個復制出來的臨時目錄就是Workdir,結果也保存在Upper層
?
以上就是GraphDriver的data部分的四部分,可以從前面的圖中看到lowerdir,包含多個層,因為它就是rootfs,容器鏡像,也就是我們pull鏡像的時候看到的層級
?
overlay2存儲在/var/lib/docker/overlay2目錄中,如果你只有少數鏡像,比較好查看,多個鏡像的時候,就只能通過docker inspect的方式查找對應鏡像的層級id,然后通過這個id去overlay2目錄去找對應的overlayfs目錄
?
在該目錄下有個“l”目錄,這個目錄是存放所有overlayfs目錄的短名稱的,通過軟連接的方式與overlyafs目錄下的所有目錄鏈接,這個是為了在mount掛載的時候避免參數太長,達到頁面大小限制
?
接著我們分別看下LowerDir、MergerDir、UpperDir、WorkDir
?
我這里通過個redis容器來說明,首先是lowerdir,lowerdir因為有多個,你可以一個一個進去看一下,就能夠看明白它每個層級存儲的東西,如下:
"LowerDir": "/var/lib/docker/overlay2/3d56c8ea55a05f092442c3cdf01c49556a71b8f089a5772413ef566ec68bca31-init/diff
:/var/lib/docker/overlay2/7e6a11d051174f5a71ce038268e6fa8a310d046032ed435b10ed7846f9eb2d92/diff
:/var/lib/docker/overlay2/bbcd520d1d0d62323bcac4a66328164b99fa1704974ceffc412d348c6f7b55e9/diff
/var/lib/docker/overlay2/dee456318494848b5ea4182ddb8f67f25969b121580afa9ff8aa73cf9c0d256e/diff
:/var/lib/docker/overlay2/a45bb32d7cd7d2386d12826e4d8e524b410616d06cf1ec29f2eab03ba3eb80b2/diff
:/var/lib/docker/overlay2/085f1022334a3727171e3d038ed59f69cb0c6199b93a84afeb1e0b1b8674abf1/diff"
?
從上面可以看出來,diff就是存儲該層rootfs的目錄,而每個層級里面的link文件里面存儲的就是l文件夾下的短名稱的超鏈接,除了最底層,上層都有一個lower文件,該文件里面就存儲了它上層的短名稱
而work目錄,用來聯合掛載指定的工作目錄
?
接著看MergedDir、UpperDir、WorkDir其實是指向一個層級id,只是目錄不同,是不是覺得這個id這么熟悉,其實在LowerDir的最上層的id就是這個,只不過它最后面有個init,這個init稍后介紹,先來看下這三個目錄
mergedir對外提供統一的視圖,這里可以看到整合了所有lowerdir層級的文件
因為是新啟動的容器,upperdir目錄沒有內容,workdir目錄因為寫時復制很快,所以通常也無法看到,后面進入容器寫入文件進行會在upperdir目錄看到內容
關于init層
init層是以一個uuid+-init結尾表示,夾在只讀層和讀寫層之間,作用是專門存放/etc/hosts、/etc/resolv.conf等信息,需要這一層的原因是當容器啟動時候,這些本該屬于image層的文件或目錄,比如hostname,用戶需要修改,但是image層又不允許修改,所以啟動時候通過單獨掛載一層init層,通過修改init層中的文件達到修改這些文件目的。而這些修改往往只在當前容器生效,而在docker commit提交為鏡像時候,并不會將init層提交。該層文件存放的目錄為/var/lib/docker/overlay2/<init_id>/diff
?
從上面這部分可以看到,所有容器或者鏡像的層級目錄都存在overlay2目錄下,那么一個容器或者鏡像是怎么把這些整合起來的?答案是元數據關聯,元數據分為image元數據和layer元數據
?
鏡像元數據存儲在了/var/lib/docker/image/<storage_driver>/imagedb/content/sha256/目錄下,名稱是以鏡像ID命名的文件,鏡像ID可通過docker images查看,這些文件以json的形式保存了該鏡像的rootfs信息、鏡像創建時間、構建歷史信息、所用容器、包括啟動的Entrypoint和CMD等等,比如剛才的redis鏡像
打開該文件,是一個json格式文件,但是沒有vim默認沒有格式化,通過:%!python -m json.tool工具轉換成json格式查看
其中rootfs部分
可以看到對應前面的6層overlayfs,其排列也是有順序的,從上到下依次表示鏡像層的最低層到最頂層
diff_id如何關聯進行層?具體說來,docker 利用 rootfs 中的每個diff_id 和歷史信息計算出與之對應的內容尋址的索引(chainID) ,而chaiID則關聯了layer層,進而關聯到每一個鏡像層的鏡像文件
?
layer元數據中layer對應鏡像層的概念,在 docker 1.10 版本以前,鏡像通過一個 graph 結構管理,每一個鏡像層都擁有元數據,記錄了該層的構建信息以及父鏡像層 ID,而最上面的鏡像層會多記錄一些信息作為整個鏡像的元數據。graph 則根據鏡像 ID(即最上層的鏡像層 ID) 和每個鏡像層記錄的父鏡像層 ID 維護了一個樹狀的鏡像層結構。
?
在 docker 1.10 版本后,鏡像元數據管理巨大的改變之一就是簡化了鏡像層的元數據,鏡像層只包含一個具體的鏡像層文件包。用戶在 docker 宿主機上下載了某個鏡像層之后,docker 會在宿主機上基于鏡像層文件包和 image 元數據構建本地的 layer 元數據,包括 diff、parent、size 等。而當 docker 將在宿主機上產生的新的鏡像層上傳到 registry 時,與新鏡像層相關的宿主機上的元數據也不會與鏡像層一塊打包上傳。
?
Docker 中定義了 Layer 和 RWLayer 兩種接口,分別用來定義只讀層和可讀寫層的一些操作,又定義了 roLayer 和 mountedLayer,分別實現了上述兩種接口。其中,roLayer 用于描述不可改變的鏡像層,mountedLayer 用于描述可讀寫的容器層。具體來說,roLayer 存儲的內容主要有索引該鏡像層的 chainID、該鏡像層的校驗碼 diffID、父鏡像層 parent、storage_driver 存儲當前鏡像層文件的 cacheID、該鏡像層的 size 等內容。這些元數據被保存在 /var/lib/docker/image/<storage_driver>/layerdb/sha256/<chainID>/ 文件夾下
每個chainID目錄下會存在三個文件cache-id、diff、zize:
cache-id文件:
docker隨機生成的uuid,內容是保存鏡像層的目錄索引,也就是/var/lib/docker/overlay2/中的目錄,這就是為什么通過chainID能找到對應的layer目錄
如圖對應的overlay目錄為/var/lib/docker/overlay2/e701317468246c6188f1bff4f9b9c159648d86108bb02e0ef5f224fd49efd1f0
diff文件:
保存了鏡像元數據中的diff_id(與元數據中的diff_ids中的uuid對應)
size文件:
保存了鏡像層的大小
在 layer 的所有屬性中,diffID 采用 SHA256 算法,基于鏡像層文件包的內容計算得到。而 chainID 是基于內容存儲的索引,它是根據當前層與所有祖先鏡像層 diffID 計算出來的,具體算如下:
-
如果該鏡像層是最底層(沒有父鏡像層),該層的 diffID 便是 chainID。
-
該鏡像層的 chainID 計算公式為 chainID(n)=SHA256(chain(n-1) diffID(n)),也就是根據父鏡像層的 chainID 加上一個空格和當前層的 diffID,再計算 SHA256 校驗碼。?
?
綜合上述一個完整的容器層如下圖:
回到開頭,啟動后的容器數據存在哪里?
?
可以肯定的是在可讀寫層,結合overlayfs原理看,就是在upperdir,也就是可讀寫層中的diff目錄,比如我們進入容器,在home目錄下寫入個測試文件,然后查看diff目錄
在mergedir目錄下同樣也有該文件
?
從實際中看,并不是所有數據都在這個目錄,當啟動容器的時候通過掛載本地目錄的形式映射容器內部目錄的時候,數據不再存儲在overlayfs,而是直接存儲在本地映射的目錄
?
另外一種情況是,當使用dockerfile指定workdir的情況下,啟動容器會自動掛載一個volume目錄到workdir目錄
那么這個時候,存在workdir目錄下的數據會存在自動映射的Source目錄下
?
總結如下:
只要不刪除容器,數據完全可以找回