土法搞docker系列之自制docker的graph driver vdisk

寫在最前

偶然整理,翻出來14年剛開始學docker的時候的好多資料。當時docker剛剛進入國內,還有不少的問題。當時咱們的思考方式很簡單,docker確實是個好的工具,雖然還不成熟。可是不能由於短期內造橋不行,就不過河了。咱們的方式很簡單,先造個小船划過去。因爲各類條件的侷限,因此不少方法真的是因陋就簡,土法上馬,一切就是爲了抓緊落地。時代更迭、版本變遷,這其中的不少技術方案自己可能已經沒法爲現有的方案提供有力的幫助了。可是解決問題的思路和原理可能還能爲你們提供一點參考吧。這於我本身,也是一個整理回顧。因此我計劃寫成一個小的系列文章,這個系列直接取名爲土法搞docker。git

當時遇到的第一個問題,就是docker的底層graph driver,在centos 6下的devicemapper不穩定,有很大的機率會形成內核崩潰。可是若是不解決這個問題,是絕對沒法將docker上到生產環境中的。以我貧瘠的內核知識和存儲知識,徹底無力解決。那怎麼辦,那就用土辦法,本身寫一個graph driver。之因此叫自制而不叫自研,由於真的沒有多少能夠稱之爲研究的東西,徹底是拼湊而成。自制的這個driver自己沒有多少技術含量,可是須要深刻了解docker的運行原理和底層的存儲方式,而後尋找一種恰當的方式來解決它。github

graph driver原理

graph driver的原理和接口從1.3到如今的最新版本,基本沒有什麼變化。這也有賴於docker當時優秀的設計。首先說graph driver是幹什麼的。咱們都知道docker的鏡像/容器是由多層組成。graph driver其實就是負責了層文件的管理工做。golang

這裏是driver接口的一些方法:docker

// ProtoDriver defines the basic capabilities of a driver.
// This interface exists solely to be a minimum set of methods
// for client code which choose not to implement the entire Driver
// interface and use the NaiveDiffDriver wrapper constructor.
//
// Use of ProtoDriver directly by client code is not recommended.
type ProtoDriver interface {
    // String returns a string representation of this driver.
    String() string
    // Create creates a new, empty, filesystem layer with the
    // specified id and parent. Parent may be "".
    Create(id, parent string) error
    // Remove attempts to remove the filesystem layer with this id.
    Remove(id string) error
    // Get returns the mountpoint for the layered filesystem referred
    // to by this id. You can optionally specify a mountLabel or "".
    // Returns the absolute path to the mounted layered filesystem.
    Get(id, mountLabel string) (dir string, err error)
    // Put releases the system resources for the specified id,
    // e.g, unmounting layered filesystem.
    Put(id string)
    // Exists returns whether a filesystem layer with the specified
    // ID exists on this driver.
    Exists(id string) bool
    // Status returns a set of key-value pairs which give low
    // level diagnostic status about this driver.
    Status() [][2]string
    // Cleanup performs necessary tasks to release resources
    // held by the driver, e.g., unmounting all layered filesystems
    // known to this driver.
    Cleanup() error
}

爲了方便,咱們舉一個例子。假設某個鏡像由兩層組成,layer1(lower)和layer2(upper)。layer1中有一個文件A,layer2中有一個文件B。那麼對單一的layer2來講,其實只有一個文件也就是B。可是經過聯合文件系統將layer1和layer2聯合起來時,獲得layer1+2,將layer1+2掛載起來,那麼得到的掛載點文件夾下應該包含了A和B兩個文件(本文中將這種掛載點稱爲聯合掛載點)。centos

graph

這裏結合這個例子分別對這些中比較重要的方法進行一下介紹:app

  • Create: 建立一層,好比建立layer2
  • Remove: 移除某一層,好比移除layer2
  • Get: 將id及其下層的全部層經過聯合文件系統聯合起來的layer1+2,將layer1+2掛載起來,返回掛載點
  • Put: 將id及其下層的全部層經過聯合文件系統聯合起來的layer1+2的掛載點umount掉
  • Exists: 判斷id層是否存在
  • Cleanup: 將全部的掛載起來的所有卸載掉

其實docker對於層文件的操做,都是經過這些接口組合而成的。好比docker建立容器時,最終須要用Create爲容器建立一個新的可讀寫的層。而docker運行容器時,須要經過Get接口獲取容器及其鏡像全部層聯合起來的文件從而造成容器的rootfs。工具

