容器基礎-隔離與限制

容器其實是一種沙盒技術。顧名思義,沙盒就是能夠像一個集裝箱一樣,把你的應用“裝”起來的技術。
這樣,應用與應用之間,就因爲有了邊界而不至於相互干擾;
而被裝進集裝箱的應用,也可以被方便地搬來搬去,
這不就是 PaaS 最理想的狀態嘛。

"程序"被執行起來,它就從磁盤上的二進制文件,變成了計算機內存中的數據、寄存器裏的值、堆棧中的指令、被打開的文件,以及各種設備的狀態信息的一個集合。
像這樣一個程序運行起來後的計算機執行環境的總和,就是:進程

容器技術的核心功能,就是通過約束和修改進程的動態表現,從而爲其創造出一個“邊界”。

對於 Docker 等大多數 Linux 容器來說,Cgroups 技術是用來製造約束的主要手段,而 Namespace 技術則是用來修改進程視圖的主要方法

在docker容器中執行ps命令,可以看到我們在 Docker 裏最開始執行的 /bin/sh,
就是這個容器內部的第 1 號進程(PID=1),
而這個容器裏一共只有兩個進程在運行。
這就意味着,前面執行的 /bin/sh,以及我們剛剛執行的 ps,
已經被 Docker 隔離在了一個跟宿主機完全不同的世界當中

img

在宿主機中執行/bin/sh如下,PID是1536

img

這種機制,其實就是對被隔離應用的進程空間做了手腳,使得這些進程只能看到重新計算過的進程編號,比如 PID=1。
可實際上,他們在宿主機的操作系統裏,還是原來的第 1536號進程。

這種技術,就是 Linux 裏面的 Namespace 機制。
在 Linux 系統中創建進程的系統調用是 clone(),比如:

 int pid = clone(main_function, stack_size, SIGCHLD, NULL); 

這個系統調用就會爲我們創建一個新的進程,並且返回它的進程號 pid。

當我們用 clone() 系統調用創建一個新進程時,就可以在參數中指定 CLONE_NEWPID 參數,比如

int pid = clone(main_function, stack_size, CLONE_NEWPID | SIGCHLD, NULL); 

除了我們剛剛用到的 PID Namespace,Linux 操作系統還提供了 Mount、UTS、IPC、Network 和 User 這些 Namespace,用來對各種不同的進程上下文進行“障眼法”操作。
比如,Mount Namespace,用於讓被隔離進程只看到當前 Namespace 裏的掛載點信息;Network Namespace,用於讓被隔離進程看到當前 Namespace 裏的網絡設備和配置。
就是 Linux 容器最基本的實現原理了。
所以說,容器,其實是一種特殊的進程而已。

img

在理解了 Namespace 的工作方式之後,你就會明白,跟真實存在的虛擬機不同,
在使用 Docker 的時候,並沒有一個真正的“Docker 容器”運行在宿主機裏面。
Docker 項目幫助用戶啓動的,還是原來的應用進程,
只不過在創建這些進程時,Docker 爲它們加上了各種各樣的 Namespace 參數。
這些進程就會覺得自己是各自 PID Namespace 裏的第 1 號進程,
只能看到各自 Mount Namespace 裏掛載的目錄和文件,
只能訪問到各自 Network Namespace 裏的網絡設備,
就彷彿運行在一個個“容器”裏面。

基於 Linux Namespace 的隔離機制相比於虛擬化技術也有很多不足之處,其中最主要的問題就是:隔離得不徹底
首先,既然容器只是運行在宿主機上的一種特殊的進程,那麼多個容器之間使用的就還是同一個宿主機的操作系統內核。

在 Linux 內核中,有很多資源和對象是不能被 Namespace 化的,最典型的例子就是:時間。
如果在一個容器中修改了系統時間,那麼整個宿主機的時間都會被修改。

Linux Cgroups 就是 Linux 內核中用來爲進程設置資源限制的一個重要功能。Linux Cgroups 的全稱是 Linux Control Group。它最主要的作用,就是限制一個進程組能夠使用的資源上限,包括 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 帶寬。

容器是一個“單進程”模型

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

Docker 項目來說,它最核心的原理實際上就是爲待創建的用戶進程:
啓用 Linux Namespace 配置;
設置指定的 Cgroups 參數;
切換進程的根目錄(Change Root)。

img

總結:

1、虛擬機 是硬件隔離,因爲hypervisor 虛擬一系列硬件資源

2、容器是 進程級隔離,依靠NameSpace 機制實現進程間的隔離

3、容器的資源限制,依靠Linux Cgroups,它就是限制一個進程組能夠使用的資源上限,包括 CPU、內存、磁盤、網絡帶寬等等。

4、容器只是運行在宿主機上的一種特殊的進程,那麼多個容器之間使用的就還是同一個宿主機的操作系統內核。

5、通過exec在容器中執行啓動的後臺進程,實際不受docker控制(回收和生命週期),只有PID=1的受控制。

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