1. 程式人生 > >docker原理 ---- 容器的隔離與限制

docker原理 ---- 容器的隔離與限制

在上一篇文章中,我詳細介紹了 Linux 容器中用來實現“隔離”的技術手段:Namespace。而通過這些講解,你應該能夠明白,Namespace 技術實際上修改了應用程序看待整個計算機“檢視”,即它的“視線”被作業系統做了限制,只能“看到”某些指定的內容。但對於宿主機來說,這些被“隔離”了的程序跟其他程序並沒有太大區別。

說到這一點,相信你也能夠知道我在上一篇文章最後給你留下的第一個思考題的答案了:在之前虛擬機器與容器技術的對比圖裡,不應該把 Docker Engine 或者任何容器管理工具放在跟 Hypervisor 相同的位置,因為它們並不像 Hypervisor 那樣對應用程序的隔離環境負責,也不會建立任何實體的“容器”,真正對隔離環境負責的是宿主機作業系統本身:

所以,在這個對比圖裡,我們應該把 Docker 畫在跟應用同級別並且靠邊的位置。這意味著,使用者執行在容器裡的應用程序,跟宿主機上的其他程序一樣,都由宿主機作業系統統一管理,只不過這些被隔離的程序擁有額外設定過的 Namespace 引數。而 Docker 專案在這裡扮演的角色,更多的是旁路式的輔助和管理工作。

我在後續分享 CRI 和容器執行時的時候還會專門介紹到,其實像 Docker 這樣的角色甚至可以去掉。

這樣的架構也解釋了為什麼 Docker 專案比虛擬機器更受歡迎的原因。

這是因為,使用虛擬化技術作為應用沙盒,就必須要由 Hypervisor 來負責建立虛擬機器,這個虛擬機器是真實存在的,並且它裡面必須執行一個完整的 Guest OS 才能執行使用者的應用程序。這就不可避免地帶來了額外的資源消耗和佔用。

根據實驗,一個執行著 CentOS 的 KVM 虛擬機器啟動後,在不做優化的情況下,虛擬機器自己就需要佔用 100~200 MB 記憶體。此外,使用者應用執行在虛擬機器裡面,它對宿主機作業系統的呼叫就不可避免地要經過虛擬化軟體的攔截和處理,這本身又是一層效能損耗,尤其對計算資源、網路和磁碟 I/O 的損耗非常大。

而相比之下,容器化後的使用者應用,卻依然還是一個宿主機上的普通程序,這就意味著這些因為虛擬化而帶來的效能損耗都是不存在的;而另一方面,使用 Namespace 作為隔離手段的容器並不需要單獨的 Guest OS,這就使得容器額外的資源佔用幾乎可以忽略不計。

所以說,“敏捷”和“高效能”是容器相較於虛擬機器最大的優勢,也是它能夠在 PaaS 這種更細粒度的資源管理平臺上大行其道的重要原因。

不過,有利就有弊,基於 Linux Namespace 的隔離機制相比於虛擬化技術也有很多不足之處,其中最主要的問題就是:隔離得不徹底。

首先,既然容器只是執行在宿主機上的一種特殊的程序,那麼多個容器之間使用的就還是同一個宿主機的作業系統核心。

儘管你可以在容器裡通過 Mount Namespace 單獨掛載其他不同版本的作業系統檔案,比如 CentOS 或者 Ubuntu,但這並不能改變共享宿主機核心的事實。這意味著,如果你要在 Windows 宿主機上執行 Linux 容器,或者在低版本的 Linux 宿主機上執行高版本的 Linux 容器,都是行不通的。

而相比之下,擁有硬體虛擬化技術和獨立 Guest OS 的虛擬機器就要方便得多了。最極端的例子是,Microsoft 的雲端計算平臺 Azure,實際上就是執行在 Windows 伺服器叢集上的,但這並不妨礙你在它上面建立各種 Linux 虛擬機器出來。

其次,在 Linux 核心中,有很多資源和物件是不能被 Namespace 化的,最典型的例子就是:時間。

