http://www.infoq.com/cn/articles/docker-source-code-analysis-part11
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個優勢
WebAssembly領進門及未來發展
PayPal線上風控平台技術優化實踐
數字營銷領域的千人千面智能投放算法研究及應用
基于CPU分析醫療影像,實作快速輔助診斷
相關贊助商

(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所示:
圖11-2 aufs driver目錄結構圖
簡要分析圖11-2,圖中的layers、diff以及mnt為目錄/var/lib/docker/aufs下的三個子目錄,1、2、3是鏡像ID,分别代表三個鏡像,三個目錄下的1均代表同一個鏡像ID。其中layers目錄下保留每一個鏡像的中繼資料,這些中繼資料是這個鏡像的祖先鏡像ID清單;diff目錄下存儲着每一個鏡像所在的layer,具體包含的檔案系統内容;mnt目錄下每一個檔案,都是一個鏡像ID,代表在該層鏡像之上挂載的可讀寫layer。是以,下載下傳的鏡像中與檔案系統相關的具體内容,都會存儲在diff目錄下的某個鏡像ID目錄下。
再次回到Create函數,此時mnt,diff以及layer三個目錄下的鏡像ID檔案已經建立完畢。下一步需要完成的是:為layers目錄下的鏡像ID檔案填充中繼資料。中繼資料内容為該鏡像所有的祖先鏡像ID清單。填充中繼資料的流程如下:
(1) Docker Daemon首先通過f, err := os.Create(path.Join(a.rootPath(), "layers", id))打開layers目錄下鏡像ID檔案;
(2) 然後,通過ids, err := getParentIds(a.rootPath(), parent)擷取父鏡像的祖先鏡像ID清單ids;
(3) 其次,将父鏡像鏡像ID寫入檔案f;
(4) 最後,将父鏡像的祖先鏡像ID清單ids寫入檔案f。
最終的結果是:該鏡像的所有祖先鏡像的鏡像ID資訊都寫入layers目錄下該鏡像ID檔案中。
4.2 mount祖先鏡像并傳回根目錄
Create函數執行完畢,意味着建立鏡像路徑并配置鏡像中繼資料完畢,接着Docker Daemon傳回了鏡像的根目錄,源碼實作如下:
rootfs, err := graph.driver.Get(img.ID, "")
Get函數看似傳回了鏡像的根目錄rootfs,實則執行了更為重要的内容——挂載祖先鏡像檔案系統。具體而言,Docker Daemon為目前層的鏡像完成所有祖先鏡像的Union Mount。Mount完畢之後,目前鏡像的read-write層位于/var/lib/docker/aufs/mnt/image_ID。Get函數的具體實作位于./docker/daemon/graphdriver/aufs/aufs.go#L247-L278,如下:
func (a *Driver) Get(id, mountLabel string) (string, error) {
ids, err := getParentIds(a.rootPath(), id)
if err != nil {
if !os.IsNotExist(err) {
return "", err
}
ids = []string{}
}
// Protect the a.active from concurrent access
a.Lock()
defer a.Unlock()
count := a.active[id]
// If a dir does not have a parent ( no layers )do not try to mount
// just return the diff path to the data
out := path.Join(a.rootPath(), "diff", id)
if len(ids) > 0 {
out = path.Join(a.rootPath(), "mnt", id)
if count == 0 {
if err := a.mount(id, mountLabel); err != nil {
return "", err
}
}
}
a.active[id] = count + 1
return out, nil
}
分析以上Get函數的定義,可以得出以下内容:
(1) 函數名為Get;
(2) 函數調用者類型為Driver;
(3) 函數傳入參數有兩個:id與mountlabel;
(4) 函數傳回内容有兩部分:string類型的鏡像根目錄與錯誤對象error。
清楚Get函數的定義,再來看Get函數的實作。分析Get函數實作時,有三個部分較為關鍵,分别是Driver執行個體a的active屬性、mount操作、以及傳回值out。
首先分析Driver執行個體a的active屬性。分析active屬性之前,需要追溯到Aufs類型的graphdriver中Driver類型的定義以及graphdriver與graph的關系。兩者的關系如圖11-3所示:
圖11-3 graph與graphdriver關系圖
Driver類型的定義位于./docker/daemon/graphdriver/aufs/aufs#L53-L57,如下:
type Driver struct {
root string
sync.Mutex // Protects concurrent modification to active
active map[string]int
}
Driver結構體中root屬性代表graphdriver所在的根目錄,為/var/lib/docker/aufs。active屬性為map類型,key為string,具體運用時key為Docker Image的ID,value為int類型,代表該層鏡像layer被引用的次數總和。Docker鏡像技術中,某一層layer的Docker鏡像被引用一次,則active屬性中key為該鏡像ID的value值會累加1。使用者執行鏡像删除操作時,Docker Dameon會檢查該Docker鏡像的引用次數是否為0,若引用次數為0,則可以徹底删除該鏡像,若不是的話,則僅僅将active屬性中引用參數減1。屬性sync.Mutex用于多個Job同時操作active屬性時,確定active資料的同步工作。
接着,進入mount操作的分析。一旦Get參數傳入的鏡像ID參數不是一個Base Image,那麼說明該鏡像存在父鏡像,Docker Daemon需要将該鏡像所有的祖先鏡像都mount到指定的位置,指定位置為/var/lib/docker/aufs/mnt/image_ID。所有祖先鏡像的原生态檔案系統内容分别位于/var/lib/docker/aufs/diff/<ID>。其中mount函數用以實作該部分描述的功能,mount的過程包含很多與aufs檔案系統相關的參數配置與系統調用。
最後,Get函數傳回out與nil。其中out的值為/var/lib/docker/aufs/mnt/image_ID,即使用該層Docker鏡像時其根目錄所在路徑,也可以認為是鏡像的RW層所在路徑,但一旦該層鏡像之上還有鏡像,那麼在mount後者之後,在上層鏡像看來,下層鏡像仍然是隻讀檔案系統。
5.存儲鏡像内容
存儲鏡像内容,Docker Daemon的運作意味着已經驗證過鏡像ID,同時還為鏡像準備了存儲路徑,并傳回了其所有祖先鏡像union mount後的路徑。萬事俱備,隻欠“鏡像内容的存儲”。
Docker Daemon存儲鏡像具體内容完成的工作很簡單,僅僅是通過某種合适的方式将兩部分内容存儲于本地檔案系統并進行有效管理,它們是:鏡像壓縮内容、鏡像json資訊。
存儲鏡像内容的源碼實作位于./docker/graph/graph.go#L209-L211,如下:
if err := image.StoreImage(img, jsonData, layerData, tmp, rootfs); err != nil {
return err
}
其中,StoreImage函數的定義位于./docker/docker/image/image.go#L74,如下:
func StoreImage(img *Image, jsonData []byte, layerData
archive.ArchiveReader, root, layer string) error {
分析StoreImage函數的定義,可以得出以下資訊:
(1) 函數名稱:StoreImage;
(2) 函數傳入參數名:img,jsonData,layerData,root,layer;
(3) 函數傳回類型error。
簡要分析傳入參數的含義如表11-1所示:
表11-1 StoreImage函數參數表
參數名稱 | 參數含義 |
img | 通過下載下傳的imgJSON資訊建立出的Image對象執行個體 |
jsonData | Docker Daemon之前下載下傳的imgJSON資訊 |
layerData | 鏡像作為一個layer的壓縮包,包含鏡像的具體檔案内容 |
root | graphdriver根目錄下建立的臨時檔案”_tmp”,值為/var/lib/docker/aufs/_tmp |
layer | Mount完所有祖先鏡像之後,該鏡像在mnt目錄下的路徑 |
掌握StoreImage函數傳入參數的含義之後,了解其實作就十分簡單。總體而言,StoreImage亦可以分為三個步驟:
(1) 解壓鏡像内容layerData至diff目錄;
(2) 收集鏡像所占空間大小,并記錄;
(3) 将jsonData資訊寫入臨時檔案。
以下詳細深入三個步驟的實作。
5.1解壓鏡像内容
StoreImage函數傳入的鏡像内容是一個壓縮包,Docker Daemon理應在鏡像存儲時将其解壓,為後續建立容器時直接使用鏡像創造便利。
既然是解壓鏡像内容,那麼這項任務的完成,除了需要代表鏡像的壓縮包之後,還需要解壓任務的目标路徑,以及解壓時的參數。壓縮包為StoreImage傳入的參數layerData,而目标路徑為/var/lib/docker/aufs/diff/<image_ID>。解壓流程的執行源代碼位于./docker/docker/image/image.go#L85-L120,如下:
// If layerData is not nil, unpack it into the new layer
if layerData != nil {
if differ, ok := driver.(graphdriver.Differ); ok {
if err := differ.ApplyDiff(img.ID, layerData); err != nil {
return err
}
if size, err = differ.DiffSize(img.ID); err != nil {
return err
}
} else {
start := time.Now().UTC()
log.Debugf("Start untar layer")
if err := archive.ApplyLayer(layer, layerData); err != nil {
return err
}
log.Debugf("Untar time: %vs", time.Now().UTC().Sub(start).Seconds())
if img.Parent == "" {
if size, err = utils.TreeSize(layer); err != nil {
return err
}
} else {
parent, err := driver.Get(img.Parent, "")
if err != nil {
return err
}
defer driver.Put(img.Parent)
changes, err := archive.ChangesDirs(layer, parent)
if err != nil {
return err
}
size = archive.ChangesSize(layer, changes)
}
}
}
可見當鏡像内容layerData不為空時,Docker Daemon需要為鏡像壓縮包執行解壓工作。以aufs這種graphdriver為例,一旦aufs driver實作了graphdriver包中的接口Diff,則Docker Daemon會使用aufs driver的接口方法實作後續的解壓操作。解壓操作的源代碼如下:
if differ, ok := driver.(graphdriver.Differ); ok {
if err := differ.ApplyDiff(img.ID, layerData); err != nil {
return err
}
if size, err = differ.DiffSize(img.ID); err != nil {
return err
}
}
以上代碼即實作了鏡像壓縮包的解壓,與鏡像所占空間大小的統計。代碼differ.ApplyDiff(img.ID, layerData)将layerData解壓至目标路徑。理清目标路徑,且看aufs這個driver中ApplyDiff的實作,位于./docker/docker/daemon/graphdriver/aufs/aufs.go#L304-L306,如下:
func (a *Driver) ApplyDiff(id string, diff archive.ArchiveReader) error {
return archive.Untar(diff, path.Join(a.rootPath(), "diff", id), nil)
}
解壓過程中,Docker Daemon通過aufs driver的根目錄/var/lib/docker/aufs、diff目錄與鏡像ID,拼接出鏡像的解壓路徑,并執行解壓任務。舉例說明diff檔案的作用,鏡像27d474解壓後的内容如圖11-4所示:
圖11-4鏡像解壓後示意圖
回到StoreImage函數的執行流中,ApplyDiff任務完成之後,Docker Daemon通過DiffSize開啟鏡像磁盤空間統計任務。
5.2收集鏡像大小并記錄
Docker Daemon接管鏡像存儲之後,Docker鏡像被解壓到指定路徑并非意味着“任務完成”。Docker Daemon還額外做了鏡像所占空間大小統計的空間,以便記錄鏡像資訊,最終将這類資訊傳遞給Docker使用者。
鏡像所占磁盤空間大小的統計與記錄,實作過程簡單且有效,源代碼位于./docker/docker/image/image.go#L122-L125,如下:
img.Size = size
if err := img.SaveSize(root); err != nil {
return err
}
首先Docker Daemon将鏡像大小收集起來,更新Image類型執行個體img的Size屬性,然後通過img.SaveSize(root)将鏡像大小寫入root目錄,由于傳入的root參數為臨時目錄_tmp,即寫入臨時目錄_tmp下。深入SaveSize函數的實作,如以下源碼:
func (img *Image) SaveSize(root string) error {
if err := ioutil.WriteFile(path.Join(root, "layersize"), []
byte(strconv.Itoa(int(img.Size))), 0600); err != nil {
return fmt.Errorf("Error storing image size in %s/layersize: %s", root, err)
}
return nil
}
SaveSize函數在root目錄(臨時目錄/var/lib/docker/graph/_tmp)下建立檔案layersize,并寫入鏡像大小的值img.Size。
5.3存儲jsonData資訊
Docker鏡像中jsonData是一個非常重要的概念。在筆者看來,Docker的鏡像并非隻是Docker容器檔案系統中的檔案内容,同時還包括Docker容器運作的動态資訊。這裡的動态資訊更多的是為了适配Dockerfile的标準。以Dockerfile中的ENV參數為例,ENV指定了Docker容器運作時,内部程序的環境變量。而這些隻有容器運作時才存在的動态資訊,并不會被記錄在靜态的鏡像檔案系統中,而是存儲在以jsonData的形式先存儲在主控端的檔案系統中,并與鏡像檔案系統做清楚的區分,存儲在不同的位置。當Docker Daemon啟動Docker容器時,Docker Daemon會準備好mount完畢的鏡像檔案系統環境;接着加載jsonData資訊,并在運作Docker容器内部程序時,使用動态的jsonData内部資訊為容器内部程序配置環境。
當Docker Daemon下載下傳Docker鏡像時,關于每一個鏡像的jsonData資訊均會被下載下傳至主控端。通過以上jsonData的功能描述可以發現,這部分資訊的存儲同樣扮演重要的角色。Docker Daemon如何存儲jsonData資訊,實作源碼位于./docker/docker/image/image.go#L128-L139,如下:
if jsonData != nil {
if err := ioutil.WriteFile(jsonPath(root), jsonData, 0600); err != nil {
return err
}
} else {
if jsonData, err = json.Marshal(img); err != nil {
return err
}
if err := ioutil.WriteFile(jsonPath(root), jsonData, 0600); err != nil {
return err
}
}
可見Docker Daemon将jsonData寫入了檔案jsonPath(root)中,并為該檔案設定的權限為0600。而jsonPath(root)的實作如下,即在root目錄(/var/lib/docker/graph/_tmp目錄)下建立檔案json:
func jsonPath(root string) string {
return path.Join(root, "json")
}
鏡像大小資訊layersize資訊統計完畢,jsonData資訊也成功記錄,兩者的存儲檔案均位于/var/lib/docker/graph/_tmp下,檔案名分别為layersize和json。使用臨時檔案夾來存儲這部分資訊并非偶然,11.6節将闡述其中的原因。
6.注冊鏡像ID
Docker Daemon執行完鏡像的StoreImage操作,回到Register函數之後,執行鏡像的commit操作,即完成鏡像在graph中的注冊。
注冊鏡像的代碼實作位于./docker/docker/graph/graph.go#L212-L216,如下:
// Commit
if err := os.Rename(tmp, graph.ImageRoot(img.ID)); err != nil {
return err
}
graph.idIndex.Add(img.ID)
11.5節StoreImage過程中使用到的臨時檔案_tmp在注冊鏡像環節有所展現。鏡像的注冊行為,第一步就是将tmp檔案(/var/lib/docker/graph/_tmp )重命名為graph.ImageRoot(img.ID),實則為/var/lib/docker/graph/<img.ID>。使得Docker Daemon在而後的操作中可以通過img.ID在/var/lib/docker/graph目錄下搜尋到相應鏡像的json檔案與layersize檔案。
成功為json檔案與layersize檔案配置完正确的路徑之後,Docker Daemon執行的最後一個步驟為:添加鏡像ID至graph.idIndex。源代碼實作是graph.idIndex.Add(img.ID),graph中idIndex類型為*truncindex.TruncIndex, TruncIndex的定義位于./docker/docker/pkg/truncindex/truncindex.go#L22-L28,如下:
// TruncIndex allows the retrieval of string identifiers by any of their unique prefixes.
// This is used to retrieve image and container IDs by more convenient shorthand prefixes.
type TruncIndex struct {
sync.RWMutex
trie *patricia.Trie
ids map[string]struct{}
}
Docker使用者使用Docker鏡像時,一般可以通過指定鏡像ID來定位鏡像,如Docker官方的mongo:2.6.1鏡像id為c35c0961174d51035d6e374ed9815398b779296b5f0ffceb7613c8199383f4b1,該ID長度為64。當Docker使用者指定運作這個mongo鏡像Repository中tag為2.6.1的鏡像時,完全可以通過64為的鏡像ID來指定,如下:
docker run –it c35c0961174d51035d6e374ed9815398b779296b5f0ffceb7613c8199383f4b1 /bin/bash
然而,記錄如此長的鏡像ID,對于Docker使用者來說稍顯不切實際,而TruncIndex的概念則大大幫助Docker使用者可以通過簡短的ID定位到指定的鏡像,使得Docker鏡像的使用變得尤為友善。原理是:Docker使用者指定鏡像ID的字首,隻要字首滿足在全局所有的鏡像ID中唯一,則Docker Daemon可以通過TruncIndex定位到唯一的鏡像ID。而graph.idIndex.Add(img.ID)正式完成将img.ID添加儲存至TruncIndex中。
為了達到上一條指令的效果,Docker 使用者完全可以使用TruncIndex的方式,當然前提是c35這個字元串作為字首全局唯一,指令如下:
docker run –it c35 /bin/bash
至此,Docker鏡像存儲的整個流程已經完成。概括而言,主要包含了驗證鏡像、存儲鏡像、注冊鏡像三個步驟。
7.總結
Docker鏡像的存儲,使得Docker Hub上的鏡像能夠傳播于世界各地變為現實。Docker鏡像在Docker Registry中的存儲方式與本地化的存儲方式并非一緻。Docker Daemon必須針對自身的graphdriver類型,選擇适配的存儲方式,實施鏡像的存儲。本章的分析,也在不斷強調一個事實,即Docker鏡像并非僅僅包含檔案系統中的靜态檔案,除此之外還包含了鏡像的json資訊,json資訊中有Docker容器的配置資訊,如暴露端口,環境變量等。
可以說Docker容器的運作強依賴于Docker鏡像,Docker鏡像的由來就變得尤為重要。Docker鏡像的下載下傳,Docker鏡像的commit以及docker build新的鏡像,都無法跳出鏡像存儲的範疇。Docker鏡像的存儲知識,也會有助于Docker其他概念的了解,如docker commit、docker build等。
8.作者介紹
孫宏亮,DaoCloud初創團隊成員,軟體工程師,浙江大學VLIS實驗室應屆研究所學生。讀研期間活躍在PaaS和Docker開源社群,對Cloud Foundry有深入研究和豐富實踐,擅長底層平台代碼分析,對分布式平台的架構有一定經驗,撰寫了大量有深度的技術部落格。2014年末以合夥人身份加入DaoCloud團隊,緻力于傳播以Docker為主的容器的技術,推動網際網路應用的容器化步伐。郵箱:[email protected]
參考文獻
http://aufs.sourceforge.net/aufs.html