在分析這些接口時,咱們其實能夠發現一個問題,其實接口中沒有獲取單層,好比只獲取layer2的接口。好比docker save鏡像時,由於要導出每一層的單獨的文件,這又是如何實現的呢?其基本原理其實算是Get(layer1)以及Get(layer2),而後將兩層的掛載的文件夾進行diff,從而獲得只歸屬於layer2層的文件。oop

func (gdw *NaiveDiffDriver) Diff(id, parent string) (arch archive.Archive, err error) {
    driver := gdw.ProtoDriver
    //獲取id的聯合掛載點
    layerFs, err := driver.Get(id, "")
    ...
    //獲取parent的聯合掛載點
    parentFs, err := driver.Get(parent, "")
    ...
    //遍歷兩個掛載點內的全部文件並進行比較,獲得兩者的差別文件
    //則差別文件就是隻屬於id層的文件列表
    changes, err := archive.ChangesDirs(layerFs, parentFs)
    ...

這裏咱們能夠特別想下,接口沒有獲取單層,也就是獲取layer2這種層的接口,那麼其實就意味着docker其實並不真的須要一個聯合文件系統。這也就是咱們可以自制vdisk的基礎。學習

那麼你們可能會有個小疑問,既然不必定真的須要聯合文件系統,那麼使用或者不使用聯合文件系統有什麼差異呢?差異並不在Get接口上,而是在Create接口上。使用聯合文件系統時,建立一個新的單層能夠很是快速,由於新的層的內容爲空。而不使用聯合文件系統呢,則須要將全部父層的文件所有拷貝到新的層中,以便在Get接口調用時能夠快速掛載。這樣兩者的建立效率就一目瞭然了。this

docker自身也支持一個默認的非聯合文件系統的graph driver,也就是vfs。
vfs這個驅動簡單明瞭。我當年就是從這裏開始graph driver的理解和學習的。

vdisk原理

咱們的實際需求實際上是要在centos下用一個非聯合文件系統的方式來取代devicemapper,實現一個穩定可靠的底層存儲。那麼如何實現,其實有幾種路線選擇。vfs足夠簡單穩定,可是沒法限制用戶對於磁盤的使用量。使用不一樣的lvm盤來存儲每層,因爲須要預分配足夠的磁盤空間,又會致使磁盤空間的浪費。最終,咱們選擇了一個折中的方案。就是使用稀疏文件來存儲每一層,而後經過loop設備掛載,來表達聯合文件系統的掛載效果。

那麼同上一個例子,對於layer1的所在層,咱們其實能夠建立稀疏文件file1,並在其中存儲文件A。而對於layer2的所在層如何處理呢?由於接口中沒有獲取單層文件的接口,咱們所以能夠建立file2,並在其中存儲文件A和B,也就是layer1+2,來實現layer1和layer2的聯合。而對於只導出layer2時,只須要將file1和file2的文件進行diff就能夠處理了(同上文所說)。

明白了這個原理後,其實代碼就好寫了。這也是我當時剛學golang後寫的第一個docker功能。代碼原理上我參考了vfs的實現,也參考了dm驅動的deviceset進行loop設備的管理。其實徹底是東拼西湊來的,這裏就不獻醜了,回頭我傳到github (https://github.com/xuxinkun) 上去,有興趣的再來圍觀吧。

vdisk的弊端

這個驅動由於使用的是稀疏文件和loop設備,所以我命名爲loopfile,後來被更名爲vdisk。這個驅動原是想應急使用。可是由於足夠簡單,因此足夠穩定。在線上幾乎是零故障。雖而後來修復了devicemapper的bug,可是在JDOS 1.0的集羣上仍然大規模使用的是這個。固然這其中的一個重要緣由實際上是由於1.0(基於openstack,採用nova+docker方式管理)仍是將容器當作虛擬機來使用,實際建立完容器,仍然須要用戶經過部署平臺來部署腳本。所以對於容器建立時間不是那麼敏感。同時因爲鏡像預分發,因此建立時間並非太大的問題。

可是若是鏡像層數過多,由於每層的文件中要包含所有父層的文件,存在很大的冗餘空間佔用。爲了解決Dockerfile或者屢次commit致使的鏡像多層問題,我還爲docker增長了compress功能,用以將多層壓縮爲一層。這個的實現方式我將在後續文章中講述。

後來,進入到JDOS 2.0時代,這種方式就徹底沒法應付快速啓動容器的需求了。dm的問題也由團隊後來的內核專家進行了解決。今後咱們就跨入了dm的時代。固然這些就是後話了。

相關文章
相關標籤/搜索