Docker分層原理與內部結構


轉自:1 :   https://www.csdn.net/article/2015-08-21/2825511

          2:    http://blog.51cto.com/wzlinux/2044797

第一部分:Docker鏡像的基本知識

1.1 什么是Docker鏡像

       從整體的角度來講,一個完整的Docker鏡像可以支撐一個Docker容器的運行,在 Docker容器運行過程中主要提供文件系統視角。例如一個ubuntu:14.04的鏡像,提供了一個基本的ubuntu:14.04的發行版,當然此 鏡像是不包含操作系統Linux內核的。

       以上內容是從宏觀的角度看看Docker鏡像是什么,我們再從微觀的角度進一步深入 Docker鏡像。剛才提到了“Debian鏡像中安裝MySQL 5.6,就成了mysql:5.6鏡像”,其實在此時Docker鏡像的層級概念就體現出來了。底層一個Debian操作系統鏡像,上面疊加一個 mysql層,就完成了一個mysql鏡像的構建。層級概念就不難理解,此時我們一般debian操作系統鏡像稱為mysql鏡像層的父鏡像。

Docker有兩方面的技術非常重要,第一是Linux 容器方面的技術,第二是Docker鏡像的技術。從技術本身來講,兩者的可復制性很強,不存在絕對的技術難點,然而Docker Hub由於存在大量的數據的原因,導致Docker Hub的可復制性幾乎不存在,這需要一個生態的營造。

1.2 Docker鏡像的內容

大致介紹了Docker鏡像是什么,我們來看看Docker鏡像中有哪些內容?

介紹之前,我先分享一下,我個人在接觸Docker的兩年時間中,對Docker鏡像內容認識的變化。

第一階段:初步接觸Docker。相信很多愛好者都會和我一樣,有這樣一個認識:Docker 鏡像代表一個容器的文件系統內容;

第二階段:初步接觸聯合文件系統。聯合文件系統的概念,讓我意識到鏡像層級管理的技術,每一層鏡像都是容器文件系統內容的一部分。

第三階段:研究鏡像與容器的關系:容器是一個動態的環境,每一層鏡像中的文件屬於靜態內 容,然而 Dockerfile 中的 ENV、VOLUME、CMD 等內容最終都需要落實到容器的運行環境中,而這些內容均不可能直接坐落到每一層鏡像所包含的文件系統內容中,那此時每一個Docker鏡像還會包含 json文件記錄與容器之間的關系。

因此,Docker鏡像的內容主要包含兩個部分:第一,鏡像層文件內容;第二,鏡像json文件。

1.3 Docker鏡像存儲位置

既然是說鏡像存儲的位置,那么應該包含:鏡像層文件和鏡像json文件。如一個ubuntu:14.04鏡像,包含4個鏡像層,在aufs存儲驅動的情況下,在磁盤上的情況可以如以下圖所示:

1.3.1 查看鏡像層組成:

我們可以通過命令 docker history ubuntu:14.04 查看 ubuntu:14.04,結果如下:


1.3.2 鏡像層文件內容存儲

Docker 鏡像層的內容一般在 Docker 根目錄的 aufs 路徑下,為 /var/lib/docker/aufs/diff/,具體情況如下: 


圖中顯示了鏡像 ubuntu:14.04 的 4 個鏡像層內容,以及每個鏡像層內的一級目錄情況。需要額外注意的是:鏡像層 d2a0ecffe6fa 中沒有任何內容,也就是所謂的空鏡像。

1.3.3 鏡像 json 文件存儲

對於每一個鏡像層,Docker 都會保存一份相應的 json 文件,json 文件的存儲路徑為 /var/lib/docker/graph,ubuntu:14.04 所有鏡像層的 json 文件存儲路徑展示如下:



除了 json 文件,大家還看到每一個鏡像層還包含一個 layersize 文件,該文件主要記錄鏡像層內部文件內容的總大小。既然談到了鏡像 json 文件,為了給下文鋪墊,以下貼出 ubuntu:14.04 中空鏡像層 d2a0ecffe6fa 的 json 文件:


Docker鏡像存儲,就和大家一起先看到這。同時介紹Docker鏡像的基本知識也告一段落。以下我們進入此次分享的第二部分。

第二部分 Dockerfile、Docker鏡像和Docker容器的關系

