1. 鏡像與容器
1.1 鏡像
Docker鏡像類似于未運行的exe應用程序,或者停止運行的VM。當使用docker run命令基于鏡像啟動容器時,容器應用便能為外部提供服務。
鏡像實際上就是這個用來為容器進程提供隔離后執行環境的文件系統。我們也稱之為根文件系統(Rootfs)。(注意,rootfs 只是一個操作系統所包含的文件、配置和目錄,并不包括操作系統內核。同一臺機器上的所有容器,都共享宿主機操作系統的內核。)
由于 rootfs 里封裝的不僅僅是應用,還包括它運行所需要的所有依賴。這就賦予了容器的強一致性:無論在本地、云端,還是其他任何地方,用戶只需要解壓打包好的容器鏡像,這個應用運行所需要的完整的執行環境就被重現出來了。
我們可以將 Docker 鏡像理解為包含應用程序以及其相關依賴的一個基礎文件系統,在 Docker 容器啟動的過程中,它以只讀的方式被用于創建容器的運行環境。
1.1.1 鏡像分層
但還有一個不容忽視的問題,例如如果我需要一個在CentOS環境中跑的apache應用,我可以將它打包成一個apache鏡像;如果我還需要一個在CentOS環境中跑的mysql應用,我又將它打包成一個mysql鏡像……
這幾個鏡像中都有全部的CentOS的全部環境,將造成大量空間占用問題及碎片化問題。
Docker的解決方法是: 在鏡像的設計中,引入了層(layer)的概念。即: 用戶制作鏡像的每一步操作,都生成一個層,也就是一個增量 rootfs。
Docker 鏡像其實是由基于 UnionFS 文件系統的一組鏡像層依次掛載而得,而每個鏡像層包含的其實是對上一鏡像層的修改,這些修改其實是發生在容器運行的過程中的。所以,我們也可以反過來理解,鏡像是對容器運行環境進行持久化存儲的結果。
1.1.1 鏡像的實現
1.1.1.1 Docker 是如何構建并且存儲鏡像的
Docker 中的每一個鏡像都是由一系列只讀的層組成的,Dockerfile 中的每一個命令都會在已有的只讀層上創建一個新的層,容器中的每一層都只對當前容器進行了非常小的修改
當鏡像被 docker run 命令創建時就會在鏡像的最上層添加一個可寫的層,也就是容器層,所有對于運行時容器的修改其實都是對這個容器讀寫層的修改。
上面的這張圖片非常好的展示了組裝的過程,每一個鏡像層都是建立在另一個鏡像層之上的,同時所有的鏡像層都是只讀的,只有每個容器最頂層的容器層才可以被用戶直接讀寫,所有的容器都建立在一些底層服務(Kernel)上,包括命名空間、控制組、rootfs 等等,這種容器的組裝方式提供了非常大的靈活性,只讀的鏡像層通過共享也能夠減少磁盤的占用。
1.1.1.2鏡像概述
所有的 Docker 鏡像都是按照 Docker 所設定的邏輯打包的,也是受到 Docker Engine 所控制的。
我們常見的虛擬機鏡像,通常是由熱心的提供者以他們自己熟悉的方式打包成鏡像文件,被我們從網上下載或是其他方式獲得后,恢復到虛擬機中的文件系統里的。而 Docker 的鏡像我們必須通過 Docker 來打包,也必須通過 Docker 下載或導入后使用,不能單獨直接恢復成容器中的文件系統。
雖然這么做失去了很多靈活性,但固定的格式意味著我們可以很輕松的在不同的服務器間傳遞 Docker 鏡像,配合 Docker 自身對鏡像的管理功能,讓我們在不同的機器中傳遞和共享 Docker 變得非常方便。
對于每一個記錄文件系統修改的鏡像層來說,Docker 都會根據它們的信息生成了一個 Hash 碼,這是一個 64 長度的字符串,足以保證全球唯一性。
由于鏡像層都有唯一的編碼,我們就能夠區分不同的鏡像層并能保證它們的內容與編碼是一致的,這帶來了另一項好處,就是允許我們在鏡像之間共享鏡像層。
舉一個實際的例子,由 Docker 官方提供的兩個鏡像 elasticsearch 鏡像和 jenkins 鏡像都是在 openjdk 鏡像之上修改而得,那么在我們實際使用的時候,這兩個鏡像是可以共用 openjdk 鏡像內部的鏡像層的。
1.1.2 查看鏡像
如果要查看當前連接的 docker daemon 中存放和管理了哪些鏡像,我們可以使用 docker images 這個命令 ( Linux、macOS 還是 Windows 上都是一致的 )。
在 docker images 命令的結果中,我們可以看到鏡像的 ID ( IMAGE ID)、構建時間 ( CREATED )、占用空間 ( SIZE ) 等數據。
1.1.3 鏡像命名
鏡像層的 ID 既可以識別每個鏡像層,也可以用來直接識別鏡像 ( 因為根據最上層鏡像能夠找出所有依賴的下層鏡像,所以最上層進行的鏡像層 ID 就能表示鏡像的 ID ),但是使用這種無意義的超長哈希碼顯然是違背人性的,通過鏡像名我們能夠更容易的識別鏡像。
準確的來說,鏡像的命名我們可以分成三個部分:username、repository 和 tag。
- username: 主要用于識別上傳鏡像的不同用戶,與 GitHub 中的用戶空間類似。
對于 username 來說,在上面我們展示的 docker images 結果中,有的鏡像有 username 這個部分,而有的鏡像是沒有的。沒有 username 這個部分的鏡像,表示鏡像是由 Docker 官方所維護和提供的,所以就不單獨標記用戶了。
-
repository:主要用于識別進行的內容,形成對鏡像的表意描述。
Docker 中鏡像的 repository 部分通常采用的是軟件名。我們推崇一個容器運行一個程序的做法,那么自然容器的鏡像也會僅包含程序以及與它運行有關的一些依賴包,所以我們使用程序的名字直接套用在鏡像之上,既祛除了鏡像取名的麻煩,又能直接表達鏡像中的內容。 -
tag:主要用戶表示鏡像的版本,方便區分進行內容的不同細節
在鏡像命名中,還有一個非常重要的部分,也就是鏡像的標簽 ( tag )。鏡像的標簽是對同一種鏡像進行更細層次區分的方法,也是最終識別鏡像的關鍵部分。
通常來說,鏡像的標簽主要是為了區分同類鏡像不同構建過程所產生的不同結果的。由于時間、空間等因素的不同,Docker 每次構建鏡像的內容也就有所不同,具體體現就是鏡像層以及它們的 ID 都會產生變化。而標簽就是在鏡像命名這個層面上區分這些鏡像的方法。
與鏡像的 repository 類似,鏡像 tag 的命名方法也通常參考鏡像所關聯的應用程序。更確切的來說,我們通常會采用鏡像內應用程序的版本號以及一些環境、構建方式等信息來作為鏡像的 tag。
1.2 容器
容器就是將軟件打包成標準化單元,以用于開發、交付和部署。
- 容器鏡像是輕量的、可執行的獨立軟件包 ,包含軟件運行所需的所有內容:代碼、運行時環境、系統工具、系統庫和設置。
- 容器化軟件適用于基于Linux和Windows的應用,在任何環境中都能夠始終如一地運行。
- 容器賦予了軟件獨立性,使其免受外在環境差異(例如,開發和預演環境的差異)的影響,從而有助于減少團隊間在相同基礎設施上運行不同軟件時的沖突。
1.2.1 容器的生命周期
由于 Docker 攬下了大部分對容器管理的活,只提供給我們非常簡單的操作接口,這就意味著 Docker 里對容器的一些運行細節會被更加嚴格的定義,這其中就包括了容器的生命周期。
這里有一張容器運行的狀態流轉圖:
1.2.2 主進程
當我們啟動容器時,Docker 其實會按照鏡像中的定義,啟動對應的程序,并將這個程序的主進程作為容器的主進程 ( 也就是 PID 為 1 的進程 )。而當我們控制容器停止時,Docker 會向主進程發送結束信號,通知程序退出。
而當容器中的主進程主動關閉時 ( 正常結束或出錯停止 ),也會讓容器隨之停止。
1.2.3 寫時復制
Docker 的寫時復制與編程中的相類似,也就是在通過鏡像運行容器時,并不是馬上就把鏡像里的所有內容拷貝到容器所運行的沙盒文件系統中,而是利用 UnionFS 將鏡像以只讀的方式掛載到沙盒文件系統中。只有在容器中發生對文件的修改時,修改才會體現到沙盒環境上。
也就是說,容器在創建和啟動的過程中,不需要進行任何的文件系統復制操作,也不需要為容器單獨開辟大量的硬盤空間,與其他虛擬化方式對這個過程的操作進行對比,Docker 啟動的速度可見一斑。
Docker的容器是一個多層的結構。如果對鏡像做history操作,我們可以看見他里面每一次dockerfile的命令都會創建一個新的層次。
[root@ip-172-16-1-4 ec2-user]# docker image history nginx
IMAGE CREATED CREATED BY SIZE COMMENT
8cf1bfb43ff5 6 days ago /bin/sh -c #(nop) CMD ["nginx" "-g" "daemon… 0B
<missing> 6 days ago /bin/sh -c #(nop) STOPSIGNAL SIGTERM 0B
<missing> 6 days ago /bin/sh -c #(nop) EXPOSE 80 0B
<missing> 6 days ago /bin/sh -c #(nop) ENTRYPOINT ["/docker-entr… 0B
<missing> 6 days ago /bin/sh -c #(nop) COPY file:0fd5fca330dcd6a7… 1.04kB
<missing> 6 days ago /bin/sh -c #(nop) COPY file:1d0a4127e78a26c1… 1.96kB
<missing> 6 days ago /bin/sh -c #(nop) COPY file:e7e183879c35719c… 1.2kB
<missing> 6 days ago /bin/sh -c set -x && addgroup --system -… 63.3MB
<missing> 6 days ago /bin/sh -c #(nop) ENV PKG_RELEASE=1~buster 0B
<missing> 6 days ago /bin/sh -c #(nop) ENV NJS_VERSION=0.4.2 0B
<missing> 6 days ago /bin/sh -c #(nop) ENV NGINX_VERSION=1.19.1 0B
<missing> 6 days ago /bin/sh -c #(nop) LABEL maintainer=NGINX Do… 0B
<missing> 6 days ago /bin/sh -c #(nop) CMD ["bash"] 0B
<missing> 6 days ago /bin/sh -c #(nop) ADD file:6ccb3bbcc69b0d44c… 69.2MB
Docker里面有一個重要的概念叫做 Storage driver,他可以幫助我們實現對容器的分層和讀寫。目前,docker的默認storage driver 是overlay2。所有的容器相關的文件都保存在/var/lib/docker這個目錄下。我們可以看見在overlay2里面有很多不同的文件
[root@ip-172-16-1-4 ec2-user]# ls /var/lib/docker/overlay2/
12597d435b78d470bed7cf3a4cc7d60691432e74f12c00fd44def7ecf6ab659f 6945f4530a5212bc3a8aa598dc88839d39b763799ccdfbd18a5f04180e3b676e d1f1bcc388595272f11f4ace02f9e6976dc96fd80cce8216d3626484ba114aad
145e76991ae57691ed94de5dd2ea950ff7b50dc26729605e711cd0f35f275e84 7c4d732c22b84e0df8c0d317df825623958d4eff59055827e6b2c76cbe8b0350 d1f1bcc388595272f11f4ace02f9e6976dc96fd80cce8216d3626484ba114aad-init
16da856b3acb71eea396f529a80cf728d664a4bf3eb042f828cb9651d82e90bf 9b6749a9a76ad9f76d7795ebbf8e47595dada7a06b9126f5d9c6e1084f1d0c02 f089b3fd763c560e1c398c9b432100cea56ad4dae3a6b760de42b45e856ce693
16da856b3acb71eea396f529a80cf728d664a4bf3eb042f828cb9651d82e90bf-init a60b187ea615a59402ad2fe6687a4dc87f1c037eafea6880167795c6e1b7f900 f089b3fd763c560e1c398c9b432100cea56ad4dae3a6b760de42b45e856ce693-init
658b1eab364b4523a8c7274e9b8a2bdc55095e9bd56dcb697f6456b4064abe66 a60b187ea615a59402ad2fe6687a4dc87f1c037eafea6880167795c6e1b7f900-init l
658b1eab364b4523a8c7274e9b8a2bdc55095e9bd56dcb697f6456b4064abe66-init backingFsBlockDev
一個典型的場景如下所示,一個鏡像文件里面,里面分了多層,最下面的是基礎鏡像,這個基礎鏡像不包括內核文件,執行的時候他會直接調用宿主機的內核,因此他的空間并不大。在基礎鏡像上面,又分了很多層,每一層代表在dockerfile里面執行的一行命令。這整個鏡像文件都是只讀的。每個容器通過鏡像創建自己的容器層,而容器層是可以讀寫的,修改的內容他們會保存在自己的目錄下面。因此每個容器對自己的修改 不會影響到其他容器。
在docker里面,我們通過storage driver來進行所謂的copy on write ( 寫時復制)的操作。storage driver有很多種,目前默認的是overlay2
overlay的基本工作原理如下
我們通過鏡像創建的容器包括了三層。最下面的是一個只讀的鏡像層,第二層是容器層,在他上面最上面的容器掛載層。最上層顯示的是我們在容器上直接看見的內容,他通過UnionFS,或者說類似軟連接的方式,文件的路徑指向了容器層或者是鏡像層。當我們嘗試讀取,修改或者創建一個新的文件時,我們總是從上到下進行搜索,如果在容器層找到了,那么就直接打開;如果容器層沒有,那就從鏡像層打開。如果是一個新建的文檔,那么就從鏡像層拷貝到容器層,再打開操作。
參考資料
《開發者必備的 Docker 實踐指南》