作者| 阿里巴巴高級開發工程師 傅偉
一、容器與鏡像
什么是容器?
在介紹容器的具體概念之前,先簡單回顧一下操作系統是如何管理進程的。
首先,當我們登錄到操作系統之后,可以通過 ps 等操作看到各式各樣的進程,這些進程包括系統自帶的服務和用戶的應用進程。那么,這些進程都有什么樣的特點?
- 第一,這些進程可以相互看到、相互通信;
- 第二,它們使用的是同一個文件系統,可以對同一個文件進行讀寫操作;
- 第三,這些進程會使用相同的系統資源。
這樣的三個特點會帶來什么問題呢?
- 因為這些進程能夠相互看到並且進行通信,高級權限的進程可以攻擊其他進程;
- 因為它們使用的是同一個文件系統,因此會帶來兩個問題:這些進程可以對於已有的數據進行增刪改查,具有高級權限的進程可能會將其他進程的數據刪除掉,破壞掉其他進程的正常運行;此外,進程與進程之間的依賴可能會存在沖突,如此一來就會給運維帶來很大的壓力;
- 因為這些進程使用的是同一個宿主機的資源,應用之間可能會存在資源搶占的問題,當一個應用需要消耗大量 CPU 和內存資源的時候,就可能會破壞其他應用的運行,導致其他應用無法正常地提供服務。
針對上述的三個問題,如何為進程提供一個獨立的運行環境呢?
- 針對不同進程使用同一個文件系統所造成的問題而言,Linux 和 Unix 操作系統可以通過 chroot 系統調用將子目錄變成根目錄,達到視圖級別的隔離;進程在 chroot 的幫助下可以具有獨立的文件系統,對於這樣的文件系統進行增刪改查不會影響到其他進程;
- 因為進程之間相互可見並且可以相互通信,使用 Namespace 技術來實現進程在資源的視圖上進行隔離。在 chroot 和 Namespace 的幫助下,進程就能夠運行在一個獨立的環境下了;
- 但在獨立的環境下,進程所使用的還是同一個操作系統的資源,一些進程可能會侵蝕掉整個系統的資源。為了減少進程彼此之間的影響,可以通過 Cgroup 來限制其資源使用率,設置其能夠使用的 CPU 以及內存量。
那么,應該如何定義這樣的進程集合呢?
其實,容器就是一個視圖隔離、資源可限制、獨立文件系統的進程集合。所謂“視圖隔離”就是能夠看到部分進程以及具有獨立的主機名等;控制資源使用率則是可以對於內存大小以及 CPU 使用個數等進行限制。容器就是一個進程集合,它將系統的其他資源隔離開來,具有自己獨立的資源視圖。
容器具有一個獨立的文件系統,因為使用的是系統的資源,所以在獨立的文件系統內不需要具備內核相關的代碼或者工具,我們只需要提供容器所需的二進制文件、配置文件以及依賴即可。只要容器運行時所需的文件集合都能夠具備,那么這個容器就能夠運行起來。
什么是鏡像?
綜上所述,我們將這些容器運行時所需要的所有的文件集合稱之為容器鏡像。
那么,一般都是通過什么樣的方式來構建鏡像的呢?通常情況下,我們會采用 Dockerfile 來構建鏡像,這是因為 Dockerfile 提供了非常便利的語法糖,能夠幫助我們很好地描述構建的每個步驟。當然,每個構建步驟都會對已有的文件系統進行操作,這樣就會帶來文件系統內容的變化,我們將這些變化稱之為 changeset。當我們把構建步驟所產生的變化依次作用到一個空文件夾上,就能夠得到一個完整的鏡像。
changeset 的分層以及復用特點能夠帶來幾點優勢:
- 第一,能夠提高分發效率,簡單試想一下,對於大的鏡像而言,如果將其拆分成各個小塊就能夠提高鏡像的分發效率,這是因為鏡像拆分之后就可以並行下載這些數據;
- 第二,因為這些數據是相互共享的,也就意味着當本地存儲上包含了一些數據的時候,只需要下載本地沒有的數據即可,舉個簡單的例子就是 golang 鏡像是基於 alpine 鏡像進行構建的,當本地已經具有了 alpine 鏡像之后,在下載 golang 鏡像的時候只需要下載本地 alpine 鏡像中沒有的部分即可;
- 第三,因為鏡像數據是共享的,因此可以節約大量的磁盤空間,簡單設想一下,當本地存儲具有了 alpine 鏡像和 golang 鏡像,在沒有復用的能力之前,alpine 鏡像具有 5M 大小,golang 鏡像有 300M 大小,因此就會占用 305M 空間;而當具有了復用能力之后,只需要 300M 空間即可。
如何構建鏡像?
如下圖所示的 Dockerfile 適用於描述如何構建 golang 應用的。
如圖所示:
- FROM 行表示以下的構建步驟基於什么鏡像進行構建,正如前面所提到的,鏡像是可以復用的;
- WORKDIR 行表示會把接下來的構建步驟都在哪一個相應的具體目錄下進行,其起到的作用類似於 Shell 里面的 cd;
- COPY 行表示的是可以將宿主機上的文件拷貝到容器鏡像內;
- RUN 行表示在具體的文件系統內執行相應的動作。當我們運行完畢之后就可以得到一個應用了;
- CMD 行表示使用鏡像時的默認程序名字。
當有了 Dockerfile 之后,就可以通過 docker build 命令構建出所需要的應用。構建出的結果存儲在本地,一般情況下,鏡像構建會在打包機或者其他的隔離環境下完成。
那么,這些鏡像如何運行在生產環境或者測試環境上呢?這時候就需要一個中轉站或者中心存儲,我們稱之為 docker registry,也就是鏡像倉庫,其負責存儲所有產生的鏡像數據。我們只需要通過 docker push 就能夠將本地鏡像推動到鏡像倉庫中,這樣一來,就能夠在生產環境上或者測試環境上將相應的數據下載下來並運行了。
如何運行容器?
運行一個容器一般情況下分為三步:
- 第一步:從鏡像倉庫中將相應的鏡像下載下來;
- 第二步:當鏡像下載完成之后就可以通過 docker images 來查看本地鏡像,這里會給出一個完整的列表,我們可以在列表中選中想要的鏡像;
- 第三步:當選中鏡像之后,就可以通過 docker run 來運行這個鏡像得到想要的容器,當然可以通過多次運行得到多個容器。一個鏡像就相當於是一個模板,一個容器就像是一個具體的運行實例,因此鏡像就具有了一次構建、到處運行的特點。
小結
簡單回顧一下,容器就是和系統其它部分隔離開來的進程集合,這里的其他部分包括進程、網絡資源以及文件系統等。而鏡像就是容器所需要的所有文件集合,其具備一次構建、到處運行的特點。
二、容器的生命周期
容器運行時的生命周期
容器是一組具有隔離特性的進程集合,在使用 docker run 的時候會選擇一個鏡像來提供獨立的文件系統並指定相應的運行程序。這里指定的運行程序稱之為 initial 進程,這個 initial 進程啟動的時候,容器也會隨之啟動,當 initial 進程退出的時候,容器也會隨之退出。
因此,可以認為容器的生命周期和 initial 進程的生命周期是一致的。當然,因為容器內不只有這樣的一個 initial 進程,initial 進程本身也可以產生其他的子進程或者通過 docker exec 產生出來的運維操作,也屬於 initial 進程管理的范圍內。當 initial 進程退出的時候,所有的子進程也會隨之退出,這樣也是為了防止資源的泄漏。
但是這樣的做法也會存在一些問題,首先應用里面的程序往往是有狀態的,其可能會產生一些重要的數據,當一個容器退出被刪除之后,數據也就會丟失了,這對於應用方而言是不能接受的,所以需要將容器所產生出來的重要數據持久化下來。容器能夠直接將數據持久化到指定的目錄上,這個目錄就稱之為數據卷。
數據卷有一些特點,其中非常明顯的就是數據卷的生命周期是獨立於容器的生命周期的,也就是說容器的創建、運行、停止、刪除等操作都和數據卷沒有任何關系,因為它是一個特殊的目錄,是用於幫助容器進行持久化的。簡單而言,我們會將數據卷掛載到容器內,這樣一來容器就能夠將數據寫入到相應的目錄里面了,而且容器的退出並不會導致數據的丟失。
通常情況下,數據卷管理主要有兩種方式:
- 第一種是通過 bind 的方式,直接將宿主機的目錄直接掛載到容器內;這種方式比較簡單,但是會帶來運維成本,因為其依賴於宿主機的目錄,需要對於所有的宿主機進行統一管理。
- 第二種是將目錄管理交給運行引擎。
三、容器項目架構
moby 容器引擎架構
moby 是目前最流行的容器管理引擎,moby daemon 會對上提供有關於容器、鏡像、網絡以及 Volume的管理。moby daemon 所依賴的最重要的組件就是 containerd,containerd 是一個容器運行時管理引擎,其獨立於 moby daemon ,可以對上提供容器、鏡像的相關管理。
containerd 底層有 containerd shim 模塊,其類似於一個守護進程,這樣設計的原因有幾點:
- 首先,containerd 需要管理容器生命周期,而容器可能是由不同的容器運行時所創建出來的,因此需要提供一個靈活的插件化管理。而 shim 就是針對於不同的容器運行時所開發的,這樣就能夠從 containerd 中脫離出來,通過插件的形式進行管理。
- 其次,因為 shim 插件化的實現,使其能夠被 containerd 動態接管。如果不具備這樣的能力,當 moby daemon 或者 containerd daemon 意外退出的時候,容器就沒人管理了,那么它也會隨之消失、退出,這樣就會影響到應用的運行。
- 最后,因為隨時可能會對 moby 或者 containerd 進行升級,如果不提供 shim 機制,那么就無法做到原地升級,也無法做到不影響業務的升級,因此 containerd shim 非常重要,它實現了動態接管的能力。
本節課程只是針對於 moby 進行一個大致的介紹,在后續的課程也會詳細介紹。
四、容器 VS VM
容器和 VM 之間的差異
VM 利用 Hypervisor 虛擬化技術來模擬 CPU、內存等硬件資源,這樣就可以在宿主機上建立一個 Guest OS,這是常說的安裝一個虛擬機。
每一個 Guest OS 都有一個獨立的內核,比如 Ubuntu、CentOS 甚至是 Windows 等,在這樣的 Guest OS 之下,每個應用都是相互獨立的,VM 可以提供一個更好的隔離效果。但這樣的隔離效果需要付出一定的代價,因為需要把一部分的計算資源交給虛擬化,這樣就很難充分利用現有的計算資源,並且每個 Guest OS 都需要占用大量的磁盤空間,比如 Windows 操作系統的安裝需要 10~30G 的磁盤空間,Ubuntu 也需要 5~6G,同時這樣的方式啟動很慢。正是因為虛擬機技術的缺點,催生出了容器技術。
容器是針對於進程而言的,因此無需 Guest OS,只需要一個獨立的文件系統提供其所需要文件集合即可。所有的文件隔離都是進程級別的,因此啟動時間快於 VM,並且所需的磁盤空間也小於 VM。當然了,進程級別的隔離並沒有想象中的那么好,隔離效果相比 VM 要差很多。
總體而言,容器和 VM 相比,各有優劣,因此容器技術也在向着強隔離方向發展。
本文總結
- 容器是一個進程集合,具有自己獨特的視圖視角;
- 鏡像是容器所需要的所有文件集合,其具備一次構建、到處運行的特點;
- 容器的生命周期和 initial 進程的生命周期是一樣的;
- 容器和 VM 相比,各有優劣,容器技術在向着強隔離方向發展。