【深入剖析Kubernetes】容器基礎(二):隔離與限制

 

 

在上一篇文章中,我詳細介紹了 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 目錄下:

 


root@ubuntu:/sys/fs/cgroup/cpu$ mkdir container
root@ubuntu:/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 核數、可用內存等信息都是宿主機上的數據,這會給應用的運行帶來非常大的困惑和風險。

這也是在企業中,容器化應用碰到的一個常見問題,也是容器相較於虛擬機另一個不盡如人意的地方。

 

 

思考題

你是否知道如何修復容器中的 top 指令以及 /proc 文件系統中的信息呢?(提示:lxcfs)

在從虛擬機向容器環境遷移應用的過程中,你還遇到哪些容器與虛擬機的不一致問題?

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章