容器化之路:誰偷走了我的構建時間
作為一個開發,每天總少不了要出N個測試版本進行調試,容器化以後每次出版本都需要打成鏡像,老劉發現每次他做一個鏡像都要20分鐘,而小王只要10分鐘,對比來對比去只有這個東西不一樣!
Storage-Dirver到底是何方神聖?為什麽能夠導致構建時間上的差異?現在讓我們來一窺究竟。
在回答這個問題之前我們需要先回答三個問題——什麽是鏡像?什麽是鏡像構建?什麽是storage-driver?
什麽是鏡像?
說到鏡像就繞不開容器,我們先看一張來自官方對鏡像和容器解釋的圖片:
看完以後是不是更疑惑了,我們可以這樣簡單粗暴的去理解,鏡像就是一堆只讀層的堆疊。那只讀層裏到底是什麽呢,另外一個簡單粗暴的解釋:裏邊就是放了一堆被改動的文件。這個解釋在不同的storage-driver下不一定準確但是我們可以先這樣簡單去理解。
那不對呀,執行容器的時候明明是可以去修改刪除容器裏的文件的,都是只讀的話怎麽去修改呢?實際上我們運行容器的時候是在那一堆只讀層的頂上再增加了一個讀寫層,所有的操作都是在這個讀寫層裏進行的,當需要修改一個文件的時候我們會將需要修改的文件從底層拷貝到讀寫層再進行修改。那如果是刪除呢,我們不是沒有辦法刪除底層的文件麽?沒錯,確實沒有辦法刪除,但只需要在上層把這個文件隱藏起來,就可以達到刪除的效果。按照官方說法,這就是Docker的寫時復制策略。
為了加深大家對鏡像層的理解我們來舉個栗子,用下面的Dockerfile構建一個etcd鏡像:
構建完成以後生成了如下的層文件:
每次進入容器的時候都感覺仿佛進入了一臺虛機,裏面包含linux的各個系統目錄。那是不是有一層目錄裏包含了所有的linux系統目錄呢?
bingo答對!在最底層的層目錄的確包含了linux的所有的系統目錄文件。
上述Dockerfile中有這樣一步操作
ADD . /go/src/github.com/coreos/etcd
將外面目錄的文件拷到了鏡像中,那這一層鏡像裏究竟保存了什麽呢?
打開發現裏面就只有
/go/src/github.com/coreos/etcd這個目錄,目錄下存放了拷貝進來的文件。
到這裏是不是有種管中窺豹的感覺,接下來我們再來了解什麽是鏡像構建,這樣基本上能夠窺其全貌了。
什麽是鏡像構建?
通過第一節的內容我們知道了鏡像是由一堆層目錄組成的,每個層目錄裏放著這一層修改的文件,鏡像構建簡單的說就是制作和生成鏡像層的過程,那這一過程是如何實現的呢?以下圖流程為例:
Docker Daemon首先利用基礎鏡像ubuntu:14.04創建了一個容器環境,通過第一節的內容我們知道容器的最上層是一個讀寫層,在這一層我們是可以寫入修改的,Docker Daemon首先執行了RUN apt-update get命令,執行完成以後,通過Docker的commit操作將這個讀寫層的內容保存成一個只讀的鏡像層文件。接下來再在這一層的基礎上繼續執行 ADD run.sh命令,執行完成後繼續commit成一個鏡像層文件,如此反復直到將所有的Dockerfile都命令都被提交後,鏡像也就做好了。
這裏我們就能解釋為什麽etcd的某個層目錄裏只有一個go目錄了,因為構建的過程是逐層提交的,每一層裏只會保存這一層操作所涉及改動的文件。
這樣看來鏡像構建就是一個反復按照Dockerfile啟動容器執行命令並保存成只讀文件的過程,那為什麽速度會不一樣呢?接下來就得說到storage-driver了。
什麽是storage-driver?
再來回顧一下這張圖:
之前我們已經知道了,鏡像是由一個個的層目錄疊加起來的,容器運行時只是在上面再增加一個讀寫層,同時還有寫時復制策略保證在最頂層能夠修改底層的文件內容,那這些原理是怎麽實現的呢?就是靠storage-driver!
簡單介紹三種常用的storage-driver:
- AUFS
AUFS通過聯合掛載的方式將多個層文件堆疊起來,形成一個統一的整體提供統一視圖,當在讀寫層進行讀寫的時,先在本層查找文件是否存在,如果沒有則一層一層的往下找。aufs的操作都是基於文件的,需要修改一個文件時無論大小都會將整個文件從只讀層拷貝到讀寫層,因此如果需要修改的文件過大,會導致容器執行速度變慢,docker官方給出的建議是通過掛載的方式將大文件掛載進來而不是放在鏡像層中。
- OverlayFS
OverlayFS可以認為是AUFS的升級版本,容器運行時鏡像層的文件是通過硬鏈接的方式組成一個下層目錄,而容器層則是工作在上層目錄,上層目錄是可讀寫的,下層目錄是只讀的,由於大量的采用了硬鏈接的方式,導致OverlayFS會可能會出現inode耗盡的情況,後續Overlay2對這一問題進行了優化,且性能上得到了很大的提升,不過Overlay2也有和AUFS有同樣的弊端——對大文件的操作速度比較慢。
- DeviceMapper
DeviceMapper和前兩種Storage-driver在實現上存在很大的差異。首先DeviceMapper的每一層保存的是上一層的快照,其次DeviceMapper對數據的操作不再是基於文件的而是基於數據塊的。
下圖是devicemapper在容器層讀取文件的過程:
首先在容器層的快照中找到該文件指向下層文件的指針。
再從下層0xf33位置指針指向的數據塊中讀取的數據到容器的存儲區
最後將數據返回app。
在寫入數據時還需要根據數據的大小先申請1~N個64K的容器快照,用於保存拷貝的塊數據。
DeviceMapper的塊操作看上去很美,實際上存在很多問題,比如頻繁操作較小文件時需要不停地從資源池中分配數據庫並映射到容器中,這樣效率會變得很低,且DeviceMapper每次鏡像運行時都需要拷貝所有的鏡像層信息到內存中,當啟動多個鏡像時會占用很大的內存空間。
針對不同的storage-driver我們用上述etcd的dockerfile進行了一組構建測試
文件存儲系統 | 單次構建時間 | 並發10次平均構建時間 |
---|---|---|
DevivceMapper | 44s | 269.5s |
AUFS | 8s | 26s |
Overlay2 | 10s | 269.5s |
註:該數據因dockerfile以及操作系統、文件系統、網絡環境的不同測試結果可能會存在較大差異
我們發現在該實驗場景下DevivceMapper在時間上明顯會遜於AUFS和Overlay2,而AUFS和Overlay2基本相當,當然該數據僅能作為一個參考,實際構建還受到具體的Dockerfile內容以及操作系統、文件系統、網絡環境等多方面的影響,那要怎麽樣才能盡量讓構建時間最短提升我們的工作效率呢?
且看下回分解!
容器化之路:誰偷走了我的構建時間