天天看點

【Docker 那些事兒】容器為什麼傲嬌?全靠鏡像撐腰(上)

🌟 前言

Docker 鏡像是 Docker 容器的基石,容器是鏡像的運作執行個體,有了鏡像才能啟動容器。

Docker 鏡像是一個隻讀的模闆,一個獨立的檔案系統,包括運作一個容器所需的資料,可以用來建立容器。

1. base鏡像

base(基礎) 鏡像是指完全從零開始建構的鏡像, 它不會依賴其他鏡像,甚至會成為被依賴的鏡像,其他鏡像以它為基礎進行擴充。

通常 base 鏡像都是 Linux 的系統鏡像, 如 Ubuntu、CentOS、Debian 等。

下面通過 Docker 拉取一個 base 鏡像并檢視, 這裡以 CentOS 為例, 示例代碼如下:

【Docker 那些事兒】容器為什麼傲嬌?全靠鏡像撐腰(上)

從以上示例中可以看出,一個 CentOS 鏡像大小隻有 202MB,但在安裝系統時,一個 CentOS 大概有幾 GB,這與作業系統有關。

先觀察 Linux 原本的作業系統結構,如圖所示👇

【Docker 那些事兒】容器為什麼傲嬌?全靠鏡像撐腰(上)

Kernel 是核心空間。bootfs 檔案系統在 Linux 啟動時加載。rootfs 是包含操作指令的檔案系統。

base 鏡像的建立過程中,Kernel、 bootfs 與 rootfs 都會加載,然後 bootfs 檔案系統 (包括 Kernel) 被解除安裝掉,鏡像隻保留 rootfs 檔案系統,供使用者進行操作。bootfs 與 Kernel 将與主控端共享。

另外,為了增加 Docker 的靈活性,base 鏡像提供的都是最小安裝的 Linux 系統。

Linux 系統不同的發行版之間最大的差別就是 rootfs 的不同,例如,Ubuntu 系統的應用程式管理器是 apt,而 CentOS 是 yum。

由此可見,隻要提供不同的 rootfs 檔案系統就可以同時支援多種作業系統,如圖所示👇

【Docker 那些事兒】容器為什麼傲嬌?全靠鏡像撐腰(上)

從上圖中可以看到,兩個不同的 Linux 發行版提供了各自的 rootfs 檔案系統,而它們共用的是底層主控端的 Kernel。

假設主控端的系統是 Ubuntu 16.04,Kernel 版本是 4.4.0,無論 base 鏡像原本的發行版 Kernel 版本如何,在這台主控端上都是 4.4.0。

下面通過示例來驗證,示例代碼如下:

【Docker 那些事兒】容器為什麼傲嬌?全靠鏡像撐腰(上)

從上述示例中可以看出,base 鏡像與主控端的 Kernel 版本都是 3.10。

base 鏡像的 Kernel 是與主控端共享的,其版本與主控端一緻,并且不能進行修改。

2. 鏡像的本質

Docker 鏡像是一個隻讀的檔案系統,由一層一層的檔案系統組成,每一層僅鏡像的本質包含前一層的差異部分,這種層級檔案系統被稱為 UnionFS。

大多數 Docker 鏡像都在 base 鏡像的基礎上進行建立,每進行一次新的建立就會在鏡像上建構一個新的 UnionFS。

檢視 ubuntu:15.04 鏡像的層級結構,示例代碼如下:

【Docker 那些事兒】容器為什麼傲嬌?全靠鏡像撐腰(上)

通常,對 Docker 的操作指令都是以 “docker” 開頭。pull 是下載下傳鏡像的指令,在英文中是 “拉” 的意思,是以下載下傳鏡像又叫作 拉取鏡像。

以上示例中,第 5 行到第 8 行是每一層 UnionFS 的 ID 号,第 9 行是整個鏡像的 ID 号,這個 ID 号可以用來操控鏡像。

然後,檢視鏡像,示例代碼如下:

【Docker 那些事兒】容器為什麼傲嬌?全靠鏡像撐腰(上)
在以上示例中,不僅可以看到先前下載下傳的 Ubuntu15.04 鏡像,還可以看到其他鏡像,說明

docker images

是檢視本地所有鏡像的指令。而檢視到的資訊中,除了鏡像名稱,還有版本号、鏡像 ID 号、建立時間以及鏡像大小。

接着,通過指令檢視鏡像的建構過程,示例代碼如下:

【Docker 那些事兒】容器為什麼傲嬌?全靠鏡像撐腰(上)

這裡使用 “history” 與鏡像 ID 号組合的指令檢視鏡像建構過程,所顯示的資訊包括鏡像 ID 号、建立時間、由什麼指令建立以及鏡像大小。

從以上示例中的資訊可以看出,ubuntu:15.04 鏡像由四個隻讀層 (Read Layer) 建構而成,每一層都是由一條指令構成的,最終得到 ID 号為 dlb55fd07600 的鏡像,但以使用者的視角隻能看到最上層。

當使用者将這個鏡像放在容器中運作時,四層之上會建立出一個可讀可寫層(Read-Write Layer),使用者對 Docker 的操作都通過可讀可寫層進行。如果使用者修改了一個已存在的檔案,那該檔案将會從可讀可寫層下的隻讀層複制到可讀可寫層,該檔案的隻讀版本仍然存在,隻是已經被可讀可寫層中該檔案的副本所隐藏。

可讀可寫層又叫作容器層,隻讀層又叫作鏡像層,容器層之下均為鏡像層,層級結構如圖所示👇

【Docker 那些事兒】容器為什麼傲嬌?全靠鏡像撐腰(上)

鏡像的這種分層機制最大的一個好處就是:共享資源。

例如,有很多個鏡像都基于一個基礎鏡像建構而來,那麼在本地的倉庫中就隻需要儲存一份基礎鏡像,所有需要此基礎鏡像的容器都可以共享它,而且鏡像的每一層都可以被共享,進而節省磁盤空間。

因為有了分層機制,本地儲存的基礎鏡像都是隻讀的檔案系統,不用擔心對容器的操作會對鏡像有什麼影響。

為了将零星的資料整合起來,人們提出了鏡像層 (Image Layer) 這個概念,如圖所示👇

下圖所示為一個鏡像層,我們能夠發現,一個層并不僅僅包含檔案系統的改變,它還能包含其他重要的資訊。

【Docker 那些事兒】容器為什麼傲嬌?全靠鏡像撐腰(上)
中繼資料 (Metadata) 就是關于這個層的額外資訊,包括 Docker 運作時的資訊與父鏡像層的資訊,并且隻讀層與可讀可寫層都包含中繼資料,如圖所示👇
【Docker 那些事兒】容器為什麼傲嬌?全靠鏡像撐腰(上)
除此之外,每一層還有一個指向父鏡像層的指針。如果沒有這個指針,說明它處于最底層,是一個基礎鏡像,如圖所示👇
【Docker 那些事兒】容器為什麼傲嬌?全靠鏡像撐腰(上)

3. 查找本地鏡像

Docker 本地鏡像通常是儲存在伺服器上的,下面驗證本地鏡像的儲存路徑,示例代碼如下:
【Docker 那些事兒】容器為什麼傲嬌?全靠鏡像撐腰(上)
從以上示例中可以看到,Docker 本地鏡像儲存路徑是

/var/Iib/Docker

在本地檢視鏡像時,通常使用

docker images

指令,示例代碼如下:
【Docker 那些事兒】容器為什麼傲嬌?全靠鏡像撐腰(上)
從以上示例中可以看到,結果顯示中有多項鏡像資訊,下面對資訊進行解釋。

🍇 REPOSITORY

鏡像倉庫,即一些關聯鏡像的集合。

例如,Ubuntu 的每個鏡像對應着不同的版本。與 Docker Registry 不同,鏡像倉庫提供 Docker 鏡像的存儲服務。

即 Docker Registry 中有很多鏡像倉庫,鏡像倉庫中有很多鏡像 (互相獨立)。

🍇 TAG

鏡像的标簽,常用來區分不同的版本,預設标簽為 latest。

🍇 IMAGE ID

鏡像的ID号,鏡像的唯一辨別,常用于操作鏡像 (預設值隻列出前 12 位)。

🍇 CREATED

鏡像建立的時間。

🍇 SIZE

鏡像的大小。

🍇 參數用法

docker images

指令後加上不同的參數就形成了不同的查詢方式,導緻不同的查詢結果。

下面介紹各參數的含義以及用法。

  • -a

表示顯示所有本地鏡像,預設不顯示中間層鏡像,這是工作中經常使用到的參數,用來從本地鏡像中尋找符合生産條件的鏡像。

示例代碼如下:

【Docker 那些事兒】容器為什麼傲嬌?全靠鏡像撐腰(上)
  • -q

表示隻顯示本地所有鏡像 ID 号。

示例代碼如下:

【Docker 那些事兒】容器為什麼傲嬌?全靠鏡像撐腰(上)
  • -no-trunc

表示使用不截斷的模式顯示,并顯示完整的鏡像 ID 号。

示例代碼如下:

【Docker 那些事兒】容器為什麼傲嬌?全靠鏡像撐腰(上)

4. 建構鏡像

Docker 的官方鏡像庫 Docker Hub 釋出了成千上萬的公共鏡像供全球使用者使用。使用者可以直接拉取(下載下傳)所需要的鏡像,提高了工作效率。但是在很多工作環境中,一旦對鏡像有特殊需求,就需要我們手動去建構鏡像。

本文章将會介紹基于

docker commit

指令與 Dockerfile 兩種方式來建構自己的 Docker 鏡像。

🍑 使用 docker commit 指令建構鏡像

使用

docker commit

指令将容器的可讀可寫層轉換為一個隻讀層,這樣就把一個容器轉換成了一個不可變的鏡像,如圖所示👇
【Docker 那些事兒】容器為什麼傲嬌?全靠鏡像撐腰(上)

下面我們給一個 Centos 的鏡像安裝一個 Vim 服務,設定開機啟動,并将其建構成一個新的鏡像,以免每次啟動容器都要再次安裝 Vim。

首先啟動一個 Centos 的容器,示例代碼如下:

【Docker 那些事兒】容器為什麼傲嬌?全靠鏡像撐腰(上)

從以上示例中可以看到,容器啟動之後,主機名發生了改變,說明使用者直接進入了容器,再進行操作就是對容器的操作。

然後,在容器中安裝 Vim,示例代碼如下:

【Docker 那些事兒】容器為什麼傲嬌?全靠鏡像撐腰(上)
安裝完成之後,退出容器,示例代碼如下:
【Docker 那些事兒】容器為什麼傲嬌?全靠鏡像撐腰(上)

使用 exit 指令退出容器之後, 該容器将預設關閉。

下面使用

docker commit

指令在 CentOS 鏡像的基礎上建立新的鏡像,示例代碼如下:
【Docker 那些事兒】容器為什麼傲嬌?全靠鏡像撐腰(上)

在指令中需要用鏡像 ID 号來指定基礎鏡像,并不需要将 ID 号都輸入進去,隻要輸入幾個字元使 ID 号與其他鏡像不沖突即可。

此時可以看到剛剛建構的新鏡像,代碼如下:

【Docker 那些事兒】容器為什麼傲嬌?全靠鏡像撐腰(上)

從以上示例中可以看到, 新鏡像的大小是 326MB,而此前的 CentOS 鏡像隻有 202MB, 這是因為在安裝 Vim 時還安裝了許多依賴包。

然後, 檢視鏡像中是否已經自動安裝了 Vim, 示例代碼如下:

【Docker 那些事兒】容器為什麼傲嬌?全靠鏡像撐腰(上)

從以上示例中可以看到,新鏡像已經包含了 Vim。

這種建構新鏡像的方式在工作中并不常見,原因如下。

(1)效率低下,如果要給 Ubuntu 鏡像也添加一個Vim,需要将上述全部過程重複一遍。

(2)不透明,使用者使用時不知道鏡像是如何建構的,難以對鏡像做出正确的判斷。

🍑 使用 Dockerfile 建構鏡像

鏡像可以基于 Dockerfile 建構。Dockerfile 是一個描述檔案,包含若幹條指令,每條指令都會為基礎檔案系統建立新的層次結構,這正好彌補了

docker commit

建構鏡像效率低下的缺點。

Dockerfile 定義容器内部環境中發生的事情。網絡接口和磁盤驅動器等資源的通路在此環境内虛拟化,與系統的其餘部分隔離。

Dockerfile 主要使用

docker build

指令,根據 Dockerfile 檔案中的指令,執行若幹次

docker commit

指令建構鏡像,每次執行

docker commit

指令時都會生成一個新的層,是以許多新的層會被建立,如圖所示👇
【Docker 那些事兒】容器為什麼傲嬌?全靠鏡像撐腰(上)

🍑 Dockerfile常用指令

下面介紹 Dockerfile 中常用的指令,完整說明見官方文檔。
  • FROM
指定源鏡像,必須是已經存在的鏡像,必須是Dockerfile中第一條非注釋的指令,因為其後的所有指令都使用該鏡像。
  • MAINTAINER
指定作者資訊。
  • RUN
在目前容器中運作指定的指令。
  • EXPOSE
指定運作容器時要使用的端口。可以使用多個EXPOSE指令。
  • CMD
指定容器啟動時運作的指令,Dockerfile 可以出現多個 CMD 指令,但隻有最後一個生效。CMD 可以被啟動容器時添加的指令覆寫。
  • ENTRYPOINT
CMD 或容器啟動時添加的指令會被當做參數傳遞給 ENTRYPOINT。
  • COPY
檔案或目錄複制到目前容器中。
  • ADD
将檔案或者目錄複制到目前容器中,源檔案如果是歸檔(壓縮)檔案,則會被自動解壓到目标位置。
  • VOLUME
為容器添加容器卷,可以存在于一個或多個目錄,用來提供共享存儲。該指令會在容器資料卷部分詳細介紹。
  • WORKDIR
在容器内設定工作目錄。
  • ENV
設定環境變量。
  • USER
指定容器以什麼使用者身份運作,預設是 root。

🍑 運作一個Dockerfile

下面示範使用 Docker file 建立

centos/vim

,示例代碼如下:
【Docker 那些事兒】容器為什麼傲嬌?全靠鏡像撐腰(上)

這裡在主控端的 root 目錄下建立了一個 Dockerfile 檔案。

接着,向 Docker file 檔案中添加内容, 示例代碼如下:

【Docker 那些事兒】容器為什麼傲嬌?全靠鏡像撐腰(上)

添加完成之後,儲存并退出。

有了 Dockerfile 檔案之後即可建立新的鏡像,示例代碼如下:

【Docker 那些事兒】容器為什麼傲嬌?全靠鏡像撐腰(上)
通過

docker build

指令執行 Dockerfile 檔案,-t 用來指定新鏡像名為

centos/vim-Dockerfile

,指令行末尾的

.

表示 Dockerfile 檔案在目前目錄,Docker 預設從指定的目錄尋找 Dockerfile 檔案,也可以使用 -f 參數指定 Dockerfile 檔案的位置。
【Docker 那些事兒】容器為什麼傲嬌?全靠鏡像撐腰(上)
建構完成之後,檢視鏡像是否建構成功,示例代碼如下:
【Docker 那些事兒】容器為什麼傲嬌?全靠鏡像撐腰(上)

從以上示例中可以看到,新鏡像已經建構成功。

使用 Dockerfile 建構鏡像基本可以分為以下五步。

(1) 選擇一個基礎鏡像,運作一個臨時容器。

(2) 執行一條指令,對容器做修改。

(3) 執行類似

docker commit

的操作,生成一個新的鏡像。

(4) 删除臨時容器,再基于剛剛建構好的新鏡像運作一個臨時容器。

(5) 重複 (2) (3) (4) 步,直到執行完 Dockerfile 中的所有指令。

centos/vim-Dockerfile

由 CentOS 基礎鏡像和

RUN yum -y install vim

構成,現在兩個鏡像都包含了 ID 号為 lel148e4cc2c 的隻讀層,如圖所示👇
【Docker 那些事兒】容器為什麼傲嬌?全靠鏡像撐腰(上)
以上結論可以使用

docker history

指令驗證,

docker history

指令專門用來檢視鏡像的結構,示例代碼如下:
【Docker 那些事兒】容器為什麼傲嬌?全靠鏡像撐腰(上)

這裡可以看到 CentOS 鏡像中确實包含了 ID 号為 1e1148e4cc2c 的隻讀層。

接着再檢視新鏡像

centos/vim-Dockerfile

的結構,示例代碼如下:
【Docker 那些事兒】容器為什麼傲嬌?全靠鏡像撐腰(上)

從以上示例中可以看到,兩個鏡像都含有一個相同的隻讀層,并且這個隻讀層是共享的。

Docker 建構鏡像時有緩存機制,如果建構鏡像層時該鏡像層已經存在,就直接使用,無須重新建構。

