- 互聯網企業容器技術實踐
- 龔曦主編
- 4487字
- 2019-07-25 11:39:34
1.2 Docker核心原理
??1.2.2 Docker服務流程
Docker 服務運行的流程如圖 1-2 所示。Docker 客戶端與 Docker 守護進程通信,Docker守護進程負責構建、運行和分發Docker容器。Docker客戶端和守護進程可以在同一個系統上運行,也可以將Docker客戶端連接到遠程Docker守護進程。Docker客戶端和守護進程使用REST API通過UNIX套接字或網絡接口進行通信。

圖1-2 Docker服務運行的流程
??1.2.3 Docker核心技術
前文提到 Docker 主要是使用了一些已有的技術實現的,主要的核心技術是 Cgroup+Namespaces和UnionFS。
1.Namespaces
命名空間(Namespaces)是 Linux 系統提供的一種內核級別的環境隔離方法,Docker 就是利用的這種技術來實現容器環境隔離的。Linux Namespace提供了對UTS、IPC、mount、PID、network、User等的隔離機制。
(1)UTS Namespace
UTS Namespace主要是用來隔離hostname和domainname兩個系統標識的,可以通過Go語言創建一個UTS Namespace,如下所示。
上述代碼需要在Root權限下執行。在編譯并執行后進入新的UTS Namespace。
當前PID為20011,再通過pstree查看其進程樹,可知其父進程是20006。下面驗證是否在同一個UTS Namespace下。
可以看到當前進程和父進程不在一個UTS Namespace下,父進程和1號進程在同一個UTS Namespace下,說明當前UTS Namespace是新創建的。下面測試UTS Namespace對hostname的隔離功能。
可以看到,在新的UTS Namespace下先修改hostname為docker,查看修改成功,退出當前Namespace,或者打開一個新的終端,查看hostname還是原來的hostname,外部的hostname并沒有受到影響。由此可以看到UTS Namespace的隔離作用。
(2)IPC Namespace
IPC Namespace主要提供了進程間通信的隔離能力。同樣可以用Go語言實現IPC Namespace的創建。代碼與創建 UTS Namespace 的基本相同,只要把標識符 CLONE_NEWUTS 換成CLONE_NEWIPC即可。
同樣,編譯后在Root環境下啟動后就可以進入新的IPC Namespace。在此Namespace下,可以查看IPC Namespace與進程1的IPC Namespace不同,說明是新創建的IPC Namespace。
此時,在此被隔離的ipc namespace中創建一個消息隊列。
在新建的IPC Namespace中已經有了一條消息隊列0x9ea09b5b。另外再起一個終端,確認是否能夠看到剛創建的消息隊列。
確實無法看到剛創建的消息隊列,說明消息隊列確實被IPC Namespace隔離起來了。
(3)PID Namespace
PID Namespace用來隔離進程。同樣的進程在不同的PID Namespace下擁有不同的PID,通過代碼創建一個新的PID Namespace,同樣只需要把UTS Namespace的代碼做少量修改,把標識符修改為CLONE_NEWPID即可。
依舊編譯后在Root下啟動,通過命令查看在新的PID Namespace下此進程的PID,如下所示。
如果使用ps命令查看,看到的還是所有的進程,因為/proc文件系統(procfs)沒有掛載到與原/proc不同的位置。如果只想看到PID Namespace本身應該看到的進程,則需要重新掛載/proc,如下所示。
可以看到在PID Namespace中只有bash和ps進程。進程通過Namespace隔離,需要注意的是,由于沒有進行Mount Namespace的隔離,當退出當前Namespace再執行ps命令時,系統會報錯,需要將/proc目錄重新“mount”回去。
(4)Network Namespace
Network Namespace在Docker中被用來隔離網絡。Network Namespace可以讓每個容器擁有自己的網絡設備、端口、IP地址等。因為其網絡是完全隔離的,所以每個Namespace下的端口不會產生任何沖突。既然完全隔離了,容器與外部之間需要通信該怎么辦?在Docker中可以通過虛擬網橋來實現。在Linux系統中,可以直接通過命令創建一個Network Namespace。當然,為了與上文保持一致,仍然通過代碼來實現。代碼實現方式與上文幾個 Namespace 一樣,只是把標識符改為CLONE_NEWNET,然后將代碼編譯并執行,通過ifconfig命令查看其Namespace下的網絡設備,如下所示。
可以看到里面并沒有任何的網絡設備。然后在宿主機中查看宿主機的網絡設備,可見Network Namespace網絡隔離成功。
(5)Mount Namespace
Mount Namespace用來隔離各個進程看到的掛載點視圖。在不同Namespace中的進程看到的文件系統層次是不一樣的,不同的 Namespace 進行 mount 和 umount 操作只會對自己的Namespace內的文件系統產生影響,對其他的Namespace沒有影響。
由于沒有進行 Mount Namespace,所以退出 PID Namespace 時才會報錯。所以在 PID Namespace的基礎上再加上Mount Namespace進行測試。這個可以直接在PID Namespace的代碼基礎上再加上Mount Namespace的標識符。因為Mount Namespace是Linux 實現的第一個Namesapce 類型,其標識符比較特殊,是CLONE_NEWNS,代碼如下:
測試方式是在Root下編譯并執行的,在此Namespace下執行掛載/proc并執行ps命令,如下所示。
另起一終端,執行如下 ps 命令,可見兩個不同的 Namespace 下的/proc 并不一樣,說明mount已經隔離成功,在新的Namespace下的mount操作并沒有影響到外部的Namespace下的系統文件。
(6)User Namespace
User Namespace 可以用來隔離用戶的用戶組ID。在不同的User Namespace下進程的User ID和Group ID是不同的。同樣通過Go語言來實現創建一個新的User Namespace,代碼與上面的基本相同,修改一個標識符為CLONE_NEWUSER即可。
首先在Root環境下查看宿主機當前的用戶和用戶組:
可以看到是root 用戶。運行一下程序,創建新的User Namespace,在新的Namespace下查看用戶和用戶組。
2.Cgroups
(1)什么是Cgroups
Cgroups是Linux系統中提供的對一組進程及其子進程進行資源(CPU、內存、存儲和網絡等)限制、控制和統計的能力。Cgroup可以直接通過操作Cgroup文件系統的方式完成使用。例如,使用mount-t cgroup cgroup/cgroup命令進行操作,此時就會在/cgroup下生成很多默認的文件,這就創建一個Cgroup,在這個目錄下每創建一個目錄就表示創建了一個子Cgroup。進入子目錄會發現里面會生成一些文件與上層 Cgroup 即/cgroup 目錄內容大致相同。這就是Cgroup文件系統的樹形層次結構。
創建完Cgroup之后,可以為其分配可用的資源并將不同的進程放進去。當創建完第一個Cgroup時,系統會把所有的進程都放到主Cgroup中,可以查看Cgroup中的tasks文件來查看此 Cgroup 中的進程 PID;同樣可以通過在 tasks 中添加對應的進程 PID,會把該進程放入該Cgroup中。但需要注意,如果在子Cgroup中添加一個進程,則子Cgroup的上層Cgroup中的tasks文件中也會有這個PID,因為子Cgroup屬于上層Cgroup,所以子Cgroup中的進程也同時會屬于上層Cgroup,但是同一層級的Cgroup卻不能同時擁有同一個進程。比如A和BCgroup同屬于C的子Cgroup,那么A和B就不能同時擁有同一個進程。至于每個Cgroup中的資源配置量都是通過設置當前Cgroup的子系統來配置的。
Cgroups為不同的資源定義了各自的Cgroup 子系統,來實現對資源的具體管理。Cgroup實現的子系統及其實現的功能如下。
? devices:設備權限控制。
? cpuset:分配指定的CPU和內存節點。
? cpu:控制CPU占用率。
? cpuacct:統計CPU的使用情況。
? memory:限制內存的使用上限。
? freezer:暫停Cgroup中的進程。
? net_cls:配合traffic controller限制網絡帶寬。
? net_prio:可以動態控制每個網卡流量的優先級。
? blkio:限制進程的塊設備 I/O。
? ns:使不同Cgroups中的進程使用不同的Namespace。
(2)Cgroups的使用
前文提到可以使用mount-t cgroup cgroup/cgroup命令創建cgroups,其中可以通過-o參數添加子系統,如命令mount-t cgroup-o cpu、cpuset、memory cgroup/cgroup。這個命令表示創建了一個名為Cgroup的層級,并附加了cpu、cpuset、memory三個子系統,并把層級掛載到/cgroup目錄上。但實際執行命令時會報出already mounted錯誤,且執行不成功。這是因為該命令一般在Linux發行版啟動時就已經執行了,對應的子系統的Cgroup已經被創建并掛載了。并且雖然cgroupfs可以掛載在任意目錄中,但是標準掛載點是/sys/fs/cgroup目錄并且在啟動時已經掛載上了,所以一般并不需要執行該命令。由于系統的/sys/fs/cgroup目錄已經掛載了各種cgroupfs,可以直接在該Cgroup上進行操作。
首先查看/sys/fs/cgroup,如下所示。
可以看到/sys/fs/cgroup 目錄下有很多子目錄,分別對應擁有對應子系統的 Cgroup,以cpuset為例,查看cpuset目錄,如下所示。
可以看到里面有很多的控制文件,其中以cpuset開頭的是cpuset子系統產生的,剩下的是由Cgroup產生的。前文已經提到過默認所有進程的PID都是在Cgroup的根目錄的tasks文件中,通過mkdir創建一個childA目錄,就創建了一個子Cgroup,如下所示。
接著進入childA目錄對該子Cgroup進行配置,可以通過修改文件的方式進行配置,如下所示。
兩個命令分別表示限制Cgroup里的進程只能在0號CPU上運行,并只會從0號內存節點分配內存。接下來是給Cgroup分配進程,上文也已經提到通過修改tasks的方式把進程添加到當前Cgroup中,如下所示。
上面的命令表示把當前進程添加到Cgroup中,其中$$變量表示當前進程的PID。這時進程的所有子進程也會被自動地添加到Cgroup中,并受到該Cgoup資源的限制。
3.UnionFS
(1)什么是UnionFS
UnionFS(聯合文件系統)是把不同物理位置的目錄合并到同一個目錄中的文件系統服務。其早期是應用在LiveCD領域的,通過聯合文件系統可以非??焖俚匾龑到y初始化或檢測磁盤等硬件資源。這是因為只需要把CD只讀掛載到特定目錄,然后在其上附加一層可讀寫的文件層,對文件的任何變動修改都會被添加到新的文件層內,這種技術被稱為寫時復制。
寫時復制是一種可以有效節約資源的技術,它被很好地應用在Docker鏡像上。其思想是如果有一個資源需要被重復利用,在沒有任何修改的情況下,新舊實例會共享資源,并不需要進行復制,如果有實例需要對資源進行任何的修改,并不會直接修改資源,而是會創建一個新的資源并在其上進行修改,這樣原來的資源并不會進行任何修改,而是與新創建的資源結合在外,表現為修改后的資源狀態。這樣做可以顯著地減輕對未修改資源的復制而帶來的資源消耗問題。下面通過講解在Docker中如何使用UnionFS更深入地理解寫時復制。
Docker支持的第一種UnionFS是AUFS,下面主要從鏡像層和容器層兩個方面介紹AUFS在Docker中的使用。
(2)AUFS在Images中的使用
Docker鏡像是由一層層的只讀層組合而成的。鏡像層的內容存儲在/var/lib/docker/aufs/diff目錄下,在/var/lib/docker/aufs/layers目錄下則存放著對應的metadata,描述鏡像需要的層。
拉取一個ubuntu:latest鏡像,看到ubuntu:latest鏡像是分為5層的,可以在/var/lib/docker/aufs/diff目錄中確認。
可以看到在本地沒有別的鏡像的情況下,目錄中確實有5層,而這5層是如何組合成鏡像的呢?來看/var/lib/docker/aufs/layers中的文件:
由于層ID太長,截取前5個字符表示一下,可以看出e85c0文件中包含了剩下的所有的層,說明其是在文件的最上層依賴于下面的所有層;而824dc文件是空的所以是最基礎、最底層的,所以整個鏡像文件從最高層到最底層依次是e85c0→47c4e→ed7b3→66915→824dc,再分別查看各目錄的文件,如下所示。
根據上面各層的文件目錄,可以推斷出,在build ubuntu:latest的鏡像過程中,最近的一次文件的修改是 run 目錄下的,按時間順序鏡像推算,修改的文件所屬目錄為 bin,boot 等->etc,sbin,usr,var->var->etc->run。具體是不是這樣呢?可以驗證一下,如下是 ubuntu 鏡像的Dockerfile。
從Dockerfile可以看到在制作ubuntu鏡像的過程中共執行了5次文件修改命令,也可以看到該鏡像是由5層組成的,每一層正好對應一次的文件修改。按照順序文件,修改得越早,產生越早越在底層。由于在ADD添加一個壓縮文件時會自動解壓為目錄,所以最開始修改的文件是bin、boot等,對應的是第一個ADD中添加的壓縮文件ubuntu-artful-core-cloudimg-amd64-root.tar.gz解壓出來的目錄。再看第二條命令中一共修改了/usr、/sbin、/etc、/var這些目錄文件,正好和第二層修改的文件目錄相同,剩下的每一層的目錄也正好和Dockerfile中修改的文件目錄一一對應。由此可以確定,在Build鏡像中對文件的每一次修改都會增加一個文件層包含著對應修改后的文件。而當進行刪除文件操作時,也會創建一個新的層,在新層里創建一個特殊名稱隱藏文件,這樣的一個隱藏文件對應著一個文件的刪除。
下面在ubuntu:latest鏡像的基礎上創建一個新的鏡像test,首先創建一個test文件。然后編輯Dockerfile,生成鏡像。
test鏡像已經創建成功,進入/var/lib/docker/aufs目錄下查看鏡像層的變化。
可以看到只增加了一層,多了一個test文件,前4層被ubuntu和test兩個鏡像復用了,從而減少了磁盤的空間占用。
(3)AUFS在Container中的使用
AUFS在容器中的使用和在鏡像中略有不同。在鏡像中一個文件進行了改動,需要將整個文件進行復制,這樣會對容器的性能產生一定的影響。在容器中,每一個鏡像層最多只需要復制一次,后續的改動都會在復制的容器層上面進行,不需要再復制生成新的容器層。
首先在服務器上啟動一個容器,查看文件層發生了哪些變化,如下所示。
發現多了兩個文件層,其中的init文件層中主要存放與容器內環境相關的內容,這是一個只讀文件層,另外一個文件層是一個可讀寫文件層,用來存儲容器的寫操作。當容器內部文件發生任何改變時,改變后的文件就會存儲在這層文件系統中,當容器停止時它們仍然是存在的,只有容器被刪除時才會刪除這兩個文件層。
同時還有一個/var/lib/docker/aufs/mnt 目錄用來存放容器的 mount 目錄,與/var/lib/docker/aufs/diff目錄內容保持一致,當容器停止運行時,這些目錄仍然是存在的,但是目錄里面是空的,因為AUFS只在容器運行時才會把/var/lib/docker/aufs/diff中的內容映射過來。