Dockerfile 是軟件的原材料,Docker 鏡像是軟件的交付品,而 Docker 容器則可以認為是軟件的運行態。從應用軟件的角度來看,Dockerfile、Docker 鏡像與 Docker 容器分別代表軟件的三個不同階段,Dockerfile 面向開發,Docker 鏡像成為交付標准,Docker 容器則涉及部署與運維,三者缺一不可,合力充當 Docker 體系的基石。

簡單來講,Dockerfile構建出Docker鏡像,通過Docker鏡像運行Docker容器。

我們可以從Docker容器的角度,來反推三者的關系。首先可以來看下圖:


我們假設這個容器的鏡像通過以下Dockerfile構建而得:

FROM ubuntu:14.04  

ADD run.sh /

VOLUME /data

CMD ["./run.sh"]

2.1 Dockerfile與Docker鏡像

首先,我們結合上圖來看看Dockerfile與Docker鏡像之間的關系。

FROM ubuntu:14.04:設置基礎鏡像,此時會使用基礎鏡像 ubuntu:14.04 的所有鏡像層,為簡單起見,圖中將其作為一個整體展示。

ADD run.sh /:將 Dockerfile 所在目錄的文件 run.sh 加至鏡像的根目錄,此時新一層的鏡像只有一項內容,即根目錄下的 run.sh。

VOLUME /data:設定鏡像的 VOLUME,此 VOLUME 在容器內部的路徑為 /data。需要注意的是,此時並未在新一層的鏡像中添加任何文件,即構建出的磁層鏡像中文件為空,但更新了鏡像的 json 文件,以便通過此鏡像啟動容器時獲取這方面的信息。

CMD ["./run.sh"]:設置鏡像的默認執行入口,此命令同樣不會在新建鏡像中添加任何文件,僅僅在上一層鏡像 json 文件的基礎上更新新建鏡像的 json 文件。

因此,通過以上分析,以上的Dockerfile可以構建出一個新的鏡像,包含4個鏡像層,每一條命令會和一個鏡像層對應,鏡像之間會存在父子關系。圖中很清楚的表明了這些關系。

2.2 Docker鏡像與Docker容器的關系

Docker鏡像是Docker容器運行的基礎,沒有Docker鏡像,就不可能有Docker容器,這也是Docker的設計原則之一。

可以理解的是:Docker鏡像畢竟是鏡像,屬於靜態的內容;而Docker容器就不一樣了,容器屬於動態的內容。動態的內容,大家很容易聯想到進程,內存,CPU等之類的東西。的確,Docker容器作為動態的內容,都會包含這些。

為了便於理解,大家可以把Docker容器,理解為一個或多個運行進程,而這些運行進程將占有相應的內存,相應的CPU計算資源,相應的虛擬網絡設備以及相應的文件系統資源。而Docker容器所占用的文件系統資源,則通過Docker鏡像的鏡像層文件來提供。

那么作為靜態的鏡像,如何才有能力轉化為一個動態的Docker容器呢?此時,我們可以想象:第一,轉化的依據是什么;第二,由誰來執行這個轉化操作。

其實,轉化的依據是每個鏡像的json文件,Docker可以通過解析Docker鏡像的json的文件,獲知應該在這個鏡像之上運行什么樣的進程,應該為進程配置怎么樣的環境變量,此時也就實現了靜態向動態的轉變。

誰來執行這個轉化工作?答案是Docker守護進程。也許大家早就理解這樣一句 話:Docker容器實質上就是一個或者多個進程,而容器的父進程就是Docker守護進程。這樣的,轉化工作的執行就不難理解了:Docker守護進程 手握Docker鏡像的json文件,為容器配置相應的環境,並真正運行Docker鏡像所指定的進程,完成Docker容器的真正創建。

Docker容器運行起來之后,Docker鏡像json文件就失去作用了。此時Docker鏡像的絕大部分作用就是:為Docker容器提供一個文件系統的視角,供容器內部的進程訪問文件資源。

再次回到上圖,我們再來看看容器和鏡像之間的一些特殊關系。首先,之前已經提及Docker鏡像是分層管理的,管理Docker容器的時候,Docker鏡像仍然是分層管理的。由於此時動態的容器中已經存在進程,進程就會對文件系統視角內的文件進行讀寫操作,因此,就會涉及一個問題:容器是否會篡改Docker鏡像的內容?

