[TOC]
## 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](https://github.com/docker/docker/blob/v1.2.0/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](https://github.com/docker/docker/blob/v1.2.0/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](https://github.com/docker/docker/blob/v1.2.0/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](https://github.com/docker/docker/blob/v1.2.0/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](https://github.com/docker/docker/blob/v1.2.0/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](https://github.com/docker/docker/blob/v1.2.0/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](https://github.com/docker/docker/blob/v1.2.0/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](https://github.com/docker/docker/blob/v1.2.0/daemon/graphdriver/aufs/aufs.go#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/。其中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](https://github.com/docker/docker/blob/v1.2.0/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](https://github.com/docker/docker/blob/v1.2.0/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/。解壓流程的執行源代碼位于[./docker/docker/image/image.go#L85-L120](https://github.com/docker/docker/blob/v1.2.0/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](https://github.com/docker/docker/blob/v1.2.0/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](https://github.com/docker/docker/blob/v1.2.0/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](https://github.com/docker/docker/blob/v1.2.0/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](https://github.com/docker/docker/blob/v1.2.0/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/。使得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](https://github.com/docker/docker/blob/v1.2.0/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](http://www.daocloud.io/)初創團隊成員,軟件工程師,浙江大學VLIS實驗室應屆研究生。讀研期間活躍在PaaS和Docker開源社區,對Cloud Foundry有深入研究和豐富實踐,擅長底層平臺代碼分析,對分布式平臺的架構有一定經驗,撰寫了大量有深度的技術博客。2014年末以合伙人身份加入DaoCloud團隊,致力于傳播以Docker為主的容器的技術,推動互聯網應用的容器化步伐。郵箱:[allen.sun@daocloud.io](mailto:allen.sun@daocloud.io)
## 參考文獻
[http://aufs.sourceforge.net/aufs.html](http://aufs.sourceforge.net/aufs.html)