Docker 鏡像知多少?

Docker 鏡像,已經是我們這些 IT 工程師工作中,不可或缺的一部分,可以說是我們工作的基礎,但是 docker 鏡像本質上,到底是什麼東西?我們生成一個 docker 鏡像到底做了什麼操作呢?

Dockerfile
生成 docker 鏡像

如想要生成一個我們自己的 docker 鏡像,可以先編寫自己的 dockerfile 文件,然後基於此文件使用 docker build 生成鏡像,那這個過程中到底發生了什麼呢?

發送 Build context

在執行 docker build 命令時,會在末尾加上一個 “.”,這個點就是 docker 的構建上下文,在 linux 下 “.” 即代表當前目錄;docker 構建鏡像需要使用到構建上下文裏的文件,所以需要將 build context 下的文件遍歷發送給 docker 守護進程,這樣我們就可以在構建開始的日誌信息中,看到如下信息:

Sending build context to Docker daemon  xxx.xx MB 

這條信息是在告知需要發送給 docker daemon 的文件有多少 MB 大小。

校驗 dockerfile 命令並執行

Docker daemon 在執行 dockerfile 的命令之前都會先預校驗一下命令是不是符合語法,不符合的將返回一個錯誤;命令都沒問題後 docker daemon 會逐條執行命令,執行的過程如下:在原來的鏡像上啓動一個容器,在容器內執行命令,執行完寫操作,然後 docker daemon 執行一次 commit,提交一個新的鏡像,這裏就產生了新的一層鏡像,緊接着繼續執行下面的命令,直至執行完成生成鏡像。

Docker
鏡像的結構

按照 OCI (Open Container Initiative) 規範中的容器鏡像標準,Docker 鏡像其實本質是文件目錄,包括 index 索引文件 (可選) 、配置文件、清單文件、一組文件系統層。配置文件裏包含環境變量、掛載卷、暴露的端口等;清單文件中列出了構成鏡像的層的信息。我們可以通過 docker manifest inspect 命令來查看鏡像的配置文件、清單文件裏的信息。

下面我們就來詳細看一看:

(1) index 索引文件

以下是查看一個 openjdk 鏡像元數據信息的示例:

docker manifest inspect openjdk:8-alpine  

以上 JSON 文件就是一個鏡像的 index 索引文件,這個文件的作用是標記不同的平臺該使用哪個鏡像 manifest 文件,其中的 digest 字段就是 openjdk 鏡像的 manifest 文件 ID,我們可以根據這個文件 ID (文件指紋) 來查看 manifest 文件信息。

(2) 清單文件

以下是查看清單文件的示例:

docker manifest inspect openjdk@sha256:44b3cea369c947527e266275cee85c71a81f20fc5076f6ebb5a13f19015dce71   

從上圖可以看出 manifest 文件中由配置文件和很多的層信息組成,每個層信息包含類型、文件 ID (文件指紋) 、大小信息。拉取鏡像時先獲得 manifest 文件,再根據此文件中的元數據信息,去獲取相應的配置文件、層文件,要注意的是獲取層文件時,會對比文件指紋。但如果本地已存在層文件,就不需要再拉取了,直接使用本地緩存,推送鏡像時也是如此,如果在 docker registry 中已存在了層數據,不需再次推送,只會推送改變了的層。

(3) 配置文件

配置文件的位置在以下目錄:

/var/lib/docker/image/overlay2/imagedb/content/sha256 

而 manifest 文件中的 config.digest 字段就是配置文件的名稱,查看文件的內容:

配置文件中配置了容器運行時,需要的環境變量、入口命令、掛載卷等信息。

至此,我們已經瞭解了 docker 鏡像的分層結構,那分層結構有哪些好處呢?主要好處在於資源可共享、可複用,當多個鏡像間存在相同的層時可直接複用,無需再拉取,極大地節約了存儲、網絡帶寬資源

Dockerfile
最佳實踐

通過以上內容,我們可以知道,一個 docker 鏡像產生和 dockerfile 裏的命令密不可分,因此 dockerfile 不同的寫法會影響到鏡像的大小、構建的速度等。Docker 官方有 dockerfile 編寫的最佳實踐 (網址如下)。

https://docs.docker.com/develop/develop-images/dockerfile_best-practices/

它建議從以下幾點優化:

以上是一些常用的優化配置,更多詳細的配置可查詢 docker 官網上的 dockerfile 最佳實踐。

如何提高
構建 docker 鏡像的速度

到此,我們已梳理了一個 docker 鏡像產生的過程、一個 docker 鏡像的結構、以及如何編寫一個較好的 dockerfile。那如何提高 docker 鏡像構建的速度呢,我們可以從以下幾點出發:

1. 構建上下文

構建上下文是我們構建鏡像的基礎,最好只包含 docker build 構建過程需要的資源,這裏可以參考 dockerfile 的最佳實踐做出優化。

2. Base image

在構建鏡像過程中,我們自己的程序其實是很小的,但程序運行的環境佔了很大的空間,所以我們可以選擇比較小巧的底包,這樣可以大大減小鏡像的大小,從而縮短鏡像構建過程中拉取和推送的時間;我們也可以選擇更小巧的 Linux 發行版,比如 Alpine,或者 Google 的 Distroless 鏡像。

**Alpine **是一個小巧、安全、簡單、功能完備的 linux 發行版,大小隻有幾 M,非常適合用於製作鏡像,現在很多官方鏡像都已經有 Alpine 的版本了,使用 Alpine 版本的 base image 可以極大減小構建出來的鏡像大小。