答案自然是不會的。統一來講,正如上圖,所有的Docker鏡像層對於容器來說,都是只讀的,容器對於文件的寫操作絕對不會作用在鏡像中。

既然如此,實現的原理就很重要,究其根本:Docker守護進程會在Docker鏡像的 最上層之上,再添加一個可讀寫層,容器所有的寫操作都會作用到這一層中。而如果Docker容器需要寫底層Docker鏡像中的文件,那么此時就會涉及一 個叫Copy-on-Write的機制,即aufs等聯合文件系統保證:首先將此文件從Docker鏡像層中拷貝至最上層的可讀寫層,然后容器進程再對讀 寫層中的副本進行寫操縱。對於容器進程來講,它只能看到最上層的文件。

那最后我們再來說說:Docker容器的文件系統視角中,到底是不是存在一些內容,不是存儲於Docker鏡像中的?

這次的答案依舊是肯定的。

再次重申一點,Docker鏡像中存儲的都是一些靜態文件。這些文件原則上應該和容器具體信息以及主機信息完全解藕。那么Docker容器中不存在Docker鏡像中的內容主要有以下幾點:

1./proc以及/sys等虛擬文件系統的內容

2.容器的hosts文件,hostname文件以及resolv.conf文件,這些事具體環境的信息,原則上的確不應該被打入鏡像。

3.容器的Volume路徑,這部分的視角來源於從宿主機上掛載到容器內部的路徑

4.部分的設備文件

QA選集:

問:為什么一個ubuntu:14.04鏡像的鏡像層的數量是4個,前三層的內容似乎有相同的,如etc?

孫宏亮:這一點,細心的大家肯定發現了。首先,雖然三層 都有,但是會存在兩種情況,etc的子目錄下有相同路徑的文件,那么上層的會覆蓋下層的文件;如果內部的文件路徑不相同,那么都會存在,都會呈現給最上 層。[可別較真,說目錄也是文件哈,意會]

問:關於docker安全性問題,對於安全是怎樣處理的,如果我從hub下載鏡像,能判別是否安全么2.層級之間的依賴會導致一個崩了整個docker 都崩了么?

孫宏亮:從流程上來講,如果一切可控的話,我認為是安全的。但是依然會存在一些隱患,比如Dockerfile中基於的base images是否完全受信;鏡像的傳輸過程是否受信;自己的private docker resgitry的安全級別達到什么樣的層次,這些都有影響。

問:如何保證僅有的一個deamon的穩定性健壯性?

孫宏亮:這個問題首先需要知道docker daemon的穩定性在哪些方面,那種場景下比較差?的確,docker daemon存在弊病。比如,daemon和容器的耦合等,目前general來講,docker daemon保證絕對的穩定應該還做不到。

問:生產環境中怎么用docker備份mysql數據?

孫宏亮:數據存儲上docker,我目前的建議是:三思。舉個簡單的例子,官方的mysql鏡像運行出來的 容器,密碼是明文的,明文的密碼存在於:docker inspect container_name, container.json文件中,容器的環境變量中,甚至在日志文件中都會存在,just think about it。當然也有辦法解決,緩解這種情況。

問:如果是多層構建,中間的一個層做了升級或者bugfix,會潛在影響上層吧?

孫宏亮:這個bugfix會在上層有體現,但是使用效果是不會有影響的,還有之前的bug會永遠留在下層,但是沒有影響。

///********************************************************************************************************///

一、base鏡像

base 鏡像簡單來說就是不依賴其他任何鏡像,完全從0開始建起,其他鏡像都是建立在他的之上,可以比喻為大樓的地基,docker鏡像的鼻祖。

base 鏡像有兩層含義:(1)不依賴其他鏡像,從 scratch 構建;(2)其他鏡像可以之為基礎進行擴展。

所以,能稱作 base 鏡像的通常都是各種 Linux 發行版的 Docker 鏡像,比如 Ubuntu, Debian, CentOS 等。

我們以 CentOS 為例查看 base 鏡像包含哪些內容。

