從零開始入門 K8s | 深入剖析 Linux 容器
作者 | 唐華敏(華敏) 阿里雲容器平臺技術專家
本文整理自《CNCF x Alibaba 雲原生技術公開課》第 15 講。
關注“阿里巴巴雲原生”公眾號,回覆關鍵詞“入門”,即可下載從零入門 K8s 系列文章 PPT。
導讀:Linux 容器是一種輕量級的虛擬化技術,在共享核心的基礎上,基於 namespace 和 cgroup 技術做到程序的資源隔離和限制。本文將會以 docker 為例,介紹容器映象和容器引擎的基本知識。
容器
容器是一種輕量級的虛擬化技術,因為它跟虛擬機器比起來,它少了一層 hypervisor 層。先看一下下面這張圖,這張圖簡單描述了一個容器的啟動過程。
最下面是一個磁碟,容器的映象是儲存在磁碟上面的。上層是一個容器引擎,容器引擎可以是 docker,也可以是其它的容器引擎。引擎向下發一個請求,比如說建立容器,這時候它就把磁碟上面的容器映象執行成在宿主機上的一個程序。
對於容器來說,最重要的是怎麼保證這個程序所用到的資源是被隔離和被限制住的,在 Linux 核心上面是由 cgroup 和 namespace 這兩個技術來保證的。接下來以 docker 為例,詳細介紹一下資源隔離和容器映象兩部分的內容。
一、資源隔離和限制
namespace
namespace 是用來做資源隔離的,在 Linux 核心上有七種 namespace,docker 中用到了前六種。第七種 cgroup namespace 在 docker 本身並沒有用到,但是在 runC 實現中實現了 cgroup namespace。
我們先從頭看一下:
- 第一個是 mout namespace。mout namespace 就是保證容器看到的檔案系統的檢視,是容器映象提供的一個檔案系統,也就是說它看不見宿主機上的其他檔案,除了通過 -v 引數 bound 的那種模式,是可以把宿主機上面的一些目錄和檔案,讓它在容器裡面可見的;
- 第二個是 uts namespace,這個 namespace 主要是隔離了 hostname 和 domain;
- 第三個是 pid namespace,這個 namespace 是保證了容器的 init 程序是以 1 號程序來啟動的;
- 第四個是網路 namespace,除了容器用 host 網路這種模式之外,其他所有的網路模式都有一個自己的 network namespace 的檔案;
- 第五個是 user namespace,這個 namespace 是控制使用者 UID 和 GID 在容器內部和宿主機上的一個對映,不過這個 namespace 用的比較少;
- 第六個是 IPC namespace,這個 namespace 是控制了程序兼通訊的一些東西,比方說訊號量;
- 第七個是 cgroup namespace,上圖右邊有兩張示意圖,分別是表示開啟和關閉 cgroup namespace。用 cgroup namespace 帶來的一個好處是容器中看到的 cgroup 檢視是以根的形式來呈現的,這樣的話就和宿主機上面程序看到的 cgroup namespace 的一個檢視方式是相同的;另外一個好處是讓容器內部使用 cgroup 會變得更安全。
這裡我們簡單用 unshare 示例一下 namespace 創立的過程。容器中 namespace 的建立其實都是用 unshare 這個系統呼叫來建立的。
上圖上半部分是 unshare 使用的一個例子,下半部分是我實際用 unshare 這個命令去建立的一個 pid namespace。可以看到這個 bash 程序已經是在一個新的 pid namespace 裡面,然後 ps 看到這個 bash 的 pid 現在是 1,說明它是一個新的 pid namespace。
cgroup
兩種 cgroup 驅動
cgroup 主要是做資源限制的,docker 容器有兩種 cgroup 驅動:一種是 systemd 的,另外一種是 cgroupfs 的。
cgroupfs 比較好理解。比如說要限制記憶體是多少、要用 CPU share 為多少?其實直接把 pid 寫入對應的一個 cgroup 檔案,然後把對應需要限制的資源也寫入相應的 memory cgroup 檔案和 CPU 的 cgroup 檔案就可以了;
另外一個是 systemd 的一個 cgroup 驅動。這個驅動是因為 systemd 本身可以提供一個 cgroup 管理方式。所以如果用 systemd 做 cgroup 驅動的話,所有的寫 cgroup 操作都必須通過 systemd 的介面來完成,不能手動更改 cgroup 的檔案。
容器中常用的 cgroup
接下來看一下容器中常用的 cgroup。Linux 核心本身是提供了很多種 cgroup,但是 docker 容器用到的大概只有下面六種:
- 第一個是 CPU,CPU 一般會去設定 cpu share 和 cupset,控制 CPU 的使用率;
- 第二個是 memory,是控制程序記憶體的使用量;
- 第三個 device ,device 控制了你可以在容器中看到的 device 裝置;
- 第四個 freezer。它和第三個 cgroup(device)都是為了安全的。當你停止容器的時候,freezer 會把當前的程序全部都寫入 cgroup,然後把所有的程序都凍結掉,這樣做的目的是:防止你在停止的時候,有程序會去做 fork。這樣的話就相當於防止程序逃逸到宿主機上面去,是為安全考慮;
- 第五個是 blkio,blkio 主要是限制容器用到的磁碟的一些 IOPS 還有 bps 的速率限制。因為 cgroup 不唯一的話,blkio 只能限制同步 io,docker io 是沒辦法限制的;
- 第六個是 pid cgroup,pid cgroup 限制的是容器裡面可以用到的最大程序數量。
不常用的 cgroup
也有一部分是 docker 容器沒有用到的 cgroup。容器中常用的和不常用的,這個區別是對 docker 來說的,因為對於 runC 來說,除了最下面的 rdma,所有的 cgroup 其實都是在 runC 裡面支援的,但是 docker 並沒有開啟這部分支援,所以說 docker 容器是不支援下圖這些 cgroup 的。
二、容器映象
docker images
接下來我們講一下容器映象,以 docker 映象為例去講一下容器映象的構成。
docker 映象是基於聯合檔案系統的。簡單描述一下聯合檔案系統,大概的意思就是說:它允許檔案是存放在不同的層級上面的,但是最終是可以通過一個統一的檢視,看到這些層級上面的所有檔案。
如上圖所示,右邊是從 docker 官網拿過來的容器儲存的一個結構圖。
這張圖非常形象地表明瞭 docker 的儲存,docker 儲存也就是基於聯合檔案系統,是分層的。每一層是一個 Layer,這些 Layer 由不同的檔案組成,它是可以被其他映象所複用的。可以看一下,當映象被執行成一個容器的時候,最上層就會是一個容器的讀寫層。這個容器的讀寫層也可以通過 commit 把它變成一個映象頂層最新的一層。
docker 映象的儲存,它的底層是基於不同的檔案系統的,所以它的儲存驅動也是針對不同的檔案系統作為定製的,比如 AUFS、btrfs、devicemapper 還有 overlay。docker 對這些檔案系統做了一些相對應的 graph driver 的驅動,通過這些驅動把映象存在磁碟上面。
以 overlay 為例
儲存流程
接下來我們以 overlay 這個檔案系統為例,看一下 docker 映象是怎麼在磁碟上進行儲存的。
先看一下下面這張圖,簡單地描述了 overlay 檔案系統的工作原理。
最下層是一個 lower 層,也就是映象層,它是一個只讀層;
右上層是一個 upper 層,upper 是容器的讀寫層,upper 層採用了寫實複製的機制,也就是說只有對某些檔案需要進行修改的時候才會從 lower 層把這個檔案拷貝上來,之後所有的修改操作都會對 upper 層的副本進行修改;
upper 並列的有一個 workdir,它的作用是充當一箇中間層的作用。也就是說,當對 upper 層裡面的副本進行修改時,會先放到 workdir,然後再從 workdir 移到 upper 裡面去,這個是 overlay 的工作機制;
最上面的是 mergedir,是一個統一檢視層。從 mergedir 裡面可以看到 upper 和 lower 中所有資料的整合,然後我們 docker exec 到容器裡面,看到一個檔案系統其實就是 mergedir 統一檢視層。
檔案操作
接下來我們講一下基於 overlay 這種儲存,怎麼對容器裡面的檔案進行操作?
先看一下讀操作,容器剛創建出來的時候,upper 其實是空的。這個時候如果去讀的話,所有資料都是從 lower 層讀來的。
寫操作如剛才所提到的,overlay 的 upper 層有一個寫實資料的機制,對一些檔案需要進行操作的時候,overlay 會去做一個 copy up 的動作,然後會把檔案從 lower 層拷貝上來,之後的一些寫修改都會對這個部分進行操作。
然後看一下刪除操作,overlay 裡面其實是沒有真正的刪除操作的。它所謂的刪除其實是通過對檔案進行標記,然後從最上層的統一檢視層去看,看到這個檔案如果做標記,就會讓這個檔案顯示出來,然後就認為這個檔案是被刪掉的。這個標記有兩種方式:
- 一種是 whiteout 的方式;
- 第二個就是通過設定目錄的一個擴充套件許可權,通過設定擴充套件引數來做到目錄的刪除。
操作步驟
接下來看一下實際用 docker run 去啟動 busybox 的容器,它的 overlay 的掛載點是什麼樣子的?
第二張圖是 mount,可以看到這個容器 rootfs 的一個掛載,它是一個 overlay 的 type 作為掛載的。裡面包括了 upper、lower 還有 workdir 這三個層級。
然後看一下容器裡面新檔案的寫入。docker exec 去建立一個新檔案,diff 這個從上面可以看到,是它的一個 upperdir。再看 upperdir 裡面有這個檔案,檔案裡面的內容也是 docker exec 寫入的。
最後看一下最下面的是 mergedir,mergedir 裡面整合的 upperdir 和 lowerdir 的內容,也可以看到我們寫入的資料。
三、容器引擎
containerd 容器架構詳解
接下來我們基於 CNCF 的一個容器引擎上的 containerd,來講一下容器引擎大致的構成。下圖是從 containerd 官網拿過來的一張架構圖,基於這張架構圖先簡單介紹一下 containerd 的架構。
上圖如果把它分成左右兩邊的話,可以認為 containerd 提供了兩大功能。
第一個是對於 runtime,也就是對於容器生命週期的管理,左邊 storage 的部分其實是對一個映象儲存的管理。containerd 會負責進行的拉取、映象的儲存。
按照水平層次來看的話:
第一層是 GRPC,containerd 對於上層來說是通過 GRPC serve 的形式來對上層提供服務的。Metrics 這個部分主要是提供 cgroup Metrics 的一些內容;
下面這層的左邊是容器映象的一個儲存,中線 images、containers 下面是 Metadata,這部分 Matadata 是通過 bootfs 儲存在磁碟上面的。右邊的 Tasks 是管理容器的容器結構,Events 是對容器的一些操作都會有一個 Event 向上層發出,然後上層可以去訂閱這個 Event,由此知道容器狀態發生什麼變化;
最下層是 Runtimes 層,這個 Runtimes 可以從型別區分,比如說 runC 或者是安全容器之類的。
shim v1/v2 是什麼
接下來講一下 containerd 在 runtime 這邊的大致架構。下面這張圖是從 kata 官網拿過來的,上半部分是原圖,下半部分加了一些擴充套件示例,基於這張圖我們來看一下 containerd 在 runtime 這層的架構。
如圖所示:按照從左往右的一個順序,從上層到最終 runtime 執行起來的一個流程。
我們先看一下最左邊,最左邊是一個 CRI Client。一般就是 kubelet 通過 CRI 請求,向 containerd 傳送請求。containerd 接收到容器的請求之後,會經過一個 containerd shim。containerd shim 是管理容器生命週期的,它主要負責兩方面:
- 第一個是它會對 io 進行轉發;
- 第二是它會對訊號進行傳遞。
圖的上半部分畫的是安全容器,也就是 kata 的一個流程,這個就不具體展開了。下半部分,可以看到有各種各樣不同的 shim。下面介紹一下 containerd shim 的架構。
一開始在 containerd 中只有一個 shim,也就是藍色框框起來的 containerd-shim。這個程序的意思是,不管是 kata 容器也好、runc 容器也好、gvisor 容器也好,上面用的 shim 都是 containerd。
後面針對不同型別的 runtime,containerd 去做了一個擴充套件。這個擴充套件是通過 shim-v2 這個 interface 去做的,也就是說只要去實現了這個 shim-v2 的 interface,不同的 runtime 就可以定製不同的 shim。比如:runC 可以自己做一個 shim,叫 shim-runc;gvisor 可以自己做一個 shim 叫 shim-gvisor;像上面 kata 也可以自己去做一個 shim-kata 的 shim。這些 shim 可以替換掉上面藍色框的 containerd-shim。
這樣做的好處有很多,舉一個比較形象的例子。可以看一下 kata 這張圖,它上面原先如果用 shim-v1 的話其實有三個元件,之所以有三個元件的原因是因為 kata 自身的一個限制,但是用了 shim-v2 這個架構後,三個元件可以做成一個二進位制,也就是原先三個元件,現在可以變成一個 shim-kata 元件,這個可以體現出 shim-v2 的一個好處。
containerd 容器架構詳解 - 容器流程示例
接下來我們以兩個示例來詳細解釋一下容器的流程是怎麼工作的,下面的兩張圖是基於 containerd 的架構畫的一個容器的工作流程。
start 流程
先看一下容器 start 的流程:
這張圖由三個部分組成:
- 第一個部分是容器引擎部分,容器引擎可以是 docker,也可以是其它的;
- 兩個虛線框框起來的 containerd 和 containerd-shim,它們兩個是屬於 containerd 架構的部分;
- 最下面就是 container 的部分,這個部分是通過一個 runtime 去拉起的,可以認為是 shim 去操作 runC 命令建立的一個容器。
先看一下這個流程是怎麼工作的,圖裡面也標明瞭 1、2、3、4。這個 1、2、3、4 就是 containerd 怎麼去建立一個容器的流程。
首先它會去建立一個 matadata,然後會去發請求給 task service 說要去建立容器。通過中間一系列的元件,最終把請求下發到一個 shim。containerd 和 shim 的互動其實也是通過 GRPC 來做互動的,containerd 把建立請求發給 shim 之後,shim 會去呼叫 runtime 建立一個容器出來,以上就是容器 start 的一個示例。
exec 流程
接下來看下面這張圖是怎麼去 exec 一個容器的。
和 start 流程非常相似,結構也大概相同,不同的部分其實就是 containerd 怎麼去處理這部分流程。和上面的圖一樣,我也在圖中標明瞭 1、2、3、4,這些步驟就代表了 containerd 去做 exec 的一個先後順序。
由上圖可以看到:exec 的操作還是發給 containerd-shim 的。對容器來說,去 start 一個容器和去 exec 一個容器,其實並沒有本質的區別。
最終的一個區別無非就是:是否對容器中跑的程序做一個 namespace 的建立。
- exec 的時候,需要把這個程序加入到一個已有的 namespace 裡面;
- start 的時候,容器程序的 namespace 是需要去專門建立。
本文總結
最後希望各位同學看完本文後,能夠對 Linux 容器有更深刻的瞭解。這裡為大家簡單總結一下本文的內容:
- 容器如何用 namespace 做資源隔離以及 cgroup 做資源限制;
- 簡單介紹了基於 overlay 檔案系統的容器映象儲存;
- 以 docker+containerd 為例介紹了容器引擎如何工作的。
“ 阿里巴巴雲原生微信公眾號(ID:Alicloudnative)關注微服務、Serverless、容器、Service Mesh等技術領域、聚焦雲原生流行技術趨勢、雲原生大規模的落地實踐,做最懂雲原生開發者的技術公眾號。”
更多相關內容,請關注“阿里巴巴雲原生”