日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程资源 > 编程问答 >内容正文

编程问答

Docker源码分析(十一):镜像存储

發(fā)布時(shí)間:2025/4/5 编程问答 16 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Docker源码分析(十一):镜像存储 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

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

1.前言

Docker Hub匯總眾多Docker用戶(hù)的鏡像,極大得發(fā)揮Docker鏡像開(kāi)放的思想。Docker用戶(hù)在全球任意一個(gè)角度,都可以與Docker Hub交互,分享自己構(gòu)建的鏡像至Docker Hub,當(dāng)然也完全可以下載另一半球Docker開(kāi)發(fā)者上傳至Docker Hub的Docker鏡像。

無(wú)論是上傳,還是下載Docker鏡像,鏡像必然會(huì)以某種形式存儲(chǔ)在Docker Daemon所在的宿主機(jī)文件系統(tǒng)中。Docker鏡像在宿主機(jī)的存儲(chǔ),關(guān)鍵點(diǎn)在于:在本地文件系統(tǒng)中以如何組織形式,被Docker Daemon有效的統(tǒng)一化管理。這種管理,可以使得Docker Daemon創(chuàng)建Docker容器服務(wù)時(shí),方便獲取鏡像并完成union mount操作,為容器準(zhǔn)備初始化的文件系統(tǒng)。

本文主要從Docker 1.2.0源碼的角度,分析Docker Daemon下載鏡像過(guò)程中存儲(chǔ)Docker鏡像的環(huán)節(jié)。分析內(nèi)容的安排有以下5部分:

(1) 概述Docker鏡像存儲(chǔ)的執(zhí)行入口,并簡(jiǎn)要介紹存儲(chǔ)流程的四個(gè)步驟;

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

(3) 創(chuàng)建鏡像存儲(chǔ)路徑;

相關(guān)廠商內(nèi)容

迅雷鏈相比其他主鏈,多了4個(gè)優(yōu)勢(shì)

WebAssembly領(lǐng)進(jìn)門(mén)及未來(lái)發(fā)展

PayPal在線風(fēng)控平臺(tái)技術(shù)優(yōu)化實(shí)踐

數(shù)字營(yíng)銷(xiāo)領(lǐng)域的千人千面智能投放算法研究及應(yīng)用

基于CPU分析醫(yī)療影像,實(shí)現(xiàn)快速輔助診斷

相關(guān)贊助商

(4) 存儲(chǔ)鏡像內(nèi)容;

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

2.鏡像注冊(cè)

Docker Daemon執(zhí)行鏡像下載任務(wù)時(shí),從Docker Registry處下載指定鏡像之后,仍需要將鏡像合理地存儲(chǔ)于宿主機(jī)的文件系統(tǒng)中。更為具體而言,存儲(chǔ)工作分為兩個(gè)部分:

(1) 存儲(chǔ)鏡像內(nèi)容;

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

說(shuō)到鏡像內(nèi)容,需要強(qiáng)調(diào)的是,每一層layer的Docker Image內(nèi)容都可以認(rèn)為有兩個(gè)部分組成:鏡像中每一層layer中存儲(chǔ)的文件系統(tǒng)內(nèi)容,這部分內(nèi)容一般可以認(rèn)為是未來(lái)Docker容器的靜態(tài)文件內(nèi)容;另一部分內(nèi)容指的是容器的json文件,json文件代表的信息除了容器的基本屬性信息之外,還包括未來(lái)容器運(yùn)行時(shí)的動(dòng)態(tài)信息,包括ENV等信息。

存儲(chǔ)鏡像內(nèi)容,意味著Docker Daemon所在宿主機(jī)上已經(jīng)存在鏡像的所有內(nèi)容,除此之外,Docker Daemon仍需要對(duì)所存儲(chǔ)的鏡像進(jìn)行統(tǒng)計(jì)備案,以便用戶(hù)在后續(xù)的鏡像管理與使用過(guò)程中,可以有據(jù)可循。為此,Docker Daemon設(shè)計(jì)了graph,使用graph來(lái)接管這部分的工作。graph負(fù)責(zé)記錄有哪些鏡像已經(jīng)被正確存儲(chǔ),供Docker Daemon調(diào)用。