這就意味著,如果你的容器中的程式使用 settimeofday(2) 系統呼叫修改了時間,整個宿主機的時間都會被隨之修改,這顯然不符合使用者的預期。相比於在虛擬機器裡面可以隨便折騰的自由度,在容器裡部署應用的時候,“什麼能做,什麼不能做”,就是使用者必須考慮的一個問題。

此外,由於上述問題,尤其是共享宿主機核心的事實,容器給應用暴露出來的攻擊面是相當大的,應用“越獄”的難度自然也比虛擬機器低得多。

更為棘手的是,儘管在實踐中我們確實可以使用 Seccomp 等技術,對容器內部發起的所有系統呼叫進行過濾和甄別來進行安全加固,但這種方法因為多了一層對系統呼叫的過濾,一定會拖累容器的效能。何況,預設情況下,誰也不知道到底該開啟哪些系統呼叫,禁止哪些系統呼叫。

所以,在生產環境中,沒有人敢把執行在物理機上的 Linux 容器直接暴露到公網上。當然,我後續會講到的基於虛擬化或者獨立核心技術的容器實現,則可以比較好地在隔離與效能之間做出平衡。

在介紹完容器的“隔離”技術之後,我們再來研究一下容器的“限制”問題。

也許你會好奇,我們不是已經通過 Linux Namespace 建立了一個“容器”嗎,為什麼還需要對容器做“限制”呢?

我還是以 PID Namespace 為例,來給你解釋這個問題。

雖然容器內的第 1 號程序在“障眼法”的干擾下只能看到容器裡的情況,但是宿主機上,它作為第 100 號程序與其他所有程序之間依然是平等的競爭關係。這就意味著,雖然第 100 號程序表面上被隔離了起來,但是它所能夠使用到的資源(比如 CPU、記憶體),卻是可以隨時被宿主機上的其他程序(或者其他容器)佔用的。當然,這個 100 號程序自己也可能把所有資源吃光。這些情況,顯然都不是一個“沙盒”應該表現出來的合理行為。

而Linux Cgroups 就是 Linux 核心中用來為程序設定資源限制的一個重要功能。

有意思的是,Google 的工程師在 2006 年發起這項特性的時候,曾將它命名為“程序容器”(process container)。實際上,在 Google 內部,“容器”這個術語長期以來都被用於形容被 Cgroups 限制過的程序組。後來 Google 的工程師們說,他們的 KVM 虛擬機器也執行在 Borg 所管理的“容器”裡,其實也是執行在 Cgroups“容器”當中。這和我們今天說的 Docker 容器差別很大。

Linux Cgroups 的全稱是 Linux Control Group。它最主要的作用,就是限制一個程序組能夠使用的資源上限,包括 CPU、記憶體、磁碟、網路頻寬等等。

此外,Cgroups 還能夠對程序進行優先順序設定、審計,以及將程序掛起和恢復等操作。在今天的分享中,我只和你重點探討它與容器關係最緊密的“限制”能力,並通過一組實踐來帶你認識一下 Cgroups。

在 Linux 中,Cgroups 給使用者暴露出來的操作介面是檔案系統,即它以檔案和目錄的方式組織在作業系統的 /sys/fs/cgroup 路徑下。在 Ubuntu 16.04 機器裡,我可以用 mount 指令把它們展示出來,這條命令是:

$ mount -t cgroup  cpuset on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset) cpu on /sys/fs/cgroup/cpu type cgroup (rw,nosuid,nodev,noexec,relatime,cpu) cpuacct on /sys/fs/cgroup/cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpuacct) blkio on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio) memory on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory) ... 它的輸出結果,是一系列檔案系統目錄。如果你在自己的機器上沒有看到這些目錄,那你就需要自己去掛載 Cgroups,具體做法可以自行 Google。

可以看到,在 /sys/fs/cgroup 下面有很多諸如 cpuset、cpu、 memory 這樣的子目錄,也叫子系統。這些都是我這臺機器當前可以被 Cgroups 進行限制的資源種類。而在子系統對應的資源種類下,你就可以看到該類資源具體可以被限制的方法。比如,對 CPU 子系統來說,我們就可以看到如下幾個配置檔案,這個指令是:

