作者 | 唐華敏(華敏) 阿裡雲容器平台技術專家
本文整理自《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等技術領域、聚焦雲原生流行技術趨勢、雲原生大規模的落地實踐,做最懂雲原生開發者的技術公衆号。”
更多相關内容,請關注“
阿裡巴巴雲原生”。