Docker Daemon執(zhí)行CmdPull任務(wù)的pullImage階段時(shí),實(shí)現(xiàn)Docker鏡像存儲(chǔ)與記錄的源碼位于./docker/graph/pull.go#L283-L285,如下:

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

以上源碼的實(shí)現(xiàn),實(shí)際調(diào)用了函數(shù)Register,Register函數(shù)的定義位于./docker/graph/graph.go#L162-L218:

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

分析以上Register函數(shù)定義,可以得出以下內(nèi)容:

(1) 函數(shù)名稱(chēng)為Register;

(2) 函數(shù)調(diào)用者類(lèi)型為Graph;

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

(4) 函數(shù)返回對(duì)象為err,類(lèi)型為error。

Register函數(shù)的運(yùn)行流程如圖11-1所示:

圖11-1 Register函數(shù)執(zhí)行流程圖

3.驗(yàn)證鏡像ID

Docker鏡像注冊(cè)的第一個(gè)步驟是驗(yàn)證Docker鏡像的ID。此步驟主要為確保鏡像ID命名的合法性。功能而言,這部分內(nèi)容提高了Docker鏡像存儲(chǔ)環(huán)節(jié)的魯棒性。驗(yàn)證鏡像ID由三個(gè)環(huán)節(jié)組成。

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

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

(3) 初始化鏡像目錄。

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

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

ValidateID函數(shù)的實(shí)現(xiàn)過(guò)程中,Docker Dameon檢驗(yàn)了鏡像ID是否為空,以及鏡像ID中是否存在字符‘:’,以上兩種情況只要成立其中之一,Docker Daemon即認(rèn)為鏡像ID不合法,不予執(zhí)行后續(xù)內(nèi)容。

鏡像ID的合法性驗(yàn)證完畢之后,Docker Daemon接著驗(yàn)證鏡像是否已經(jīng)存在于graph。若該鏡像已經(jīng)存在于graph,則Docker Daemon返回相應(yīng)錯(cuò)誤,不予執(zhí)行后續(xù)內(nèi)容。代碼實(shí)現(xiàn)如下:

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

驗(yàn)證工作完成之后,Docker Daemon為鏡像準(zhǔn)備存儲(chǔ)路徑。該部分源碼實(shí)現(xiàn)位于./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為鏡像初始化存儲(chǔ)路徑,實(shí)則首先刪除屬于新鏡像的存儲(chǔ)路徑,即如果該鏡像路徑已經(jīng)在文件系統(tǒng)中存在的話,立即刪除該路徑,確保鏡像存儲(chǔ)時(shí)不會(huì)出現(xiàn)路徑?jīng)_突問(wèn)題;接著還刪除graph.driver中的指定內(nèi)容,即如果該鏡像在graph.driver中存在的話,unmount該鏡像在宿主機(jī)上的目錄,并將該目錄完全刪除。以AUFS這種類(lèi)型的graphdriver為例,鏡像內(nèi)容被存放在/var/lib/docker/aufs/diff目錄下,而鏡像會(huì)被mount至目錄/var/lib/docker/aufs/mnt下的指定位置。

至此,驗(yàn)證Docker鏡像ID的工作已經(jīng)完成,并且Docker Daemon已經(jīng)完成對(duì)鏡像存儲(chǔ)路徑的初始化,使得后續(xù)Docker鏡像存儲(chǔ)時(shí)存儲(chǔ)路徑不會(huì)沖突,graph.driver對(duì)該鏡像的mount也不會(huì)沖突。

4.創(chuàng)建鏡像路徑

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

4.1創(chuàng)建mnt、diff和layers