下載及查看鏡像:


   
   
   
           
  1. root@ubuntu:~ # docker pull centos
  2. Using default tag: latest
  3. latest: Pulling from library/centos
  4. d9aaf4d82f24: Pull complete
  5. Digest: sha256: 4565fe2dd7f4770e825d4bd9c761a81b26e49cc9e3c9631c58cfc3188be9505a
  6. Status: Downloaded newer image for centos:latest

   
   
   
           
  1. root@ubuntu :~ # docker images
  2. REPOSITORY TAG IMAGE ID CREATED SIZE
  3. centos latest d123f4e55e12 3 weeks ago 197MB
  4. ubuntu 12.04 5b117edd0b76 7 months ago 104MB

我們看到CentOS的鏡像大小不到200MB,平時我們安裝一個CentOS至少是幾個GB,怎么可能才 200MB !

下面我們來解釋這個問題,Linux 操作系統由內核空間和用戶空間組成。

典型的Linux啟動到運行需要兩個FS,bootfs + rootfs,如下圖:

Docker鏡像的內部結構(四)

1、rootfs

內核空間是 kernel,Linux 剛啟動時會加載 bootfs 文件系統,之后 bootfs 會被卸載掉。用戶空間的文件系統是 rootfs,包含我們熟悉的 /dev, /proc, /bin 等目錄。

對於 base 鏡像來說,底層直接用 Host 的 kernel,自己只需要提供 rootfs 就行了。

而對於一個精簡的 OS,rootfs 可以很小,只需要包括最基本的命令、工具和程序庫就可以了。相比其他 Linux 發行版,CentOS 的 rootfs 已經算臃腫的了,alpine 還不到 10MB。

我們平時安裝的 CentOS 除了 rootfs 還會選裝很多軟件、服務、圖形桌面等,需要好幾個 GB 就不足為奇了。

2、base 鏡像提供的是最小安裝的 Linux 發行版。

下面是 CentOS 鏡像的 Dockerfile 的內容:

Docker鏡像的內部結構(四)


   
   
   
           
  1. FROM scratch
  2. ADD centos - 7-docker.tar.xz /
  3. LABEL name= "CentOS Base Image" </div>
  4. vendor= "CentOS" </div>
  5. license= "GPLv2" </div>
  6. build- date= "20170911"
  7. CMD [ "/bin/bash"]

第二行 ADD 指令添加到鏡像的 tar 包就是 CentOS 7 的 rootfs。在制作鏡像時,這個 tar 包會自動解壓到 / 目錄下,生成 /dev, /porc, /bin 等目錄。

3、支持運行多種 Linux OS

bootfs (boot file system) 主要包含 bootloader 和 kernel, bootloader主要是引導加載kernel, 當boot成功后 kernel 被加載到內存中后 bootfs就被umount了.

rootfs (root file system) 包含的就是典型 Linux 系統中的 /dev, /proc, /bin, /etc 等標准目錄和文件。

由此可見對於不同的linux發行版, bootfs基本是一致的, rootfs會有差別, 因此不同的發行版可以公用bootfs。

比如 Ubuntu 14.04 使用 upstart 管理服務,apt 管理軟件包;而 CentOS 7 使用 systemd 和 yum。這些都是用戶空間上的區別,Linux kernel 差別不大。

所以 Docker 可以同時支持多種 Linux鏡像,模擬出多種操作系統環境。

Docker鏡像的內部結構(四)
上圖 Debian 和 BusyBox(一種嵌入式 Linux)上層提供各自的 rootfs,底層共用 Docker Host 的 kernel。

這里需要說明的是:

base 鏡像只是在用戶空間與發行版一致,kernel 版本與發型版是不同的。

CentOS 7 使用 3.x.x 的 kernel,如果 Docker Host 是 Ubuntu 16.04,那么在 CentOS 容器中使用的實際是是 Host 4.x.x 的 kernel。


   
   
   
           
  1. root@ubuntu :~ # uname -r
  2. 4.4. 0- 62-generic
  3. root@ubuntu :~ # docker run -ti centos /bin/bash
  4. [root@06f13ef13853 /] # uname -r
  5. 4.4. 0- 62-generic

容器只能使用 Host 的 kernel,並且不能修改。

所有容器都共用 host 的 kernel,在容器中沒辦法對 kernel 升級。如果容器對 kernel 版本有要求(比如應用只能在某個 kernel 版本下運行),則不建議用容器,這種場景虛擬機可能更合適。

二、鏡像的分層結構

Docker 支持通過擴展現有鏡像,創建新的鏡像。

