說透 Docker:基礎

痴者工良發表於2021-11-17

既然要學習 K8S,相信各位讀者都已經使用過 Docker 了,Docker 的入門是比較容易的,但 Docker 的網路和儲存、虛擬化是相當複雜的,Docker 的技術點比較多,在本章中將會深入介紹 Docker 的各方面,期待能夠幫助讀者加深對 Docker 的理解。

本文為作者的 Kubernetes 系列電子書的一部分,電子書已經開源,歡迎關注,電子書瀏覽地址:

https://k8s.whuanle.cn【適合國內訪問】

https://ek8s.whuanle.cn 【gitbook】

容器化應用

什麼是容器化應用

containerized applications 指容器化的應用,我們常常說使用映象打包應用程式,使用 Docker 釋出、部署應用程式,那麼當你的應用成功在 Docker 上執行時,稱這個應用是 containerized applications。

應用怎麼打包

容器化應用的最主要特徵是使用映象打包應用的執行環境以及應用程式,可以通過 Docker 啟動這個映象,進而將 應用程式啟動起來。

將一個應用程式打包為映象,大約分為以下過程:

  • 編寫 Dockerfile 檔案 -- 定義構建映象的流程
  • 選擇一個基礎映象(作業系統) -- 作業系統
  • 安裝應用的需要的環境 -- 執行環境
  • 複製程式檔案 -- 應用程式
  • 啟動 Dockerfile -- 生成映象
作業系統執行環境Web程式(C#)Ubuntu 18.04.NET Core Runtime3.1安裝執行環境作業系統執行環境Web程式(C#)

Docker 映象組成

以 .NET Core(C#) 程式為例,一個 Docker 映象的層次如下圖所示:

docker_netcore_level

在 Docker 映象中,作業系統是高度精簡的,可能只有一個精簡的 Shell,甚至沒有 Shell。而且映象中的作業系統還不包含核心,容器都是共享所在的宿主機的核心。所以有時會說容器僅包含必要的作業系統(通常只有作業系統檔案和檔案系統物件),容器中檢視到的 Linux 核心版本與宿主機一致。

Docker 映象的是由一系統檔案組成的。

docker_dotnet

聯合檔案系統

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

unionfs

Docker 中,層層組成映象的技術也是聯合檔案系統,Union File System。Docker 映象中的作業系統是根檔案系統,在上一小節的圖片中,可以看到有 bin、boot 等目錄。我們都知道,Docker 映象是由多層檔案組成的,在上面的示例圖片中有三層組成:根檔案系統、環境依賴包、應用程式檔案。當映象層生成後,便不能被修改,如果再進行操作,則會在原來的基礎上生成新的映象層,層層聯合,最終生成映象。當然生成的映象可能會因為層數太多或者操作過多,導致出現大量冗餘,映象臃腫。

Docker 的映象分層是受 Linux Unionfs 啟發而開發的,Docker 支援多種檔案聯合系統,如 AUFS、OverlayFS、VFS 等。

Docker 在不同系統中可以選擇的聯合檔案系統:

Linux發行版 推薦的儲存驅動程式 替代驅動程式
Ubuntu overlay2 overlay devicemapperaufszfs,vfs
Debian overlay2 overlaydevicemapperaufs,vfs
CentOS overlay2 overlaydevicemapperzfs,vfs

 提示

Docker Desktop for Mac 和 Docker Desktop for Windows 不支援修改儲存驅動程式,只能使用預設儲存驅動程式。

Linux 核心

既然 Docker 容器需要與 Linux 核心結合才能使用,那麼我們看一下 Linux 核心的功能,稍微瞭解一下 Linux 核心在支撐 Docker 容器運作中起到什麼作用。

Linux 核心主要包含以下功能:

  • 記憶體管理:追蹤記錄有多少記憶體儲存了什麼以及儲存在哪裡;

  • 程式管理:確定哪些程式可以使用中央處理器(CPU)、何時使用以及持續多長時間;

  • 裝置驅動程式:充當硬體與程式之間的調解程式/解釋程式;

  • 系統呼叫和安全防護:接受程式請求呼叫系統服務;

  • 檔案系統:作業系統中負責管理持久資料的子系統,在 Linux 中,一切皆檔案。

Linux 層次結構如下:

linux_kernel

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

linux_docker

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_client_server

容器執行時

容器執行時是提供執行環境並啟動容器的軟體,我們最常聽說的是 Docker,此外還有 containerdCRI-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 ,請點選:

https://github.com/kubernetes/community/blob/master/contributors/devel/sig-node/container-runtime-interface.md

Docker 引擎

Docker 引擎也可以說是 Docker Server,它由 Docker 守護程式(Docker daemon)、containerd 以及 runc 組成。

當使用 Docker client 輸入命令時,命令會被髮送到 Docker daemon ,daemon 會偵聽請求並管理 Docker 物件,daemon 可以管理 映象、容器、網路和儲存卷等。

下面這個圖是新 Docker 版本的結構組成。

docker_daemon

Docker 引擎變化

Docker 首次釋出時,Docker 引擎由兩個核心元件構成:LXC 和 Docker daemon,這也是很多文章中稱 Docker 是基於 LXC 的原因,舊版本的 Docker 利用了 LXC、cgroups、Linux 核心編寫。接下來我們瞭解一下 LXC 。

LXC (Linux Container)是 Linux 提供的一種核心虛擬化技術,可以提供輕量級的虛擬化,以便隔離程式和資源,它是作業系統層面上的虛擬化技術。LXC 提供了對諸如名稱空間(namespace) 和控制組(cgroups) 等基礎工具的操作能力,它們是基於 Linux 核心的容器虛擬化技術。我們不需要深入瞭解這個東西。

docker_lxc

Docker 一開始是使用 LXC 做的,LXC 是一個很牛逼的開源專案,但是隨著 Docker 的成熟,Docker 開始拋棄 LXC,自己動手手撕容器引擎。

為什麼 Docker 要拋棄 LXC 呢?首先,LXC 是基於 Linux 的。這對於一個立志於跨平臺的 Docker 來說是個問題,離開 LXC,怎麼在 MAC、Windows 下執行?其次,如此核心的元件依賴於外部工具,這會給專案帶來巨大風險,甚至影響其發展。

 

哈哈哈。。。其實筆者覺得不支援 Windows 也罷。。。

Docker 引擎的架構

下面是一張 Docker 的架構圖。

docker_struct

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

 部落格推薦

筆者在查閱資料時,發現了這個大佬的部落格,在這個大佬的部落格中學會了很多東西。

部落格推薦:https://iximiuz.com/en/

在後面的節中,我們將繼續瞭解 Docker 中的網路和儲存,並開始探究與 Kubernetes 相關的知識點。

相關文章