創(chuàng)建鏡像路徑的源碼實(shí)現(xiàn)位于./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函數(shù)在創(chuàng)建鏡像路徑時(shí)起到舉足輕重的作用。那我們首先分析graph.driver.Create(img.ID, img.Parent)的具體實(shí)現(xiàn)。由于在Docker Daemon啟動(dòng)時(shí),注冊(cè)了具體的graphdriver,故graph.driver實(shí)際的值為具體注冊(cè)的driver。方便起見(jiàn),本章內(nèi)容全部以aufs類(lèi)型為例,即在graph.driver為aufs的情況下,闡述Docker鏡像的存儲(chǔ)。在ubuntu 14.04系統(tǒng)上,Docker Daemon的根目錄一般為/var/lib/docker,而aufs類(lèi)型driver的鏡像存儲(chǔ)路徑一般為/var/lib/docker/aufs。

AUFS這種聯(lián)合文件系統(tǒng)的實(shí)現(xiàn),在union多個(gè)鏡像時(shí)起到至關(guān)重要的作用。首先來(lái)關(guān)注,Docker Daemon如何為鏡像創(chuàng)建鏡像路徑,以便支持通過(guò)aufs來(lái)union鏡像。Aufs模式下,graph.driver.Create(img.ID, img.Parent)的具體源碼實(shí)現(xiàn)位于./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 metadataf, 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函數(shù)的實(shí)現(xiàn)過(guò)程中,createDirsFor函數(shù)在Docker Daemon根目錄下的aufs目錄/var/lib/docker/aufs中,創(chuàng)建指定的鏡像目錄。若當(dāng)前aufs目錄下,還不存在mnt、diff這兩個(gè)目錄,則會(huì)首先創(chuàng)建mnt、diff這兩個(gè)目錄,并在這兩個(gè)目錄下分別創(chuàng)建代表鏡像內(nèi)容的文件夾,文件夾名為鏡像ID,文件權(quán)限為0755。假設(shè)下載鏡像的鏡像ID為image_ID,則創(chuàng)建完畢之后,文件系統(tǒng)中的文件為/var/lib/docker/aufs/mnt/image_ID與/var/lib/docker/aufs/diff/image_ID。回到Create函數(shù)中,執(zhí)行完createDirsFor函數(shù)之后,隨即在aufs目錄下創(chuàng)建了layers目錄,并在layers目錄下創(chuàng)建image_ID文件。

如此一來(lái),在aufs下的三個(gè)子目錄mnt,diff以及l(fā)ayers中,分別創(chuàng)建了名為鏡像名image_ID的文件。繼續(xù)深入分析之前,我們直接來(lái)看Docker對(duì)這三個(gè)目錄mnt、diff以及l(fā)ayers的描述,如圖11-2所示:

圖11-2 aufs driver目錄結(jié)構(gòu)圖

簡(jiǎn)要分析圖11-2,圖中的layers、diff以及mnt為目錄/var/lib/docker/aufs下的三個(gè)子目錄,1、2、3是鏡像ID,分別代表三個(gè)鏡像,三個(gè)目錄下的1均代表同一個(gè)鏡像ID。其中l(wèi)ayers目錄下保留每一個(gè)鏡像的元數(shù)據(jù),這些元數(shù)據(jù)是這個(gè)鏡像的祖先鏡像ID列表;diff目錄下存儲(chǔ)著每一個(gè)鏡像所在的layer,具體包含的文件系統(tǒng)內(nèi)容;mnt目錄下每一個(gè)文件,都是一個(gè)鏡像ID,代表在該層鏡像之上掛載的可讀寫(xiě)layer。因此,下載的鏡像中與文件系統(tǒng)相關(guān)的具體內(nèi)容,都會(huì)存儲(chǔ)在diff目錄下的某個(gè)鏡像ID目錄下。

再次回到Create函數(shù),此時(shí)mnt,diff以及l(fā)ayer三個(gè)目錄下的鏡像ID文件已經(jīng)創(chuàng)建完畢。下一步需要完成的是:為layers目錄下的鏡像ID文件填充元數(shù)據(jù)。元數(shù)據(jù)內(nèi)容為該鏡像所有的祖先鏡像ID列表。填充元數(shù)據(jù)的流程如下:

(1) Docker Daemon首先通過(guò)f, err := os.Create(path.Join(a.rootPath(), "layers", id))打開(kāi)layers目錄下鏡像ID文件;

(2) 然后,通過(guò)ids, err := getParentIds(a.rootPath(), parent)獲取父鏡像的祖先鏡像ID列表ids;

(3) 其次,將父鏡像鏡像ID寫(xiě)入文件f;

(4) 最后,將父鏡像的祖先鏡像ID列表ids寫(xiě)入文件f。

最終的結(jié)果是:該鏡像的所有祖先鏡像的鏡像ID信息都寫(xiě)入layers目錄下該鏡像ID文件中。

4.2 mount祖先鏡像并返回根目錄

Create函數(shù)執(zhí)行完畢,意味著創(chuàng)建鏡像路徑并配置鏡像元數(shù)據(jù)完畢,接著Docker Daemon返回了鏡像的根目錄,源碼實(shí)現(xiàn)如下:

rootfs, err := graph.driver.Get(img.ID, "")

Get函數(shù)看似返回了鏡像的根目錄rootfs,實(shí)則執(zhí)行了更為重要的內(nèi)容——掛載祖先鏡像文件系統(tǒng)。具體而言,Docker Daemon為當(dāng)前層的鏡像完成所有祖先鏡像的Union Mount。Mount完畢之后,當(dāng)前鏡像的read-write層位于/var/lib/docker/aufs/mnt/image_ID。Get函數(shù)的具體實(shí)現(xiàn)位于./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 accessa.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 dataout := 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 + 1return out, nil }

分析以上Get函數(shù)的定義,可以得出以下內(nèi)容:

(1) 函數(shù)名為Get;

(2) 函數(shù)調(diào)用者類(lèi)型為Driver;

(3) 函數(shù)傳入?yún)?shù)有兩個(gè):id與mountlabel;

(4) 函數(shù)返回內(nèi)容有兩部分:string類(lèi)型的鏡像根目錄與錯(cuò)誤對(duì)象error。

清楚Get函數(shù)的定義,再來(lái)看Get函數(shù)的實(shí)現(xiàn)。分析Get函數(shù)實(shí)現(xiàn)時(shí),有三個(gè)部分較為關(guān)鍵,分別是Driver實(shí)例a的active屬性、mount操作、以及返回值out。

首先分析Driver實(shí)例a的active屬性。分析active屬性之前,需要追溯到Aufs類(lèi)型的graphdriver中Driver類(lèi)型的定義以及graphdriver與graph的關(guān)系。兩者的關(guān)系如圖11-3所示:

圖11-3 graph與graphdriver關(guān)系圖

Driver類(lèi)型的定義位于./docker/daemon/graphdriver/aufs/aufs#L53-L57,如下:

type Driver struct {root stringsync.Mutex // Protects concurrent modification to activeactive map[string]int }

Driver結(jié)構(gòu)體中root屬性代表graphdriver所在的根目錄,為/var/lib/docker/aufs。active屬性為map類(lèi)型,key為string,具體運(yùn)用時(shí)key為Docker Image的ID,value為int類(lèi)型,代表該層鏡像layer被引用的次數(shù)總和。Docker鏡像技術(shù)中,某一層layer的Docker鏡像被引用一次,則active屬性中key為該鏡像ID的value值會(huì)累加1。用戶(hù)執(zhí)行鏡像刪除操作時(shí),Docker Dameon會(huì)檢查該Docker鏡像的引用次數(shù)是否為0,若引用次數(shù)為0,則可以徹底刪除該鏡像,若不是的話,則僅僅將active屬性中引用參數(shù)減1。屬性sync.Mutex用于多個(gè)Job同時(shí)操作active屬性時(shí),確保active數(shù)據(jù)的同步工作。

接著,進(jìn)入mount操作的分析。一旦Get參數(shù)傳入的鏡像ID參數(shù)不是一個(gè)Base Image,那么說(shuō)明該鏡像存在父鏡像,Docker Daemon需要將該鏡像所有的祖先鏡像都mount到指定的位置,指定位置為/var/lib/docker/aufs/mnt/image_ID。所有祖先鏡像的原生態(tài)文件系統(tǒng)內(nèi)容分別位于/var/lib/docker/aufs/diff/<ID>。其中mount函數(shù)用以實(shí)現(xiàn)該部分描述的功能,mount的過(guò)程包含很多與aufs文件系統(tǒng)相關(guān)的參數(shù)配置與系統(tǒng)調(diào)用。

最后,Get函數(shù)返回out與nil。其中out的值為/var/lib/docker/aufs/mnt/image_ID,即使用該層Docker鏡像時(shí)其根目錄所在路徑,也可以認(rèn)為是鏡像的RW層所在路徑,但一旦該層鏡像之上還有鏡像,那么在mount后者之后,在上層鏡像看來(lái),下層鏡像仍然是只讀文件系統(tǒng)。

5.存儲(chǔ)鏡像內(nèi)容

存儲(chǔ)鏡像內(nèi)容,Docker Daemon的運(yùn)行意味著已經(jīng)驗(yàn)證過(guò)鏡像ID,同時(shí)還為鏡像準(zhǔn)備了存儲(chǔ)路徑,并返回了其所有祖先鏡像union mount后的路徑。萬(wàn)事俱備,只欠“鏡像內(nèi)容的存儲(chǔ)”。

Docker Daemon存儲(chǔ)鏡像具體內(nèi)容完成的工作很簡(jiǎn)單,僅僅是通過(guò)某種合適的方式將兩部分內(nèi)容存儲(chǔ)于本地文件系統(tǒng)并進(jìn)行有效管理,它們是:鏡像壓縮內(nèi)容、鏡像json信息。

存儲(chǔ)鏡像內(nèi)容的源碼實(shí)現(xiàn)位于./docker/graph/graph.go#L209-L211,如下:

if err := image.StoreImage(img, jsonData, layerData, tmp, rootfs); err != nil {return err }

其中,StoreImage函數(shù)的定義位于./docker/docker/image/image.go#L74,如下:

func StoreImage(img *Image, jsonData []byte, layerData archive.ArchiveReader, root, layer string) error {

分析StoreImage函數(shù)的定義,可以得出以下信息:

(1) 函數(shù)名稱(chēng):StoreImage;

(2) 函數(shù)傳入?yún)?shù)名:img,jsonData,layerData,root,layer;

(3) 函數(shù)返回類(lèi)型error。

簡(jiǎn)要分析傳入?yún)?shù)的含義如表11-1所示:

表11-1 StoreImage函數(shù)參數(shù)表

參數(shù)名稱(chēng)

參數(shù)含義

img

通過(guò)下載的imgJSON信息創(chuàng)建出的Image對(duì)象實(shí)例

jsonData

Docker Daemon之前下載的imgJSON信息

layerData

鏡像作為一個(gè)layer的壓縮包,包含鏡像的具體文件內(nèi)容

root

graphdriver根目錄下創(chuàng)建的臨時(shí)文件”_tmp”,值為/var/lib/docker/aufs/_tmp

layer

Mount完所有祖先鏡像之后,該鏡像在mnt目錄下的路徑

掌握StoreImage函數(shù)傳入?yún)?shù)的含義之后,理解其實(shí)現(xiàn)就十分簡(jiǎn)單。總體而言,StoreImage亦可以分為三個(gè)步驟:

(1) 解壓鏡像內(nèi)容layerData至diff目錄;

(2) 收集鏡像所占空間大小,并記錄;

(3) 將jsonData信息寫(xiě)入臨時(shí)文件。

以下詳細(xì)深入三個(gè)步驟的實(shí)現(xiàn)。

5.1解壓鏡像內(nèi)容

StoreImage函數(shù)傳入的鏡像內(nèi)容是一個(gè)壓縮包,Docker Daemon理應(yīng)在鏡像存儲(chǔ)時(shí)將其解壓,為后續(xù)創(chuàng)建容器時(shí)直接使用鏡像創(chuàng)造便利。

