Docker源碼分析(十一):鏡像存儲

jopen 9年前發布 | 17K 次閱讀 Docker

1.前言

Docker Hub匯總眾多Docker用戶的鏡像,極大得發揮Docker鏡像開放的思想。Docker用戶在全球任意一個角度,都可以與Docker Hub交互,分享自己構建的鏡像至Docker Hub,當然也完全可以下載另一半球Docker開發者上傳至Docker Hub的Docker鏡像。

無論是上傳,還是下載Docker鏡像,鏡像必然會以某種形式存儲在Docker Daemon所在的宿主機文件系統中。Docker鏡像在宿主機的存儲,關鍵點在于:在本地文件系統中以如何組織形式,被Docker Daemon有效的統一化管理。這種管理,可以使得Docker Daemon創建Docker容器服務時,方便獲取鏡像并完成union mount操作,為容器準備初始化的文件系統。

本文主要從Docker 1.2.0源碼的角度,分析Docker Daemon下載鏡像過程中存儲Docker鏡像的環節。分析內容的安排有以下5部分:

(1) 概述Docker鏡像存儲的執行入口,并簡要介紹存儲流程的四個步驟;

(2) 驗證鏡像ID的有效性;

(3) 創建鏡像存儲路徑;

(4) 存儲鏡像內容;

(5) 在graph中注冊鏡像ID。

2.鏡像注冊

Docker Daemon執行鏡像下載任務時,從Docker Registry處下載指定鏡像之后,仍需要將鏡像合理地存儲于宿主機的文件系統中。更為具體而言,存儲工作分為兩個部分:

(1) 存儲鏡像內容;

(2) 在graph中注冊鏡像信息。

說到鏡像內容,需要強調的是,每一層layer的Docker Image內容都可以認為有兩個部分組成:鏡像中每一層layer中存儲的文件系統內容,這部分內容一般可以認為是未來Docker容器的靜態文件內容;另一部分內容指的是容器的json文件,json文件代表的信息除了容器的基本屬性信息之外,還包括未來容器運行時的動態信息,包括ENV等信息。

存儲鏡像內容,意味著Docker Daemon所在宿主機上已經存在鏡像的所有內容,除此之外,Docker Daemon仍需要對所存儲的鏡像進行統計備案,以便用戶在后續的鏡像管理與使用過程中,可以有據可循。為此,Docker Daemon設計了graph,使用graph來接管這部分的工作。graph負責記錄有哪些鏡像已經被正確存儲,供Docker Daemon調用。

Docker Daemon執行CmdPull任務的pullImage階段時,實現Docker鏡像存儲與記錄的源碼位于./docker/graph/pull.go#L283-L285,如下:

err = s.graph.Register(imgJSON,utils.ProgressReader(layer,
 imgSize, out, sf, false, utils.TruncateID(id), “Downloading”),img)

以上源碼的實現,實際調用了函數Register,Register函數的定義位于./docker/graph/graph.go#L162-L218:

func (graph *Graph) Register(jsonData []byte, layerData 
archive.ArchiveReader, img *image.Image) (err error)

分析以上Register函數定義,可以得出以下內容:

(1) 函數名稱為Register;

(2) 函數調用者類型為Graph;

(3) 函數傳入的參數有3個,第一個為jsonData,類型為數組,第二個為layerData,類型為archive.ArchiveReader,第三個為img,類型為*image.Image;

(4) 函數返回對象為err,類型為error。

Register函數的運行流程如圖11-1所示:

 Docker源碼分析(十一):鏡像存儲

圖11-1 Register函數執行流程圖

3.驗證鏡像ID

Docker鏡像注冊的第一個步驟是驗證Docker鏡像的ID。此步驟主要為確保鏡像ID命名的合法性。功能而言,這部分內容提高了Docker鏡像存儲環節的魯棒性。驗證鏡像ID由三個環節組成。

(1) 驗證鏡像ID的合法性;

(2) 驗證鏡像是否已存在;

(3) 初始化鏡像目錄。

驗證鏡像ID的合法性使用包utils中的ValidateID函數完成,實現源碼位于./docker/graph/graph.go#L171-L173,如下:

if err := utils.ValidateID(img.ID); err != nil {
    return err
}

ValidateID函數的實現過程中,Docker Dameon檢驗了鏡像ID是否為空,以及鏡像ID中是否存在字符‘:’,以上兩種情況只要成立其中之一,Docker Daemon即認為鏡像ID不合法,不予執行后續內容。

鏡像ID的合法性驗證完畢之后,Docker Daemon接著驗證鏡像是否已經存在于graph。若該鏡像已經存在于graph,則Docker Daemon返回相應錯誤,不予執行后續內容。代碼實現如下:

    if graph.Exists(img.ID) {
        return fmt.Errorf("Image %s already exists", img.ID)
    }

驗證工作完成之后,Docker Daemon為鏡像準備存儲路徑。該部分源碼實現位于./docker/graph/graph.go#L182-L196,如下:

if err := os.RemoveAll(graph.ImageRoot(img.ID)); err != nil && !os.IsNotExist(err) {
        return err
    }

    // If the driver has this ID but the graph doesn't, remove it from the driver to start fresh.
    // (the graph is the source of truth).
    // Ignore errors, since we don't know if the driver correctly returns ErrNotExist.
    // (FIXME: make that mandatory for drivers).
    graph.driver.Remove(img.ID)

    tmp, err := graph.Mktemp("")
    defer os.RemoveAll(tmp)
    if err != nil {
        return fmt.Errorf("Mktemp failed: %s", err)
    }

Docker Daemon為鏡像初始化存儲路徑,實則首先刪除屬于新鏡像的存儲路徑,即如果該鏡像路徑已經在文件系統中存在的話,立即刪除該路徑,確保鏡像存儲時不會出現路徑沖突問題;接著還刪除graph.driver中的指定內容,即如果該鏡像在graph.driver中存在的話,unmount該鏡像在宿主機上的目錄,并將該目錄完全刪除。以AUFS這種類型的graphdriver為例,鏡像內容被存放在/var/lib/docker/aufs/diff目錄下,而鏡像會被mount至目錄/var/lib/docker/aufs/mnt下的指定位置。

至此,驗證Docker鏡像ID的工作已經完成,并且Docker Daemon已經完成對鏡像存儲路徑的初始化,使得后續Docker鏡像存儲時存儲路徑不會沖突,graph.driver對該鏡像的mount也不會沖突。

4.創建鏡像路徑

創建鏡像路徑,是鏡像存儲流程中的一個必備環節,這一環節直接讓Docker使用者了解以下概念:鏡像以何種形式存在于本地文件系統的何處。創建鏡像路徑完畢之后,Docker Daemon首先將鏡像的所有祖先鏡像通過aufs文件系統mount至mnt下的指定點,最終直接返回鏡像所在rootfs的路徑,以便后續直接在該路徑下解壓Docker鏡像的具體內容(只包含layer內容)。

4.1創建mnt、diff和layers

創建鏡像路徑的源碼實現位于./docker/graph/graph.go#L198-L206, 如下:

// Create root filesystem in the driver
if err := graph.driver.Create(img.ID, img.Parent); err != nil {
        return fmt.Errorf("Driver %s failed to create image rootfs %s: %s", graph.driver, img.ID, err)
}
// Mount the root filesystem so we can apply the diff/layer
rootfs, err := graph.driver.Get(img.ID, "")
if err != nil {
        return fmt.Errorf("Driver %s failed to get image rootfs %s: %s", graph.driver, img.ID, err)
}

以上源碼中Create函數在創建鏡像路徑時起到舉足輕重的作用。那我們首先分析graph.driver.Create(img.ID, img.Parent)的具體實現。由于在Docker Daemon啟動時,注冊了具體的graphdriver,故graph.driver實際的值為具體注冊的driver。方便起見,本章內容全部以aufs類型為例,即在graph.driver為aufs的情況下,闡述Docker鏡像的存儲。在ubuntu 14.04系統上,Docker Daemon的根目錄一般為/var/lib/docker,而aufs類型driver的鏡像存儲路徑一般為/var/lib/docker/aufs。

AUFS這種聯合文件系統的實現,在union多個鏡像時起到至關重要的作用。首先來關注,Docker Daemon如何為鏡像創建鏡像路徑,以便支持通過aufs來union鏡像。Aufs模式下,graph.driver.Create(img.ID, img.Parent)的具體源碼實現位于./docker/daemon/graphdriver/aufs/aufs.go#L161-L190,如下:

// Three folders are created for each id
// mnt, layers, and diff
func (a *Driver) Create(id, parent string) error {
    if err := a.createDirsFor(id); err != nil {
        return err
    }
    // Write the layers metadata
    f, err := os.Create(path.Join(a.rootPath(), "layers", id))
    if err != nil {
        return err
    }
    defer f.Close()

    if parent != "" {
        ids, err := getParentIds(a.rootPath(), parent)
        if err != nil {
            return err
        }

        if _, err := fmt.Fprintln(f, parent); err != nil {
            return err
        }
        for _, i := range ids {
            if _, err := fmt.Fprintln(f, i); err != nil {
                return err
            }
        }
    }
    return nil
}

在Create函數的實現過程中,createDirsFor函數在Docker Daemon根目錄下的aufs目錄/var/lib/docker/aufs中,創建指定的鏡像目錄。若當前aufs目錄下,還不存在mnt、diff這兩個目錄,則會首先創建mnt、diff這兩個目錄,并在這兩個目錄下分別創建代表鏡像內容的文件夾,文件夾名為鏡像ID,文件權限為0755。假設下載鏡像的鏡像ID為image_ID,則創建完畢之后,文件系統中的文件為/var/lib/docker/aufs/mnt/image_ID與/var/lib/docker/aufs/diff/image_ID。回到Create函數中,執行完createDirsFor函數之后,隨即在aufs目錄下創建了layers目錄,并在layers目錄下創建image_ID文件。

如此一來,在aufs下的三個子目錄mnt,diff以及layers中,分別創建了名為鏡像名image_ID的文件。繼續深入分析之前,我們直接來看Docker對這三個目錄mnt、diff以及layers的描述,如圖11-2所示:


 Docker源碼分析(十一):鏡像存儲

來自:http://www.infoq.com/cn/articles/docker-source-code-analysis-part11

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!