$ ls /sys/fs/cgroup/cpu cgroup.clone_children cpu.cfs_period_us cpu.rt_period_us  cpu.shares notify_on_release cgroup.procs      cpu.cfs_quota_us  cpu.rt_runtime_us cpu.stat  tasks 如果熟悉 Linux CPU 管理的話,你就會在它的輸出裡注意到 cfs_period 和 cfs_quota 這樣的關鍵詞。這兩個引數需要組合使用,可以用來限制程序在長度為 cfs_period 的一段時間內,只能被分配到總量為 cfs_quota 的 CPU 時間。

而這樣的配置檔案又如何使用呢?

你需要在對應的子系統下面建立一個目錄,比如,我們現在進入 /sys/fs/cgroup/cpu 目錄下:

[email protected]:/sys/fs/cgroup/cpu$ mkdir container [email protected]:/sys/fs/cgroup/cpu$ ls container/ cgroup.clone_children cpu.cfs_period_us cpu.rt_period_us  cpu.shares notify_on_release cgroup.procs      cpu.cfs_quota_us  cpu.rt_runtime_us cpu.stat  tasks 這個目錄就稱為一個“控制組”。你會發現,作業系統會在你新建立的 container 目錄下,自動生成該子系統對應的資源限制檔案。

現在,我們在後臺執行這樣一條指令碼:

$ while : ; do : ; done & [1] 226 顯然,它執行了一個死迴圈,可以把計算機的 CPU 吃到 100%,根據它的輸出,我們可以看到這個指令碼在後臺執行的程序號(PID)是 226。

這樣,我們可以用 top 指令來確認一下 CPU 有沒有被打滿:

$ top %Cpu0 :100.0 us, 0.0 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st 在輸出裡可以看到,CPU 的使用率已經 100% 了(%Cpu0 :100.0 us)。

而此時,我們可以通過檢視 container 目錄下的檔案,看到 container 控制組裡的 CPU quota 還沒有任何限制(即:-1),CPU period 則是預設的 100 ms(100000 us):

$ cat /sys/fs/cgroup/cpu/container/cpu.cfs_quota_us  -1 $ cat /sys/fs/cgroup/cpu/container/cpu.cfs_period_us  100000 接下來,我們可以通過修改這些檔案的內容來設定限制。

比如,向 container 組裡的 cfs_quota 檔案寫入 20 ms(20000 us):

$ echo 20000 > /sys/fs/cgroup/cpu/container/cpu.cfs_quota_us 結合前面的介紹,你應該能明白這個操作的含義,它意味著在每 100 ms 的時間裡,被該控制組限制的程序只能使用 20 ms 的 CPU 時間,也就是說這個程序只能使用到 20% 的 CPU 頻寬。

接下來,我們把被限制的程序的 PID 寫入 container 組裡的 tasks 檔案,上面的設定就會對該程序生效了:

$ echo 226 > /sys/fs/cgroup/cpu/container/tasks  我們可以用 top 指令檢視一下:

$ top %Cpu0 : 20.3 us, 0.0 sy, 0.0 ni, 79.7 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st 可以看到,計算機的 CPU 使用率立刻降到了 20%(%Cpu0 : 20.3 us)。

除 CPU 子系統外,Cgroups 的每一項子系統都有其獨有的資源限制能力,比如:

blkio,為​​​塊​​​設​​​備​​​設​​​定​​​I/O 限​​​制,一般用於磁碟等裝置; cpuset,為程序分配單獨的 CPU 核和對應的記憶體節點; memory,為程序設定記憶體使用的限制。 Linux Cgroups 的設計還是比較易用的,簡單粗暴地理解呢,它就是一個子系統目錄加上一組資源限制檔案的組合。而對於 Docker 等 Linux 容器專案來說,它們只需要在每個子系統下面,為每個容器建立一個控制組(即建立一個新目錄),然後在啟動容器程序之後,把這個程序的 PID 填寫到對應控制組的 tasks 檔案中就可以了。