下面為先前的 Dockerfile 檔案添加一點内容,安裝一個 ntp 服務,重新建構一個新的鏡像,示例代碼如下:

【Docker 那些事兒】容器為什麼傲嬌?全靠鏡像撐腰(上)

這裡多加了一條安裝 ntp 服務的指令。

添加完成後,開始建立鏡像,示例代碼如下:

【Docker 那些事兒】容器為什麼傲嬌?全靠鏡像撐腰(上)

在示例的第 6 行代碼中可以看到,Docker 沒有重新安裝 Vim,而是直接使用了先前安裝過的緩存。

Dockerfile 檔案是從上至下依次執行的,上層依賴于下層。無論什麼時候,隻要某一層發生變化,其上面所有層的緩存都會失效。

改變先前的 Dockerfile 檔案中兩條 RUN 指令的上下順序,觀察 Docker 還會不會使用緩存機制,示例代碼如下:

【Docker 那些事兒】容器為什麼傲嬌?全靠鏡像撐腰(上)
将 Dockerfile 中兩條 RUN 指令的順序互換之後,開始建立鏡像,示例代碼如下:
【Docker 那些事兒】容器為什麼傲嬌?全靠鏡像撐腰(上)

由以上驗證可知,将兩條 RUN 指令交換順序導緻鏡像層次發生改變,Docker 會重建鏡像層。由此可見 Docker 的鏡像層級結構特性:隻有下面的層次内容、順序完全一緻才會使用緩存機制。

如果在建構鏡像時不想使用緩存,可以在

docker build

指令中添加

--no-cache

參數,否則預設使用緩存。

除了在使用 Dockerfile 建構鏡像時有緩存機制,在從倉庫拉取鏡像時也會有緩存機制,即已經拉取到本地的鏡像層可以被多個鏡像共同使用,可以說是一次拉取多次使用,前提是下層鏡像完全相同。

通常使用 Dockerfile 建構鏡像時,如果由于某些原因鏡像建構失敗,我們能夠得到前一個指令成功執行建構出的鏡像,繼而可以運作這個鏡像查找指令失敗的原因,這對調試 Dockerfile 有極大的幫助。

從 Docker Hub 拉取的 CentOS 鏡像是最小化的,其中沒有 vim 指令。下面測試錯誤建構 Docker 鏡像的結果,示例代碼如下:

【Docker 那些事兒】容器為什麼傲嬌?全靠鏡像撐腰(上)
将 Dockerfile 中任意一條 RUN 指令改為錯誤的,再開始建立鏡像,示例代碼如下:
【Docker 那些事兒】容器為什麼傲嬌?全靠鏡像撐腰(上)
在示例中,由于第三步報錯,鏡像沒有建立成功。但也生成了一個新鏡像,這個鏡像是第二步操作建構的,通常可以通過這個新鏡像排查錯誤,示例代碼如下:
【Docker 那些事兒】容器為什麼傲嬌?全靠鏡像撐腰(上)
Docker 容器技術中,編寫 Dockerfile 檔案是非常重要的部分,下面總結編寫 Dockerfile 檔案的一些小技巧,相信可以幫助大家更好地使用 Docker 與 Dockerfile。
  • (1)容器中隻運作單個應用。

從技術角度講,在一個容器中可以實作整個 LNMP (Linux+Nginx+MySQL+PHP) 架構。但這樣做有很大的弊端。首先,鏡像建構的時間會非常長,每次修改都要重新建構;

其次,鏡像檔案會非常大,大大降低容器的靈活性。

  • (2)将多個 RUN 指令合并成一個。

衆所周知,Docker 鏡像是分層的,Dockerfile 中的每一條指令都會建立一個新的鏡像層,鏡像層是隻讀的。

Docker鏡像層類似于洋蔥,想要更改内層,需要将外層全部撕掉。

  • (3)基礎鏡像的标簽盡量不要使用 latest。

當鏡像的标簽沒有指定時,預設使用 latest 标簽。

當鏡像更新時,latest 标簽會指向不同的鏡像,可能會對服務産生影響。

  • (4)執行 RUN 指令後删除多餘檔案。
  • (5)合理調整 COPY 與 RUN 的順序。
  • (6)選擇合适的基礎鏡像。

繼續閱讀