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所示:
圖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所示:
來自:http://www.infoq.com/cn/articles/docker-source-code-analysis-part11