**Distroless **是 Google 的一個鏡像構建文件,專門在安全漏洞方面做了優化,只包含應用程序及其運行時所需的依賴,不包含軟件包管理器、shell 和其他 GNU 二進制文件這些幾乎用不到的功能,大大降低了被攻擊的風險,並減少了漏洞,所以 Distroless 較 Alpine 更加的安全,不過 gcr.io 對國內用戶稍微不是很友好,鏡像拉取不了。

3. 其他優秀的鏡像分層技術

鏡像的結構如果是滿足 OCI 的標準規範的話,就可以在 OCI 的運行時中運行;換句話說只要我們能構建出滿足 OCI 標準的鏡像文件目錄,就是一個標準的 docker 鏡像;現在也有了很多優秀的鏡像分層技術,他們滿足 OCI 標準,並且解決了 docker 的一些缺點;合理的分層,可以使構建過程使用上大量的緩存,無需重複拉取,從而加快鏡像的構建,下面我們看看一些比較流行的鏡像分層技術:

Podman

Podman 提供與 Docker 非常相似的功能。可以說 podman 就是爲了替代 docker 的,podman 解決 docker 的一些痛點,比如 docker daemon 是一個守護進程、並需要 root 權限,但 podman 它不需要在系統上運行任何守護進程,並且還可以在沒有 root 權限的情況下運行。

Podman 可以管理和運行任何符合 OCI 規範的容器和容器鏡像;podman 的命令和 docker 的命令,基本上是相同的,只需要將 docker 換爲 podman,即可兼容 docker 的基本常用命令,podman 也可以根據用戶提供的 dockerfile 文件構建鏡像,不過一般不推薦使用 podman build 構建鏡像,因爲 podman 構建速度超慢,並且默認情況下使用 vfs 存儲驅動程序會耗盡大量磁盤空間,一般使用 podman 的構建工具 Buildah。

Buildah

Buildah 是一個專注於構建 OCI 容器鏡像的工具,Buildah 構建速度非常快並使用覆蓋存儲驅動程序,可以節約大量的空間。

Buildah 也支持使用 dockerfile 構建鏡像:

Buildah 使用 dockerfile 構建時是在構建的最後一步進行的 commit,這樣構建的鏡像就只有一層,無法使用到緩存,也就是要做一些重複的拉取工作;如果使用 buildah 的原生命令構建鏡像的話,分層會變得更加的靈活,我們可以自定義緩存點,在我們認爲需要緩存的地方加上 commit 命令就能提交一層新的鏡像。Buildah 的原生命令就是一個 bash 腳本,下面展示了 buildah 構建的一個簡單腳本:

當不使用 Dockerfile 而是使用 Buildah 命令構建鏡像時,我們可以使用 commit 命令來隨時決定提交緩存。在上例中,所有的變更是一起提交的;但其實可以增加中間提交,這樣就能自由標記 緩存點 (cache point):例如,我們可以在安裝完一些構建需要的工具後就提交一次,這樣下一次構建可以直接使用這個緩存。

Buildah mount 命令可以將容器的根目錄掛載到主機的一個掛載點上;這使得我們可以使用主機上的工具進行構建和安裝軟件,不用將這些構建工具打包到容器鏡像本身中。

Google 的 jib 構建工具

Jib 是 Google 的一個 java 構建鏡像的工具,將原來一層 docekr 鏡像拆分得更細,在原來我們 COPY springboot 項目的 jar 文件構建成一個 java 的鏡像,但是 springboot 的 jar 壓縮文件中,有很多的文件在我們每次編譯時,基本上是不會改變的,比如說第三方的依賴 jar、以及資源文件夾、配置文件等,改變代碼後編譯出的文件中只有 class 文件是不一樣的,所以 jib 將 springboot 項目分爲三層,分別爲第三方依賴 lib、資源文件 resources、字節碼 class 文件,體現在 dockerfile 文件裏就如下:

前兩層在大多數情況下,都是可以複用的,僅僅需要構建最後一層即可,越是大型的項目越能體現出 jib 的優勢。

下面我們看一下它們的優缺點:

鏡像分層技術現在已經是很普遍的存在,除了上述提到的外,還有谷歌的 Kaniko、Buildkit、Source-To-Image (S2I)、Bazel 等,它們都有各自的一些特性,使用於一些特定的場景,我們可以視情況選擇使用。

項目中的實踐 - jib

目前「DaoCloud 道客」有些項目中已使用 jib 作爲 JAVA 項目的鏡像構建工具,明顯能感覺到對項目開展效率的提升:

**1. 更方便地構建、調試。**Jib 讓我們可以在僅有 JAVA 的環境下完成鏡像構建,可以在本地構建鏡像到遠程倉庫,而不觸發 CICD,這樣便於更快速地進行線上程序聯調。

**2. 配置更簡潔。**不需要寫 dockerfile,更不需要考慮 dockerfile 的最佳實踐,只需要簡單的幾行配置即可。

**3. 構建速度更快。**即使進行 CICD 構建,jib 對鏡像的特殊分層,也會讓構建過程中使用到更多的緩存,受低帶寬的影響也會更小。

總結:

得益於 OCI 規範的存在,只要構建出的鏡像遵守 OCI 規範,就可以交給遵守 OCI 規範的容器運行時去運行,這樣就使得容器技術的發展,更加多元化,我們也不必再拘泥於一款工具使用,可以按需選擇那些能提高我們工作效率的工具。

 本文作者 

歐桂勇

「DaoCloud 道客」後端開發工程師

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/GT5uKafg367YBFMU1-F80Q