Docker容器鏡像是怎樣煉成的?

Docker 包括三個基本概念:鏡像、容器、倉庫。理解了這三個概念,就理解了 Docker 的整個生命週期。今天 K8sMeetup 社區給大家帶來的是社區特邀作者木子的一篇深度技術文,主要介紹容器鏡像的系統知識。

作者:木子(才雲)來源:K8sMeetup 社區 /

上週我看了很多關於容器鏡像相關的博客,從大佬們那裏偷偷學了不少知識,對容器鏡像也有了一點點深入的瞭解。趁着這週末宅在家有空,我把最近所學的知識整理成文,供大家一起食用。文章內容比較長,不過如果讀者能有耐心看完,相信還是能收穫一些知識。

鏡像是怎樣煉成的

所謂煉成像就是構建鏡像。提到容器鏡像就不得不提一下 OCI ,即 Open Container Initiative,旨在圍繞容器格式和運行時制定一個開放的工業化標準。目前 OCI 主要有三個規範:運行時規範 runtime-spec ,鏡像規範 image-spec 以及不常見的鏡像倉庫規範 distribution-spec 。關於 OCI 這些規範的作用,這裏就引用容器開放接口規範(CRI OCI)中的內容:

制定容器格式標準的宗旨概括來說就是不受上層結構的綁定,如特定的客戶端、編排棧等,同時也不受特定的供應商或項目的綁定,即不限於某種特定操作系統、硬件、CPU 架構、公有云等。

這兩個協議通過 OCI runtime filesytem bundle 的標準格式連接在一起,OCI 鏡像可以通過工具轉換成 bundle,然後 OCI 容器引擎能夠識別這個 bundle 來運行容器。

其實 OCI 規範內容很容易理解,不像 RFC 和 ISO 那麼高深莫測,所以大家想對容器鏡像有更深入的瞭解還是非常推薦大家去閱讀有關文件😂。OCI 規範是免費的,不像大多數 ISO 規範需要付費才能閱讀。

OCI image-spec

OCI 規範中的鏡像規範 image-spec 決定了我們的鏡像按照什麼標準來構建,以及構建完鏡像之後如何存放。下文提到的 Dockerfile 則決定了鏡像的 layer 內容以及鏡像的一些元數據信息。一個鏡像規範 image-spec 和一個 Dockerfile 就指導着我們構建一個鏡像。那麼接下來我們就簡單瞭解一下鏡像規範,看看鏡像是長什麼樣子的,對鏡像有個大體的主觀認識。

根據官方文檔的描述,OCI 鏡像規範的主要由以下幾個 markdown 文件組成:

總結以上幾個 markdown 文件, OCI 容器鏡像規範主要包括以下幾塊內容:

layer

文件系統:以 layer (鏡像層)保存的文件系統,每個 layer 保存了和上層之間變化的部分,layer 應該保存哪些文件,怎麼表示增加、修改和刪除的文件等。

image config

image config 文件:保存了文件系統的層級信息(每個層級的 hash 值,以及歷史信息),以及容器運行時需要的一些信息(比如環境變量、工作目錄、命令參數、mount 列表),指定了鏡像在某個特定平臺和系統的配置,比較接近我們使用 docker inspect <image_id> 看到的內容。

manifest

manifest 文件 :鏡像的 config 文件索引。manifest 文件中保存了很多和當前平臺有關的信息,例如有哪些 layer 以及額外的 annotation 信息。另外 manifest 中的 layer 和 config 中的 layer 表達的雖然都是鏡像的 layer ,但二者代表的意義不太一樣,下面會講到。manifest 文件存放在 registry 中,當我們拉取鏡像的時候,會根據該文件拉取相應的 layer 。

另外 manifest 也分好幾個版本,目前主流的版本是 Manifest Version 2, Schema 2 ,可以參考 Docker 的官方文檔 Image Manifest Version 2, Schema 2 。registry 中有個 Manifest List 文件,該文件是爲不同處理器體系架構而設計的,通過該文件指向與該處理器體系架構相對應的 Image Manifest ,這一點不要搞混。

這裏再補充一段大佬的解釋:

Manifest 是一個 JSON 文件,其定義包括兩個部分,分別是 Config 和 Layers。Config 是一個 JSON 對象,Layers 是一個由 JSON 對象組成的數組。可以看到,Config 與 Layers 中的每一個對象的結構相同,都包括三個字段,分別是 digest、mediaType 和 size。其中 digest 可以理解爲是這一對象的 ID。mediaType 表明了這一內容的類型。size 是這一內容的大小。 

容器鏡像的 Config 有着固定的 mediaType application/vnd.oci.image.config.v1+json。一個 Config 的示例配置如下,它記錄了關於容器鏡像的配置,可以理解爲是鏡像的元數據。通常它會被鏡像倉庫用來在 UI 中展示信息,以及區分不同操作系統的構建等。 

而容器鏡像的 Layers 是由多層 mediaType 爲 application/vnd.oci.image.layer.v1.*(其中最常見的是 application/vnd.oci.image.layer.v1.tar+gzip) 的內容組成的。衆所周知,容器鏡像是分層構建的,每一層就對應着 Layers 中的一個對象。

容器鏡像的 Config,和 Layers 中的每一層,都是以 Blob 的方式存儲在鏡像倉庫中的,它們的 digest 作爲 Key 存在。因此,在請求到鏡像的 Manifest 後,Docker 會利用 digest 並行下載所有的 Blobs,其中就包括 Config 和所有的 Layers。

image manifest index

index 文件 :其實就是我們上面提到的 Manifest List。在 Docker 的 distribution 中稱之爲 Manifest List ,而在 OCI 中就叫 OCI Image Index Specification 。其實兩者指的是同一個文件,甚至兩者 GitHub 上文檔給的 example 都一模一樣🤣,應該是 OCI 複製粘貼 Docker 的文檔😂。index 文件是個可選的文件,包含着一個列表爲同一個鏡像不同的處理器 arch 指向不同平臺的 manifest 文件,這個文件能保證一個鏡像可以跨平臺使用,每個處理器 arch 平臺擁有不同的 manifest 文件,使用 index 作爲索引。當我們使用 arm 架構的處理器時要額外注意,在拉取鏡像的時候要拉取 arm 架構的鏡像,一般處理器的架構都接在鏡像的 tag 後面,默認 latest tag 的鏡像是 x86 的,在 arm 處理器的機器這些鏡像上是跑不起來的。

Dockerfile

衆所周知 Docker 鏡像需要一個 Dockerfile 來構建而成,當我們對 OCI 鏡像規範有了個大致的瞭解之後,我們接下來就拿着 Dockerfile 這個 ” 圖紙 “ 去一步步構建鏡像。本文不再細講 Dockerfile 的詳細書寫和技巧,網上有很多關於寫好 Dockerfile 的技巧。

下面就是 webp server go Dockerfile 的例子:

需要注意的是,在 RUN 指令的每行結尾我使用的是 ;\ 來接下一行 shell ,另一種寫法是 && 。二者有本質的區別,比如 COMMAND 1;COMMAND 2 ,當 COMMAND 1 運行失敗時會繼續運行 COMMAND2 ,並不會退出。而 COMMAND 1&& COMMAND 2,時 COMMAND 1 運行成功時才接着運行 COMMAND 2 ,COMMAND 1 運行失敗會退出。如果沒有十足的把握保證每一行 shell 都能每次運行成功建議用 && ,這樣失敗了就退出構建鏡像,不然構建出來的鏡像會有問題。

鏡像工廠

Docker 是一個典型的 C/S 架構的應用,分爲 Docker 客戶端(即平時敲的 docker 命令) Docker 服務端(dockerd 守護進程)。

Docker 客戶端通過 REST API 和服務端進行交互,docker 客戶端每發送一條指令,底層都會轉化成 REST API 調用的形式發送給服務端,服務端處理客戶端發送的請求並給出響應。 

Docker 鏡像的構建、容器創建、容器運行等工作都是 Docker 服務端來完成的,Docker 客戶端只是承擔發送指令的角色。

Docker 客戶端和服務端可以在同一個宿主機,也可以在不同的宿主機,如果在同一個宿主機的話,Docker 客戶端默認通過 UNIX 套接字(/var/run/docker.sock)和服務端通信。

類比於鋼鐵是怎樣煉成的,如果說煉製鏡像也需要個工廠的話,那麼 Dockerd 這個守護進程就是生產鏡像的工廠。能生產鏡像的不止 Docker 一家,紅帽子家的 buildah 也能生產鏡像,不過用的人並不多。二者的最大區別在於 buildah 可以不用 root 權限來構建鏡像,而使用 Docker 構建鏡像時需要用到 root 權限,沒有 root 權限的用戶構建鏡像會當場翻車。

不過 buildah 構建出來的鏡像有一堆堆的兼容性問題,所以我們還是使用 Docker 來構建鏡像。當我們使用 docker build 命令構建一個鏡像的時候第一行日誌就是 Sending build context to Docker daemon xx MB。這一步是 docker cli 這個命令行客戶端將我們當前目錄(即構建上下文) build context 打包發送 Docker daemon 守護進程 (即 dockerd)的過程。

docker build 構建鏡像的流程大概就是:

以上就是構建鏡像的大致流程,我們也可以通過 docker history imageName:Tag 命令來逆向推算出 docker build 的過程。

base image

當我們在寫 Dockerfile 的時候都需要用 FROM 語句來指定一個基礎鏡像,這些基礎鏡像並不是無中生有,也需要一個 Dockerfile 來構建成鏡像。下面我們用 debian:buster 這個基礎鏡像的 Dockerfile 來看一下基礎鏡像是如何煉成的。

一個基礎鏡像的 Dockerfile 一般僅有三行。第一行 FROM scratch 中的 scratch 這個鏡像並不真實的存在。當你使用 docker pull scratch 命令來拉取這個鏡像的時候會翻車哦,提示 Error response from daemon: 'scratch' is a reserved name。這是因爲自從 Docker 1.5 版本開始,在 Dockerfile 中 FROM scratch 指令並不進行任何操作,也就是不會創建一個鏡像層。接着第二行的 ADD rootfs.tar.xz / 會自動把 rootfs.tar.xz 解壓到 / 目錄下,由此產生的一層鏡像就是最終構建的鏡像真實的 layer 內容。第三行 CMD ["bash"] 指定這鏡像在啓動容器的時候執行的應用程序,一般基礎鏡像的 CMD 默認爲 bash 或者 sh 。

As of Docker 1.5.0 (specifically, docker/docker#8827), FROM scratch is a no-op in the Dockerfile , and will not create an extra layer in your image (so a previously 2-layer image will be a 1-layer image instead).

ADD rootfs.tar.xz / 中,這個 rootfs.tar.xz 就是我們經過一系列操作(一般是發行版源碼編譯)構建出來的根文件系統。這裏就不細談構建過程了。如果大家對源碼構建 rootfs.tar.xz 這個過程感興趣可以去看一下構建 debian 基礎鏡像的 Jenkins 流水線任務 debuerreotype,上面有構建這個 rootfs.tar.xz 完整過程,或者參考 Debian 官方的 docker-debian-artifacts 這個 repo 裏的 shell 腳本。

這裏需要注意一點,在這裏往鏡像裏添加 rootfs.tar.xz 時使用的是 ADD 而不是 COPY ,因爲在 Dockerfile 中的 ADD 指令 src 文件如果是個 tar 包,在構建的時候 Docker 會幫我們把 tar 包解開到指定目錄,使用 copy 指令則不會解開 tar 包。另外一點區別就是 ADD 指令是添加一個文件,這個文件可以是構建上下文環境中的文件,也可以是個 URL,而 COPY 則只能添加構建上下文中的文件,所謂的構建上下文就是我們構建鏡像的時候最後一個參數啦。

構建 rootfs.tar.xz 不同的發行版方法可能不太一樣,Debian 發行版的 rootfs.tar.xz 可以在 docker-debian-artifacts 這個 repo 上找到,根據不同處理器 arch 選擇相應的 branch ,然後這個 branch 下的目錄就對應着該發行版的不同的版本的代號。意外發現 Debian 官方是將所有 arch 和所有版本的 rootfs.tar.xz 都放在這個 repo 裏的,以至於這個 repo 的大小接近 2.88 GiB 😨。

我們把這個 rootfs.tar.xz 解開就可以看到,這就是一個 Linux 的根文件系統,不同於我們使用 ISO 安裝系統的那個根文件系統,這個根文件系統是經過一系列的裁剪,去掉了一些在容器運行中不必要的文件,使之更加輕量適用於容器運行的場景,整個根文件系統的大小爲 125M,如果使用 slim 的 rootfs.tar.xz 會更小一些,僅僅 76M。當然相比於僅僅幾 M 的 alpine ,這算是夠大的了。

想要自己構建一個 debian:buster 基礎鏡像其實很簡單:

下面就是構建 Debian 基礎鏡像的過程,正如 Dockerfile 中的那樣,最終只產生了一層鏡像。

鏡像是怎樣存放的(一)本地存儲

當我們構建完一個鏡像之後,鏡像就存儲在了我們 Docker 本地存儲目錄,默認情況下爲 /var/lib/docker ,下面就探尋一下鏡像是以什麼樣的目錄結構存放的。在開始 hack 之前我們先統一一下環境信息,我使用的機器是 Ubuntu 1804,docker info 信息如下:

爲了方便分析,我將其他的 docker image 全部清空掉,只保留 debian:v1 和 debian:v2 這兩個鏡像,這兩個鏡像足夠幫助我們理解容器鏡像是如何存放的,鏡像多了多話分析下面存儲目錄的時候可能不太方便(>﹏<),這兩個鏡像是我們之前使用 Debian 的 rootfs.tar.xz 構建出來的基礎鏡像。

docker (/var/lib/docker)

根據目錄的名字我們可以大致推斷出關於容器鏡像的存儲,我們只關心 image 和 overlay2 這兩個文件夾即可,容器的元數據存放在 image 目錄下,容器的 layer 數據則存放在 overlay2 目錄下。

/var/lib/docker/image 目錄結構

overlay2 代表着本地 Docker 存儲使用的是 overlay2 該存儲驅動,目前最新版本的 Docker 默認優先採用 overlay2 作爲存儲驅動,對於已支持該驅動的 Linux 發行版,不需要任何進行任何額外的配置,可使用 lsmod 命令查看當前系統內核是否支持 overlay2 。

另外值得一提的是 devicemapper 存儲驅動已經在 Docker 18.09 版本中被廢棄,Docker 官方推薦使用 overlay2 替代 devicemapper。

repositories.json 就是存儲鏡像元數據信息,主要是 image name 和 image id 的對應,digest 和 image id 的對應。當 pull 完一個鏡像的時候 Docker 會更新這個文件。當我們 docker run 一個容器的時候也用到這個文件去索引本地是否存在該鏡像,沒有鏡像的話就自動去 pull 這個鏡像。

/var/lib/docker/overlay2

在 /var/lib/docker/overlay2 目錄下,我們可以看到,鏡像 layer 的內容都存放在一個 diff 的文件夾下,diff 的上級目錄就是以鏡像 layer 的 digest 爲名的目錄。其中還有個 l 文件夾,下面有一坨坨的硬鏈接文件指向上級目錄的 layer 目錄。這個 l 其實就是 link 的縮寫,l 下的文件都是一些比 digest 文件夾名短一些的,方便不至於 mount 的參數過長。

鏡像是怎麼搬運的

當我們在本地構建完成一個鏡像之後,如何傳遞給他人呢?這就涉及到鏡像搬運的一些知識,搬運鏡像就像我們在 GitHub 上搬運代碼一樣,Docker 也有類似於 git clone 和 git push 的搬運方式。docker push 就和我們使用 git push 一樣,將本地的鏡像推送到一個稱之爲 registry 的鏡像倉庫,這個 registry 鏡像倉庫就像 GitHub 用來存放公共 / 私有的鏡像,一箇中心化的鏡像倉庫方便大家來進行交流和搬運鏡像。docker pull 就像我們使用 git pull 一樣,將遠程的鏡像拉取本地。

docker pull

理解 docker pull 一個鏡像的流程最好的辦法是查看 OCI registry 規範中的這段文檔 pulling-an-image ,在這裏我結合大佬的博客簡單梳理一下 pull 一個鏡像的大致流程。下面這張圖是從大佬博客借來的😂

docker pull 就和我們使用 git clone 一樣效果,將遠程的鏡像倉庫拉取到本地來給容器運行時使用,結合上圖大致的流程如下:

docker push

push 推送一個鏡像到遠程的 registry 流程恰好和 pull 拉取鏡像到本地的流程相反。我們 pull 一個鏡像的時候往往需要先獲取包含着鏡像 layer 信息的 Manifest 文件,然後根據這個文件中的 layer 信息取 pull 相應的 layer。push 一個鏡像,需要先將鏡像的各個 layer 推送到 registry ,當所有的鏡像 layer 上傳完畢之後最後再 push Image Manifest 到 registry。大體的流程如下:

Monolithic Upload

Chunked Upload

Python docker-drag

這是一個很簡單粗暴的 Python 腳本,使用 request 庫請求 registry API 來從鏡像倉庫中拉取鏡像,並保存爲一個 tar 包,拉完之後使用 docker load 加載一下就能食用啦。該 Python 腳本簡單到去掉空行和註釋不到 200 行,如果把這個腳本源碼讀一遍的話就能大概知道 docker pull 和 skopeo copy 的一些原理,他們都是去調用 registry 的 API ,所以還是推薦去讀一下這個它的源碼。

食用起來也很簡單直接 python3 docker_pull.py [image name],貌似只能拉取 docker.io 上的鏡像。

skopeo

這個工具是紅帽子家的,是 Podman、Skopeo 和 Buildah (簡稱 PSB )下一代容器新架構中的一員,不過我覺着 Podman 想要取代 docker 和 containerd 容器運行時還有很長的路要走,雖然它符合 OCI 規範,但對於企業來講,替換的成本並不值得他們去換到 PSB 上去。

其中的 skopeo 這個鏡像搬運工具簡直是個神器,尤其是在 CI/CD 流水線中搬運兩個鏡像倉庫裏的鏡像簡直爽的不得了。我曾經一個工作任務就是優化我們的 Jenkins 流水線中同步兩個鏡像倉庫的過程,使用了 skopeo 替代 Docker 來同步兩個鏡像倉庫中的鏡像,將原來需要 2h 小時縮短到了 25min 😀。

在這裏我講兩個最常用的功能。

skopeo copy

使用 skopeo copy 兩個 registry 中的鏡像時,skopeo 請求兩個 registry API 直接 copy original blob 到另一個 registry ,這樣免去了像 docker pull –> docker tag –> docker push 那樣 pull 鏡像對鏡像進行解壓縮,push 鏡像進行壓縮。尤其是在搬運一些較大的鏡像(幾 GB 或者幾十 GB 的鏡像,比如 nvidia/cuda ),使用 skopeo copy 的加速效果十分明顯。

skopeo inspect

用 skopeo inspect 命令可以很方便地通過 registry 的 API 來查看鏡像的 manifest 文件,以前我都是用 curl 命令的,要 token 還要加一堆參數,所以比較麻煩,所以後來就用上了 skopeo inspect😀。

鏡像是怎麼存放的(二)registry 存儲

文章的開頭我們提到過 OCI 規範中的鏡像倉庫規範 distribution-spec,該規範就定義着容器鏡像如何存儲在遠端(即 registry)上。我們可以把 registry 看作鏡像的倉庫,使用該規範可以幫助我們把這些鏡像按照約定俗成的格式來存放,目前實現該規範的 registry 就 Docker 家的 registry 使用的多一些。其他的 registry 比如 harbor ,quay.io 使用的也比較多。

registry (/registry/docker/v2)

想要分析鏡像是如何存放在 registry 上的,我們在本地使用 docker run 運行 registry 的容器即可,我們僅僅是來分析 registry 中鏡像時如何存儲的,這種場景下不太適合用 harbor 這種重量級的 registry。

啓動完 registry 容器之後我們給之前已經構建好的鏡像重新打上該 registry 的 tag 方便後續 push 到 registry 上。

當我們在本地啓動一個 registry 容器之後,容器內默認的存儲位置爲 /var/lib/registry ,所以我們在啓動的時候加了參數 -v /var/lib/registry:/var/lib/registry 將本機的路徑掛載到容器內。進入這裏的路徑我們使用 tree 命令查看一下這個目錄的存儲結構。

樹形的結構看着不太直觀,我畫了一張層級結構的圖:

blobs 目錄

之前我們向 registry 中推送了兩個鏡像,這兩個鏡像的 layer 相同但不是用一個鏡像,在我們之前 push image 的時候也看到了 d1b85e6186f6: Layer already exists。也就可以證明,雖然兩個鏡像不同,但它們的 layer 在 registry 中存儲的時候可能是相同的。

在 blobs/sha256 目錄下一共有 5 個名爲 data 的文件,我們可以推測一下最大的那個 [26M] 應該是鏡像的 layer ,最小的 [529] 那個應該是 manifest,剩下的那個 [1.4K] 應該就是 image config 文件。

在 registry 的存儲目錄下,blobs 目錄用來存放鏡像的三種文件:layer 的真實數據,鏡像的 manifest 文件,鏡像的 image config 文件。這些文件都是以 data 爲名的文件存放在於該文件 sha256 相對應的目錄下。使用以內容尋址的 sha256 散列存儲方便索引文件,在 blob digest 目錄下有一個名爲 data 的文件,對於 layer 來講,這是個 data 文件的格式是 vnd.docker.image.rootfs.diff.tar.gzip ,我們可以使用 tar -xvf 命令將這個 layer 解開。當我們使用 docker pull 命令拉取鏡像的時候,也是去下載這個 data 文件,下載完成之後會有一個 docker-untar 的進程將這個 data 文件解開存放在 /var/lib/docker/overlay2/${digest}/diff 目錄下。

manifest 文件

其實就是一個普通的 json 文件,記錄了一個鏡像所包含的 layer 信息,當我們 pull 鏡像的時候會使用到這個文件。

image config 文件

image config 文件裏並沒有包含鏡像的 tag 信息。

_uploads 文件夾

_uploads 文件夾是個臨時的文件夾,主要用來存放 push 鏡像過程中的文件數據,當鏡像 layer 上傳完成之後會清空該文件夾。其中的 data 文件上傳完畢後會移動到 blobs 目錄下,根據該文件的 sha256 值來進行散列存儲到相應的目錄下。

上傳過程中的目錄結構:

_manifests 文件夾

_manifests 文件夾是鏡像上傳完成之後由 registry 來生成的,並且該目錄下的文件都是一個名爲 link 的文本文件,它的值指向 blobs 目錄下與之對應的目錄。

_manifests 文件夾下包含着鏡像的 tags 和 revisions 信息,每一個鏡像的每一個 tag 對應着於 tag 名相同的目錄。鏡像的 tag 並不存儲在 image config 中,而是以目錄的形式來形成鏡像的 tag,這一點比較奇妙,這和我們 Dockerfile 中並不包含鏡像名和 tag 一個道理?

鏡像的 tag

每個 tag 名目錄下面有 current 目錄和 index 目錄, current 目錄下的 link 文件保存了該 tag 目前的 manifest 文件的 sha256 編碼,對應在 blobs 中的 sha256 目錄下的 data 文件,而 index 目錄則列出了該 tag 歷史上傳的所有版本的 sha256 編碼信息。_revisions 目錄裏存放了該 repository 歷史上上傳版本的所有 sha256 編碼信息。

當我們 pull 鏡像的時候如果不指定鏡像的 tag 名,默認就是 latest,registry 會從 HTTP 請求中解析到這個 tag 名,然後根據 tag 名目錄下的 link 文件找到該鏡像的 manifest 的位置返回給客戶端,客戶端接着去請求這個 manifest 文件,客戶端根據這個 manifest 文件來 pull 相應的鏡像 layer。

最後再補充一點就是,同一個鏡像在 registry 中存儲的位置是相同的。

從上面這三個結論中我們可以推斷出 registry 存儲目錄裏並不會存儲與該 registry 相關的信息,比我們 push 鏡像的時候需要給鏡像加上 localhost:5000 這個前綴,這個前綴並不會存儲在 registry 存儲中。加入我要遷移一個很大的 registry 鏡像倉庫,鏡像的數量在 5k 以上。最便捷的辦法就是打包這個 registry 存儲目錄,將 tar 包 rsync 到另一臺機器即可。需要強調一點,打包 registry 存儲目錄的時候不需要進行壓縮,直接 tar -cvf 即可。因爲 registry 存儲的鏡像 layer 已經是個 tar.gzip 格式的文件,再進行壓縮的話效果甚微而且還浪費 CPU 時間,得不償失。

docker-archive

原先我認爲 docker save 出來的並不是一個鏡像,而是一個 .tar 文件,但我仔細思考後,還是認爲它是一個鏡像,只不過存在的方式不同而已。與在 Docker 和 registry 中存放的方式不同,使用 docker save 出來的鏡像是一個孤立的存在。就像是從蛋糕店裏拿出來的蛋糕,外面肯定要有個精美的包裝。它放在哪裏都可以,使用的時候我們使用 docker load 拆開外包裝 (.tar) 就可。比如我們離線部署 harbor 的時候就是使用官方的鏡像 tar 包來進行加載鏡像啓動容器的。

鏡像是怎麼食用的

當我們拿到一個鏡像之後,如果用它來啓動一個容器呢?這裏就涉及到了 OCI 規範中的另一個規範即運行時規範 runtime-spec 。容器運行時通過一個叫 OCI runtime filesytem bundle 的標準格式將 OCI 鏡像通過工具轉換爲 bundle ,然後 OCI 容器引擎能夠識別這個 bundle 來運行容器。

filesystem bundle 是個目錄,用於給 runtime 提供啓動容器必備的配置文件和文件系統。標準的容器 bundle 包含以下內容:

docker run

當我們啓動一個容器之後使用 tree 命令來分析一下 overlay2 就會發現,較之前的目錄,容器啓動之後 overlay2 目錄下多了一個 merged 的文件夾,該文件夾就是容器內看到的。Docker 通過 overlayfs 聯合掛載的技術將鏡像的多層 layer 掛載爲一層,這層的內容就是容器裏所看到的,也就是 merged 文件夾。

這是 Docker 官方文檔 Use the OverlayFS storage driver 裏的介紹圖:

如果想對 Overlayfs 文件系統有詳細的瞭解,可以參考 Linux 內核官網上的文檔。

https://blog.k8s.li/Exploring-container-image.html


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