既然是解壓鏡像內(nèi)容,那么這項(xiàng)任務(wù)的完成,除了需要代表鏡像的壓縮包之后,還需要解壓任務(wù)的目標(biāo)路徑,以及解壓時(shí)的參數(shù)。壓縮包為StoreImage傳入的參數(shù)layerData,而目標(biāo)路徑為/var/lib/docker/aufs/diff/<image_ID>。解壓流程的執(zhí)行源代碼位于./docker/docker/image/image.go#L85-L120,如下:

// If layerData is not nil, unpack it into the new layerif 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)}}}

可見(jiàn)當(dāng)鏡像內(nèi)容layerData不為空時(shí),Docker Daemon需要為鏡像壓縮包執(zhí)行解壓工作。以aufs這種graphdriver為例,一旦aufs driver實(shí)現(xiàn)了graphdriver包中的接口Diff,則Docker Daemon會(huì)使用aufs driver的接口方法實(shí)現(xiàn)后續(xù)的解壓操作。解壓操作的源代碼如下:

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}}

以上代碼即實(shí)現(xiàn)了鏡像壓縮包的解壓,與鏡像所占空間大小的統(tǒng)計(jì)。代碼differ.ApplyDiff(img.ID, layerData)將layerData解壓至目標(biāo)路徑。理清目標(biāo)路徑,且看aufs這個(gè)driver中ApplyDiff的實(shí)現(xiàn),位于./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) }

解壓過(guò)程中,Docker Daemon通過(guò)aufs driver的根目錄/var/lib/docker/aufs、diff目錄與鏡像ID,拼接出鏡像的解壓路徑,并執(zhí)行解壓任務(wù)。舉例說(shuō)明diff文件的作用,鏡像27d474解壓后的內(nèi)容如圖11-4所示:

圖11-4鏡像解壓后示意圖

回到StoreImage函數(shù)的執(zhí)行流中,ApplyDiff任務(wù)完成之后,Docker Daemon通過(guò)DiffSize開(kāi)啟鏡像磁盤(pán)空間統(tǒng)計(jì)任務(wù)。

5.2收集鏡像大小并記錄

Docker Daemon接管鏡像存儲(chǔ)之后,Docker鏡像被解壓到指定路徑并非意味著“任務(wù)完成”。Docker Daemon還額外做了鏡像所占空間大小統(tǒng)計(jì)的空間,以便記錄鏡像信息,最終將這類(lèi)信息傳遞給Docker用戶(hù)。

鏡像所占磁盤(pán)空間大小的統(tǒng)計(jì)與記錄,實(shí)現(xiàn)過(guò)程簡(jiǎn)單且有效,源代碼位于./docker/docker/image/image.go#L122-L125,如下:

img.Size = sizeif err := img.SaveSize(root); err != nil {return err}

首先Docker Daemon將鏡像大小收集起來(lái),更新Image類(lèi)型實(shí)例img的Size屬性,然后通過(guò)img.SaveSize(root)將鏡像大小寫(xiě)入root目錄,由于傳入的root參數(shù)為臨時(shí)目錄_tmp,即寫(xiě)入臨時(shí)目錄_tmp下。深入SaveSize函數(shù)的實(shí)現(xiàn),如以下源碼:

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函數(shù)在root目錄(臨時(shí)目錄/var/lib/docker/graph/_tmp)下創(chuàng)建文件layersize,并寫(xiě)入鏡像大小的值img.Size。

5.3存儲(chǔ)jsonData信息

Docker鏡像中jsonData是一個(gè)非常重要的概念。在筆者看來(lái),Docker的鏡像并非只是Docker容器文件系統(tǒng)中的文件內(nèi)容,同時(shí)還包括Docker容器運(yùn)行的動(dòng)態(tài)信息。這里的動(dòng)態(tài)信息更多的是為了適配Dockerfile的標(biāo)準(zhǔn)。以Dockerfile中的ENV參數(shù)為例,ENV指定了Docker容器運(yùn)行時(shí),內(nèi)部進(jìn)程的環(huán)境變量。而這些只有容器運(yùn)行時(shí)才存在的動(dòng)態(tài)信息,并不會(huì)被記錄在靜態(tài)的鏡像文件系統(tǒng)中,而是存儲(chǔ)在以jsonData的形式先存儲(chǔ)在宿主機(jī)的文件系統(tǒng)中,并與鏡像文件系統(tǒng)做清楚的區(qū)分,存儲(chǔ)在不同的位置。當(dāng)Docker Daemon啟動(dòng)Docker容器時(shí),Docker Daemon會(huì)準(zhǔn)備好mount完畢的鏡像文件系統(tǒng)環(huán)境;接著加載jsonData信息,并在運(yùn)行Docker容器內(nèi)部進(jìn)程時(shí),使用動(dòng)態(tài)的jsonData內(nèi)部信息為容器內(nèi)部進(jìn)程配置環(huán)境。

當(dāng)Docker Daemon下載Docker鏡像時(shí),關(guān)于每一個(gè)鏡像的jsonData信息均會(huì)被下載至宿主機(jī)。通過(guò)以上jsonData的功能描述可以發(fā)現(xiàn),這部分信息的存儲(chǔ)同樣扮演重要的角色。Docker Daemon如何存儲(chǔ)jsonData信息,實(shí)現(xiàn)源碼位于./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}}

可見(jiàn)Docker Daemon將jsonData寫(xiě)入了文件jsonPath(root)中,并為該文件設(shè)置的權(quán)限為0600。而jsonPath(root)的實(shí)現(xiàn)如下,即在root目錄(/var/lib/docker/graph/_tmp目錄)下創(chuàng)建文件json:

func jsonPath(root string) string {return path.Join(root, "json") }

鏡像大小信息layersize信息統(tǒng)計(jì)完畢,jsonData信息也成功記錄,兩者的存儲(chǔ)文件均位于/var/lib/docker/graph/_tmp下,文件名分別為layersize和json。使用臨時(shí)文件夾來(lái)存儲(chǔ)這部分信息并非偶然,11.6節(jié)將闡述其中的原因。

6.注冊(cè)鏡像ID

Docker Daemon執(zhí)行完鏡像的StoreImage操作,回到Register函數(shù)之后,執(zhí)行鏡像的commit操作,即完成鏡像在graph中的注冊(cè)。

注冊(cè)鏡像的代碼實(shí)現(xiàn)位于./docker/docker/graph/graph.go#L212-L216,如下:

// Commitif err := os.Rename(tmp, graph.ImageRoot(img.ID)); err != nil {return err}graph.idIndex.Add(img.ID)

11.5節(jié)StoreImage過(guò)程中使用到的臨時(shí)文件_tmp在注冊(cè)鏡像環(huán)節(jié)有所體現(xiàn)。鏡像的注冊(cè)行為,第一步就是將tmp文件(/var/lib/docker/graph/_tmp )重命名為graph.ImageRoot(img.ID),實(shí)則為/var/lib/docker/graph/<img.ID>。使得Docker Daemon在而后的操作中可以通過(guò)img.ID在/var/lib/docker/graph目錄下搜索到相應(yīng)鏡像的json文件與layersize文件。

成功為json文件與layersize文件配置完正確的路徑之后,Docker Daemon執(zhí)行的最后一個(gè)步驟為:添加鏡像ID至graph.idIndex。源代碼實(shí)現(xiàn)是graph.idIndex.Add(img.ID),graph中idIndex類(lèi)型為*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.RWMutextrie *patricia.Trieids map[string]struct{} }

