說透 Docker:基礎
- 2021 年 11 月 17 日
- 筆記
- docker, Kubernetes與Docker
既然要學習 K8S,相信各位讀者都已經使用過 Docker 了,Docker 的入門是比較容易的,但 Docker 的網路和存儲、虛擬化是相當複雜的,Docker 的技術點比較多,在本章中將會深入介紹 Docker 的各方面,期待能夠幫助讀者加深對 Docker 的理解。
本文為作者的 Kubernetes 系列電子書的一部分,電子書已經開源,歡迎關注,電子書瀏覽地址:
//k8s.whuanle.cn【適合中國訪問】
//ek8s.whuanle.cn 【gitbook】
容器化應用
什麼是容器化應用
containerized applications 指容器化的應用,我們常常說使用鏡像打包應用程式,使用 Docker 發布、部署應用程式,那麼當你的應用成功在 Docker 上運行時,稱這個應用是 containerized applications。
應用怎麼打包
容器化應用的最主要特徵是使用鏡像打包應用的運行環境以及應用程式,可以通過 Docker 啟動這個鏡像,進而將 應用程式啟動起來。
將一個應用程式打包為鏡像,大約分為以下過程:
- 編寫 Dockerfile 文件 — 定義構建鏡像的流程
- 選擇一個基礎鏡像(作業系統) — 作業系統
- 安裝應用的需要的環境 — 運行環境
- 複製程式文件 — 應用程式
- 啟動 Dockerfile — 生成鏡像
Docker 鏡像組成
以 .NET Core(C#) 程式為例,一個 Docker 鏡像的層次如下圖所示:

在 Docker 鏡像中,作業系統是高度精簡的,可能只有一個精簡的 Shell,甚至沒有 Shell。而且鏡像中的作業系統還不包含內核,容器都是共享所在的宿主機的內核。所以有時會說容器僅包含必要的作業系統(通常只有作業系統文件和文件系統對象),容器中查看到的 Linux 內核版本與宿主機一致。
Docker 鏡像的是由一系統文件組成的。

聯合文件系統
Linux 有名為 Unionfs 的文件系統服務,可以將不同文件夾中的文件聯合到一個文件夾中。Unionfs 有稱為分支的概念,一個分支包含了多個目錄和文件,多個分支可以掛載在一起,在掛載時,可以指定一個分支優先順序大於另一個分支,這樣當兩個分支都包含相同的文件名時,一個分支會優先於另一個分支,在合併的目錄中,會看到高優先順序分支的文件。

Docker 中,層層組成鏡像的技術也是聯合文件系統,Union File System。Docker 鏡像中的作業系統是根文件系統,在上一小節的圖片中,可以看到有 bin、boot 等目錄。我們都知道,Docker 鏡像是由多層文件組成的,在上面的示例圖片中有三層組成:根文件系統、環境依賴包、應用程式文件。當鏡像層生成後,便不能被修改,如果再進行操作,則會在原來的基礎上生成新的鏡像層,層層聯合,最終生成鏡像。當然生成的鏡像可能會因為層數太多或者操作過多,導致出現大量冗餘,鏡像臃腫。
Docker 的鏡像分層是受 Linux Unionfs 啟發而開發的,Docker 支援多種文件聯合系統,如 AUFS、OverlayFS、VFS 等。
Docker 在不同系統中可以選擇的聯合文件系統:
| Linux發行版 | 推薦的存儲驅動程式 | 替代驅動程式 |
|---|---|---|
| Ubuntu | overlay2 |
overlay devicemapper, aufs, zfs,vfs |
| Debian | overlay2 |
overlay, devicemapper, aufs,vfs |
| CentOS | overlay2 |
overlay, devicemapper, zfs,vfs |
提示
Docker Desktop for Mac 和 Docker Desktop for Windows 不支援修改存儲驅動程式,只能使用默認存儲驅動程式。
Linux 內核
既然 Docker 容器需要與 Linux 內核結合才能使用,那麼我們看一下 Linux 內核的功能,稍微了解一下 Linux 內核在支撐 Docker 容器運作中起到什麼作用。
Linux 內核主要包含以下功能:
-
記憶體管理:追蹤記錄有多少記憶體存儲了什麼以及存儲在哪裡;
-
進程管理:確定哪些進程可以使用中央處理器(CPU)、何時使用以及持續多長時間;
-
設備驅動程式:充當硬體與進程之間的調解程式/解釋程式;
-
系統調用和安全防護:接受程式請求調用系統服務;
- 文件系統:作業系統中負責管理持久數據的子系統,在 Linux 中,一切皆文件。
Linux 層次結構如下:

Docker 容器中包含了一個作業系統,包含簡單的 shell 或者不包含,其層次結構如圖所示:

Docker 結構
本節將了解 Docker 的組成部件和結構。
Docker 服務與客戶端
Docker 由 Service 和 Client 兩部分組成,在伺服器上可以不安裝 Docker Client,可以通過 Http Api 等方式與 Docker Servie 通訊。
在安裝了 Docker 的主機上執行命令 docker version 查看版本號。
Client: Docker Engine - Community
Version: 20.10.7
API version: 1.41
Go version: go1.13.15
Git commit: f0df350
Built: Wed Jun 2 11:58:10 2021
OS/Arch: linux/amd64
Context: default
Experimental: true
Server: Docker Engine - Community
Engine:
Version: 20.10.7
API version: 1.41 (minimum version 1.12)
Go version: go1.13.15
Git commit: b0f5bc3
Built: Wed Jun 2 11:56:35 2021
OS/Arch: linux/amd64
Experimental: false
containerd:
Version: 1.4.6
GitCommit: d71fcd7d8303cbf684402823e425e9dd2e99285d
runc:
Version: 1.0.0-rc95
GitCommit: b9ee9c6314599f1b4a7f497e1f1f856fe433d3b7
docker-init:
Version: 0.19.0
GitCommit: de40ad0
Docker 客戶端
要想跟 Docker Server 通訊,可以使用 Restful API、UNIX 套接字或網路介面(Socket)。Docker 官方的客戶端是一個二進位命令行程式,使用 Go 語言編寫,我們也可以使用 C#、Java 等語言寫一個類似的程式,Docker 客戶端不需要安裝到 Docker Server 所在的主機,Client 跟 Server 可以遠程通訊。
Docker 的客戶端是許多 Docker 用戶與 Docker 交互的主要方式,當我們使用 docker run 之類的命令時,客戶端會將這些命令發送到 Docker Server,由 Docker Server 解析並執行命令。
Docker for Linux 中最為常見的同主機通訊方式是 Unix 域套接字。很多軟體都支援使用域套接字與 Docker 通訊,例如 CI/CD 軟體 Jenkins,使用域套接字連接 Docker,能夠利用 Docker 啟動容器構建應用程式以及使用 Docker 來做一些不可描述的事情。

容器運行時
容器運行時是提供運行環境並啟動容器的軟體,我們最常聽說的是 Docker,此外還有 containerd、CRI-O 等。可以毫不誇張的說,整個 Kubernetes 建立在容器之上。
默認情況下,Kubernetes 使用 容器運行時介面(Container Runtime Interface,CRI) 來與伺服器中容器運行時交互。所以 Kubernetes 支援多種容器軟體,但只能使用一種容器運行時進行工作,在有多個容器運行時的情況下,我們需要指定使用何種運行時,如果你不指定運行時,則 kubeadm 會自動嘗試檢測到系統上已經安裝的運行時, 方法是掃描一組眾所周知的 Unix 域套接字。
Linux 是多進程作業系統,為了讓多個系統中的多個進程能夠進行高效的通訊,出現和很多方法,其中一種是域套接字(Unix domain socket),只能用於在同一電腦中的進程間通訊,但是其效率高於網路套接字(socket),域套接字不需要經過網路協議處理,通過系統調用將數據從一個進程複製到另一個進程中。
域套接字使用一個 .sock 文件進行通訊,常見的容器軟體其對應域套接字如下:
| 運行時 | 域套接字 |
|---|---|
| Docker | /var/run/dockershim.sock |
| containerd | /run/containerd/containerd.sock |
| CRI-O | /var/run/crio/crio.sock |
同一主機下常見進程通訊方式有 共享記憶體、消息隊列、管道通訊(共享文件)。
Unux 域套接字是套接字和管道之間的混合物。 在 Linux 中,有很多進程,為了讓多個進程能夠進行通訊,出現和很多方法,其中一種是套接字(socket)。一般的 socket 都是基於 TCP/IP 的,稱為網路套接字,可以實現跨主機進程通訊。在 Linux 中有一種套接字,名為域套接字,只能用於在同一電腦中的進程間通訊,但是其效率高於網路套接字。域套接字使用一個 .sock 文件進行通訊。
當電腦中有多種容器運行時,Kubernetes 默認優先使用 Docker。
如果你想了解 CRI ,請點擊:
Docker 引擎
Docker 引擎也可以說是 Docker Server,它由 Docker 守護進程(Docker daemon)、containerd 以及 runc 組成。
當使用 Docker client 輸入命令時,命令會被發送到 Docker daemon ,daemon 會偵聽請求並管理 Docker 對象,daemon 可以管理 鏡像、容器、網路和存儲卷等。
下面這個圖是新 Docker 版本的結構組成。

Docker 引擎變化
Docker 首次發布時,Docker 引擎由兩個核心組件構成:LXC 和 Docker daemon,這也是很多文章中稱 Docker 是基於 LXC 的原因,舊版本的 Docker 利用了 LXC、cgroups、Linux 內核編寫。接下來我們了解一下 LXC 。
LXC (Linux Container)是 Linux 提供的一種內核虛擬化技術,可以提供輕量級的虛擬化,以便隔離進程和資源,它是作業系統層面上的虛擬化技術。LXC 提供了對諸如命名空間(namespace) 和控制組(cgroups) 等基礎工具的操作能力,它們是基於 Linux 內核的容器虛擬化技術。我們不需要深入了解這個東西。

Docker 一開始是使用 LXC 做的,LXC 是一個很牛逼的開源項目,但是隨著 Docker 的成熟,Docker 開始拋棄 LXC,自己動手手撕容器引擎。
為什麼 Docker 要拋棄 LXC 呢?首先,LXC 是基於 Linux 的。這對於一個立志於跨平台的 Docker 來說是個問題,離開 LXC,怎麼在 MAC、Windows 下運行?其次,如此核心的組件依賴於外部工具,這會給項目帶來巨大風險,甚至影響其發展。
哈哈哈。。。其實筆者覺得不支援 Windows 也罷。。。
Docker 引擎的架構
下面是一張 Docker 的架構圖。

Docker client 和 Docker daemon 在前面已經介紹過了,接下來介紹其他組件。
containerd
containerd 是一個開源容器引擎,是從 Docker 開源出去的。之前有新聞說 Kubernetes 不再支援 Docker,只支援 containerd,很多人以為 Docker 不行了。
一開始 Docker 是一個 「大單體」,隨著 Docker 的成長,Docker 開始進行模組化,Docker 中的許多模組都是可替換的,如 Docker 網路。支援容器運行的核心程式碼自然也抽出來,單獨做一個模組,便是 containerd。Kubernetes 不再支援 Docker,只不過是降低依賴程度,減少對其他模組的依賴,只集中在 containerd 上。當我們安裝 Docker 時,自然會包含 containerd。如果我們不需要 Docker 太多組件,那麼我們可以僅僅安裝 containerd,由 Kubernetes 調度,只不過我們不能使用 Docker client 了。因此可以說,Kubernetes 不再支援 Docker,並不代表會排斥 Docker。
containerd 的主要任務是容器的生命周期管理,如啟動容器、暫停容器、停止容器等。containerd 位於 daemon 和 runc 所在的 OCI 層之間。
shim
shim 它的作用非常單一,那就是實現 CRI 規定的每個介面,然後把具體的 CRI 請求「翻譯」成對後端容器項目的請求或者操作。
這裡要區別一下,dockershim 和 containerd-shim,dockershim 是一個臨時性的方案,dockershim 會在 Kubernetes v1.24中 刪除(2022年),這也是 Kubernetes 不再支援 Docker 的另一組件。
提示
CRI 即 Container Runtime Interface,容器運行時介面,容器引擎要支援 Kubernetes ,需要實現 CRI 介面,例如 runc 、crun 兩種是常見的 Container Runtime。
shim 是容器進程的父進程,shim 的生命周期跟容器一樣長,shim 是一個輕量級的守護進程,它與容器進程緊密相關,但是 shim 與容器中的進程完全分離。shim 可以將容器的 stdin、stdout、srderr 流重定向到日誌中,我們使用 docker logs 即可看到容器輸出到控制台的流。
關於 shim,我們就先了解到這裡,後面會繼續講解一個示例。
runc
runc 實質上是一個輕量級的、針對 Libcontainer 進行了包裝的命令行交互工具,runc 生來只有一個作用——創建容器,即 runc 是一個由於運行容器的命令行工具。
提示
Libcontainer 取代了早期 Docker 架構中的 LXC。
如果主機安裝了 Docker,我們可以使用 runc --help 來查看使用說明。我們可以這樣來理解 runc,runc 是在隔離環境生成新的進程的工具,在這個隔離環境中有一個專用的根文件系統(ubuntu、centos等)和新的進程樹,這個進程樹的根進程 PID=1。
部落格推薦
筆者在查閱資料時,發現了這個大佬的部落格,在這個大佬的部落格中學會了很多東西。
部落格推薦://iximiuz.com/en/
在後面的節中,我們將繼續了解 Docker 中的網路和存儲,並開始探究與 Kubernetes 相關的知識點。


