天天看點

Docker 容器實戰 (六):容器的隔離與限制1 隔離得不徹底2 限制容器3 總結參考

原文作者:javaedge

原文連結:

https://developer.aliyun.com/article/720084?spm=a2c6h.13262185.0.0.5edf441ewzXTaI 更多雲原生技術資訊可關注 阿裡巴巴雲原生技術圈 Linux容器中用來實作“隔離”的技術手段:Namespace。

Namespace實際上修改了應用程序看待整個計算機“視圖”,即它的“視線”被作業系統做了限制,隻能“看到”某些指定的内容

。對于主控端來說,這些被“隔離”了的程序跟其他程序并沒有差別。

在之前虛拟機與容器技術的對比圖裡,不應該把Docker Engine或者任何容器管理工具放在跟Hypervisor相同的位置,因為它們并不像Hypervisor那樣對應用程序的隔離環境負責,也不會建立任何實體的“容器”,真正對隔離環境負責的是主控端作業系統本身:

Docker 容器實戰 (六):容器的隔離與限制1 隔離得不徹底2 限制容器3 總結參考

在這個對比圖裡,應該把Docker畫在跟應用同級别并且靠邊的位置。

使用者運作在容器裡的應用程序,跟主控端上的其他程序一樣,都由主控端作業系統統一管理,隻不過這些被隔離的程序擁有額外設定過的Namespace參數

Docker在這裡更多的是輔助和管理工作。

這樣的架構也解釋了為什麼Docker項目比虛拟機更受歡迎的原因。

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

根據實驗,一個運作着CentOS的KVM虛拟機啟動後,在不做優化的情況下,虛拟機自己就需要占用100~200 MB記憶體。此外,使用者應用運作在虛拟機裡面,它對主控端作業系統的調用就不可避免地要經過虛拟化軟體的攔截和處理,這本身又是一層性能損耗,尤其對計算資源、網絡和磁盤I/O的損耗非常大。

而容器化後的使用者應用,依然還是主控端上的一個普通程序,這就意味着這些因為虛拟化而帶來的性能損耗都是不存在的

使用Namespace作為隔離手段的容器并不需要單獨的Guest OS,這就使得容器額外的資源占用幾乎可以忽略不計。

“靈活”和“高性能”是容器相較于虛拟機最大的優勢

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

1 隔離得不徹底

1.1 多個容器之間使用的還是同一主控端的作業系統核心

盡管可以在容器裡通過

Mount Namespace

單獨挂載其他不同版本的作業系統檔案,比如 CentOS 或者 Ubuntu,但這并不能改變共享主控端核心的事實!

這代表如果要在Windows主控端上運作Linux容器,或者在低版本的Linux主控端上運作高版本的Linux容器,都是impossible!

相比之下,擁有硬體虛拟化技術和獨立Guest OS的虛拟機就要友善

最極端的例子是,Microsoft的雲計算平台Azure,實際上就是運作在Windows伺服器叢集上的,但這并不妨礙你在它上面建立各種Linux虛拟機

1.2 Linux核心中很多資源和對象是不能被Namespace化的

最典型的例子:時間

如果你的容器中的程式使用settimeofday(2)系統調用修改了時間,整個主控端的時間都會被随之修改,這顯然不符合使用者的預期

相比于在虛拟機裡面可以随便折騰,在容器裡部署應用的時候,“什麼能做,什麼不能做”,都是使用者必須考慮的問題。

此外,由于上述問題,尤其是共享主控端核心的事實

1.3 容器給應用暴露出來的攻擊面是相當大的

應用“越獄”的難度自然也比虛拟機低得多。

盡管可以使用Seccomp等技術,過濾和甄别容器内部發起的所有系統調用來進行安全加強,但這就多了一層對系統調用的過濾,一定會拖累容器的性能。何況,預設情況下,誰也不知道到底該開啟哪些系統調用,禁止哪些系統調用。

是以,在生産環境中,沒有人敢把運作在實體機上的Linux容器直接暴露到公網上。

基于虛拟化或者獨立核心技術的容器實作,則可以比較好地在隔離與性能之間做出平衡。

2 限制容器

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路徑下

  • 在筆者的 CentOS7 VM裡,可以用mount指令把它們展示出來
    Docker 容器實戰 (六):容器的隔離與限制1 隔離得不徹底2 限制容器3 總結參考
    它的輸出結果,是一系列檔案系統目錄(如果你在自己的機器上沒有看到這些目錄,那你就需要自己去挂載Cgroups)

在/sys/fs/cgroup下面有很多諸如cpuset、cpu、 memory這樣的子目錄,也叫

子系統

這些都是我這台機器目前可以被Cgroups進行限制的資源種類。

而在子系統對應的資源種類下,你就可以看到該類資源具體可以被限制的方法。

  • 譬如,對CPU子系統來說,就可以看到如下配置檔案
    Docker 容器實戰 (六):容器的隔離與限制1 隔離得不徹底2 限制容器3 總結參考

注意到cfs_period和cfs_quota這樣的關鍵詞,這兩個參數需要組合使用,可用來

限制程序在長度為cfs_period的一段時間内,隻能被配置設定到總量為cfs_quota的CPU時間

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

需要在對應的子系統下面建立一個目錄

比如,我們現在進入/sys/fs/cgroup/cpu目錄下:

Docker 容器實戰 (六):容器的隔離與限制1 隔離得不徹底2 限制容器3 總結參考

這個目錄就稱為一個“控制組”。

OS會在你新建立的container目錄下,自動生成該子系統對應的資源限制檔案!

現在,我們在背景執行這樣一條腳本:

Docker 容器實戰 (六):容器的隔離與限制1 隔離得不徹底2 限制容器3 總結參考

顯然,它執行了一個死循環,可以把計算機的CPU吃到100%,根據它的輸出,我們可以看到這個腳本在背景運作的程序号(PID)

于是,可以用top指令來确認一下CPU有沒有被打滿:

Docker 容器實戰 (六):容器的隔離與限制1 隔離得不徹底2 限制容器3 總結參考

在輸出裡可以看到,CPU的使用率已經100%了(%Cpu0 :100.0 us)。

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

Docker 容器實戰 (六):容器的隔離與限制1 隔離得不徹底2 限制容器3 總結參考
Docker 容器實戰 (六):容器的隔離與限制1 隔離得不徹底2 限制容器3 總結參考

接下來,我們可以通過修改這些檔案的内容來設定限制。

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

Docker 容器實戰 (六):容器的隔離與限制1 隔離得不徹底2 限制容器3 總結參考

結合前面的介紹,你應該能明白這個操作的含義,它意味着在每100 ms的時間裡,被該控制組限制的程序隻能使用20 ms的CPU時間,也就是說這個程序隻能使用到20%的CPU帶寬。

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

Docker 容器實戰 (六):容器的隔離與限制1 隔離得不徹底2 限制容器3 總結參考

我們可以用top指令檢視一下:

Docker 容器實戰 (六):容器的隔離與限制1 隔離得不徹底2 限制容器3 總結參考

可以看到,計算機的CPU使用率立刻降到了20%

除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帶寬。

3 總結

首先介紹了容器使用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核數、可用記憶體等資訊都是主控端上的資料,這會給應用的運作帶來非常大的困惑和風險。

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

參考

  • Docker官網
  • Docker實戰
  • 深入剖析Kubernetes
阿裡巴巴雲原生 關注微服務、Serverless、容器、Service Mesh 等技術領域、聚焦雲原生流行技術趨勢、雲原生大規模的落地實踐,做最懂雲原生開發者的技術圈。”

繼續閱讀