Docker用戶(hù)使用Docker鏡像時(shí),一般可以通過(guò)指定鏡像ID來(lái)定位鏡像,如Docker官方的mongo:2.6.1鏡像id為c35c0961174d51035d6e374ed9815398b779296b5f0ffceb7613c8199383f4b1?,該ID長(zhǎng)度為64。當(dāng)Docker用戶(hù)指定運(yùn)行這個(gè)mongo鏡像Repository中tag為2.6.1的鏡像時(shí),完全可以通過(guò)64為的鏡像ID來(lái)指定,如下:

docker run –it c35c0961174d51035d6e374ed9815398b779296b5f0ffceb7613c8199383f4b1? /bin/bash

然而,記錄如此長(zhǎng)的鏡像ID,對(duì)于Docker用戶(hù)來(lái)說(shuō)稍顯不切實(shí)際,而TruncIndex的概念則大大幫助Docker用戶(hù)可以通過(guò)簡(jiǎn)短的ID定位到指定的鏡像,使得Docker鏡像的使用變得尤為方便。原理是:Docker用戶(hù)指定鏡像ID的前綴,只要前綴滿(mǎn)足在全局所有的鏡像ID中唯一,則Docker Daemon可以通過(guò)TruncIndex定位到唯一的鏡像ID。而graph.idIndex.Add(img.ID)正式完成將img.ID添加保存至TruncIndex中。

為了達(dá)到上一條命令的效果,Docker 用戶(hù)完全可以使用TruncIndex的方式,當(dāng)然前提是c35這個(gè)字符串作為前綴全局唯一,命令如下:

docker run –it c35 /bin/bash

至此,Docker鏡像存儲(chǔ)的整個(gè)流程已經(jīng)完成。概括而言,主要包含了驗(yàn)證鏡像、存儲(chǔ)鏡像、注冊(cè)鏡像三個(gè)步驟。

7.總結(jié)

Docker鏡像的存儲(chǔ),使得Docker Hub上的鏡像能夠傳播于世界各地變?yōu)楝F(xiàn)實(shí)。Docker鏡像在Docker Registry中的存儲(chǔ)方式與本地化的存儲(chǔ)方式并非一致。Docker Daemon必須針對(duì)自身的graphdriver類(lèi)型,選擇適配的存儲(chǔ)方式,實(shí)施鏡像的存儲(chǔ)。本章的分析,也在不斷強(qiáng)調(diào)一個(gè)事實(shí),即Docker鏡像并非僅僅包含文件系統(tǒng)中的靜態(tài)文件,除此之外還包含了鏡像的json信息,json信息中有Docker容器的配置信息,如暴露端口,環(huán)境變量等。

可以說(shuō)Docker容器的運(yùn)行強(qiáng)依賴(lài)于Docker鏡像,Docker鏡像的由來(lái)就變得尤為重要。Docker鏡像的下載,Docker鏡像的commit以及docker build新的鏡像,都無(wú)法跳出鏡像存儲(chǔ)的范疇。Docker鏡像的存儲(chǔ)知識(shí),也會(huì)有助于Docker其他概念的理解,如docker commit、docker build等。

8.作者介紹

孫宏亮,DaoCloud初創(chuàng)團(tuán)隊(duì)成員,軟件工程師,浙江大學(xué)VLIS實(shí)驗(yàn)室應(yīng)屆研究生。讀研期間活躍在PaaS和Docker開(kāi)源社區(qū),對(duì)Cloud Foundry有深入研究和豐富實(shí)踐,擅長(zhǎng)底層平臺(tái)代碼分析,對(duì)分布式平臺(tái)的架構(gòu)有一定經(jīng)驗(yàn),撰寫(xiě)了大量有深度的技術(shù)博客。2014年末以合伙人身份加入DaoCloud團(tuán)隊(duì),致力于傳播以Docker為主的容器的技術(shù),推動(dòng)互聯(lián)網(wǎng)應(yīng)用的容器化步伐。郵箱:allen.sun@daocloud.io

參考文獻(xiàn)

http://aufs.sourceforge.net/aufs.html

轉(zhuǎn)載于:https://www.cnblogs.com/davidwang456/articles/9603073.html

總結(jié)

以上是生活随笔為你收集整理的Docker源码分析(十一):镜像存储的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。