而至於在這些控制組下面的資原始檔裡填上什麼值,就靠使用者執行 docker run 時的引數指定了,比如這樣一條命令:

$ docker run -it --cpu-period=100000 --cpu-quota=20000 ubuntu /bin/bash 在啟動這個容器後,我們可以通過檢視 Cgroups 檔案系統下,CPU 子系統中,“docker”這個控制組裡的資源限制檔案的內容來確認:

$ cat /sys/fs/cgroup/cpu/docker/5d5c9f67d/cpu.cfs_period_us  100000 $ cat /sys/fs/cgroup/cpu/docker/5d5c9f67d/cpu.cfs_quota_us  20000 這就意味著這個 Docker 容器,只能使用到 20% 的 CPU 頻寬。

總結 在這篇文章中,我首先介紹了容器使用 Linux Namespace 作為隔離手段的優勢和劣勢,對比了 Linux 容器跟虛擬機器技術的不同,進一步明確了“容器只是一種特殊的程序”這個結論。

除了建立 Namespace 之外,在後續關於容器網路的分享中,我還會介紹一些其他 Namespace 的操作,比如看不見摸不著的 Linux Namespace 在計算機中到底如何表示、一個程序如何“加入”到其他程序的 Namespace 當中,等等。

緊接著,我詳細介紹了容器在做好了隔離工作之後,又如何通過 Linux Cgroups 實現資源的限制,並通過一系列簡單的實驗,模擬了 Docker 專案建立容器限制的過程。

通過以上講述,你現在應該能夠理解,一個正在執行的 Docker 容器,其實就是一個啟用了多個 Linux Namespace 的應用程序,而這個程序能夠使用的資源量,則受 Cgroups 配置的限制。

這也是容器技術中一個非常重要的概念,即:容器是一個“單程序”模型。

由於一個容器的本質就是一個程序,使用者的應用程序實際上就是容器裡 PID=1 的程序,也是其他後續建立的所有程序的父程序。這就意味著,在一個容器中,你沒辦法同時執行兩個不同的應用,除非你能事先找到一個公共的 PID=1 的程式來充當兩個不同應用的父程序,這也是為什麼很多人都會用 systemd 或者 supervisord 這樣的軟體來代替應用本身作為容器的啟動程序。

但是,在後面分享容器設計模式時,我還會推薦其他更好的解決辦法。這是因為容器本身的設計,就是希望容器和應用能夠同生命週期,這個概念對後續的容器編排非常重要。否則,一旦出現類似於“容器是正常執行的,但是裡面的應用早已經掛了”的情況,編排系統處理起來就非常麻煩了。

另外,跟 Namespace 的情況類似,Cgroups 對資源的限制能力也有很多不完善的地方,被提及最多的自然是 /proc 檔案系統的問題。

眾所周知,Linux 下的 /proc 目錄儲存的是記錄當前核心執行狀態的一系列特殊檔案,使用者可以通過訪問這些檔案,檢視系統以及當前正在執行的程序的資訊,比如 CPU 使用情況、記憶體佔用率等,這些檔案也是 top 指令檢視系統資訊的主要資料來源。

但是,你如果在容器裡執行 top 指令,就會發現,它顯示的資訊居然是宿主機的 CPU 和記憶體資料,而不是當前容器的資料。

造成這個問題的原因就是,/proc 檔案系統並不知道使用者通過 Cgroups 給這個容器做了什麼樣的資源限制,即:/proc 檔案系統不瞭解 Cgroups 限制的存在。

在生產環境中,這個問題必須進行修正,否則應用程式在容器裡讀取到的 CPU 核數、可用記憶體等資訊都是宿主機上的資料,這會給應用的執行帶來非常大的困惑和風險。這也是在企業中,容器化應用碰到的一個常見問題,也是容器相較於虛擬機器另一個不盡如人意的地方。