實際上,Docker Hub 中 99% 的鏡像都是通過在 base 鏡像中安裝和配置需要的軟件構建出來的。比如我們現在構建一個新的鏡像,Dockerfile 如下:


   
   
   
           
  1. # Version: 0.0.1
  2. FROM debian 1.新鏡像不再是從 scratch 開始,而是直接在 Debian base 鏡像上構建。
  3. MAINTAINER wzlinux
  4. RUN apt- get update && apt- get install -y emacs 2.安裝 emacs 編輯器。
  5. RUN apt- get install -y apache2 3.安裝 apache2。
  6. CMD [ "/bin/bash"] 4.容器啟動時運行 bash。

構建過程如下圖所示:

Docker鏡像的內部結構(四)
可以看到,新鏡像是從 base 鏡像一層一層疊加生成的。每安裝一個軟件,就在現有鏡像的基礎上增加一層。

問什么 Docker 鏡像要采用這種分層結構呢?

最大的一個好處就是 - 共享資源。

比如:有多個鏡像都從相同的 base 鏡像構建而來,那么 Docker Host 只需在磁盤上保存一份 base 鏡像;同時內存中也只需加載一份 base 鏡像,就可以為所有容器服務了。而且鏡像的每一層都可以被共享,我們將在后面更深入地討論這個特性。

這時可能就有人會問了:如果多個容器共享一份基礎鏡像,當某個容器修改了基礎鏡像的內容,比如 /etc 下的文件,這時其他容器的 /etc 是否也會被修改?

答案是不會!
修改會被限制在單個容器內。
這就是我們接下來要說的容器 Copy-on-Write 特性。

  1. 新數據會直接存放在最上面的容器層。
  2. 修改現有數據會先從鏡像層將數據復制到容器層,修改后的數據直接保存在容器層中,鏡像層保持不變。
  3. 如果多個層中有命名相同的文件,用戶只能看到最上面那層中的文件。

可寫的容器層

當容器啟動時,一個新的可寫層被加載到鏡像的頂部。這一層通常被稱作“容器層”,“容器層”之下的都叫“鏡像層”。

典型的Linux在啟動后,首先將 rootfs 置為 readonly, 進行一系列檢查, 然后將其切換為 “readwrite” 供用戶使用。在docker中,起初也是將 rootfs 以readonly方式加載並檢查,然而接下來利用 union mount 的將一個 readwrite 文件系統掛載在 readonly 的rootfs之上,並且允許再次將下層的 file system設定為readonly 並且向上疊加, 這樣一組readonly和一個writeable的結構構成一個container的運行目錄, 每一個被稱作一個Layer。如下圖所示。

Docker鏡像的內部結構(四)
所有對容器的改動,無論添加、刪除、還是修改文件都只會發生在容器層中。只有容器層是可寫的,容器層下面的所有鏡像層都是只讀的。

下面我們深入討論容器層的細節。

鏡像層數量可能會很多,所有鏡像層會聯合在一起組成一個統一的文件系統。如果不同層中有一個相同路徑的文件,比如 /a,上層的 /a 會覆蓋下層的 /a,也就是說用戶只能訪問到上層中的文件 /a。在容器層中,用戶看到的是一個疊加之后的文件系統。

  1. 添加文件:在容器中創建文件時,新文件被添加到容器層中。
  2. 讀取文件:在容器中讀取某個文件時,Docker 會從上往下依次在各鏡像層中查找此文件。一旦找到,立即將其復制到容器層,然后打開並讀入內存。
  3. 修改文件:在容器中修改已存在的文件時,Docker 會從上往下依次在各鏡像層中查找此文件。一旦找到,立即將其復制到容器層,然后修改之。
  4. 刪除文件:在容器中刪除文件時,Docker 也是從上往下依次在鏡像層中查找此文件。找到后,會在容器層中記錄下此刪除操作。

只有當需要修改時才復制一份數據,這種特性被稱作 Copy-on-Write。可見,容器層保存的是鏡像變化的部分,不會對鏡像本身進行任何修改。

這樣就解釋了我們前面提出的問題:容器層記錄對鏡像的修改,所有鏡像層都是只讀的,不會被容器修改,所以鏡像可以被多個容器共享。

參考文檔:<http://developer.51cto.com/art/201312/424374_all.htm>;
<https://docs.docker.com/engine/userguide/storagedriver/imagesandcontainers/>;




免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM