Docker 的實現原理剖析

Docker 的發展歷史


Docker 公司前身是 DotCloud,由 Solomon Hykes 在2010年成立,2013年更名 Docker。同年發佈了 Docker-compose 組件提供容器的編排工具。2014年 Docker 發佈1.0版本,2015年Docker 提供 Docker-machine,支持 windows 平臺。


在此期間,Docker 項目在開源社區大受追捧,同時也被業界詬病的是 Docker 公司對於 Docker 發展具有絕對的話語權,比如 Docker 公司推行了 libcontainer  難以被社區接受。於是在一同貢獻 Docker 代碼的公司諸如 Redhat,谷歌的倡導下,成立了 OCI 開源社區,旨在於將 Docker 的發展權利迴歸社區,當然反過來講,Docker 公司也希望更多的廠商安心貢獻代碼到Docker 項目,促進 Docker 項目的發展。於是通過OCI建立了 runc 項目,替代 libcontainer,這爲開發者提供了除 Docker 之外的容器化實現的選擇。


至2017年,Docker 項目轉移到 Moby 項目,基於 Moby 項目,Docker 提供了兩種發行版,Docker CE 和 Docker EE, Docker CE 就是目前大家普遍使用的版本,Docker EE 成爲付費版本,提供了容器的編排,Service 等概念。Docker 公司承諾 Docker 的發行版會基於 Moby 項目。這樣一來,通過 Moby 項目,你也可以自己打造一個定製化的容器引擎,而不會被 Docker 公司綁定。




Docker 底層的技術



Docker 的誤解:Docker 是輕量級的虛擬機。


上圖大家看到最多的一張 Docker 和虛擬機對比的圖,從圖上看來,確實看起來 Docker 實現了類似於虛擬化的技術,能夠讓應用跑在一些輕量級的容器裏。這麼理解其實是錯誤的。實際上 Docker 是使用了很多 Linux 的隔離功能,讓容器看起來像一個輕量級虛擬機在獨立運行,容器的本質是被限制了的 Namespaces,cgroup,具有邏輯上獨立文件系統,網絡的一個進程。其底層運用瞭如下 Linux 的能力:



 Namespaces


這裏提出一個問題,在宿主機上啓動兩個容器,在這兩個容器內都各有一個 PID=1的進程,衆所周知,Linux 裏 PID 是唯一的,既然 Docker 不是跑在宿主機上的兩個虛擬機,那麼它是如何實現在宿主機上運行兩個相同 PID 的進程呢?


這裏就用到了 Linux Namespaces,它其實是 Linux 創建新進程時的一個可選參數,在 Linux 系統中創建進程的系統調用是 clone()方法。



通過調用這個方法,這個進程會獲得一個獨立的進程空間,它的 pid 是1,並且看不到宿主機上的其他進程,這也就是在容器內執行 PS 命令的結果。


不僅僅是 PID,當你啓動啓動容器之後,Docker 會爲這個容器創建一系列其他 namespaces。


這些 namespaces 提供了不同層面的隔離。容器的運行受到各個層面 namespace 的限制。


Docker Engine 使用了以下 Linux 的隔離技術:

  • The pid namespace: 管理 PID 命名空間 (PID: Process ID).

  • The net namespace: 管理網絡命名空間(NET: Networking).

  • The ipc namespace: 管理進程間通信命名空間(IPC: InterProcess Communication).

  • The mnt namespace: 管理文件系統掛載點命名空間 (MNT: Mount).

  • The uts namespace: Unix 時間系統隔離. (UTS: Unix Timesharing System).

通過這些技術,運行時的容器得以看到一個和宿主機上其他容器隔離的環境。


Cgroups


在容器環境裏,如果不做限制,運行一個 Java 應用,當應用有 Bug 導致 JVM 進行 Full GC 的時候,會佔用大量的內存,也可能導致 CPU 飆升到100%,如何避免由單個容器的問題,導致整個集羣不可用?Docker 用到了 Cgroups。Cgroups 是 Control Group 的縮寫,由2007年穀歌工程師研發,2008年併入 Linux Kernel 2.6.24,由 C 語言編寫。


Docker 底層使用 groups 對進程進行 CPU,Mem,網絡等資源的使用限制,從而實現在宿主機上的資源分配,不至於出現一個容器佔用所有宿主機的 CPU 或者內存。


具體的實現原理如上圖所示,通過使用 cgroups,爲不同的進程設定不同的配額,上圖中 cgroup1 的進程只能使用60%的 CPU,cgroup2 使用20%的 CPU,同樣可以爲進程設定容器。所以當你在 Kubernetes 裏爲某個 pod 聲明 CPU 限額時,底層就是調用的 cgroup 的設置。具體實現如下:


進入宿主機 cgroup 目錄: cd /sys/fs/cgroup/cpu


爲進程創建一個目錄,例如 my_container, 然後 cgroup 會自動在這個目錄下創建多個默認配置:



在cpu.cfs_quota_us裏輸入對應的數值,即可實現對 CPU 的配額設置:


echo 60000 > /sys/fs/cgroup/cpu/container/cpu.cfs_ q uota_us,這裏的單位是毫秒,意思是每一毫秒內,該進程能夠使用60%的 CPU 時間。如何將該配置應用到進程裏?


echo 22880 > /sys/fs/cgroup/cpu/container/task s,其中22880是宿主機上的 PID,這時候你通過調用 top 命令,能夠看到該進程的 CPU 使用率不會超過60%,這樣就能實現對進程的限制,避免之前的問題發生。


UnionFS


容器有了進程隔離(視野隔離),CGroup 資源隔離,還缺少隔離的文件系統。試想,容器 A 的應用如果能直接訪問到容器 B 的文件,會造成非常混亂的局面。爲了解決這個問題,Docker 默認使用了 AuFS(Advanced Union FS) 來支持 Docker 鏡像的 Layer,也支持其他 UnionFS 的版本。


UnionFS 的用法

在這個例子中,-o 是傳入的目錄參數, none 表示不會掛載任何驅動,最後是目標目錄。執行的結果,就是 merged-folder 目錄下會包含 fd1, fd2 兩個目錄的內容。在 Docker 的實現中,執行 Docker info 可以看到 aufs 的路徑:

那麼,如何使用 Aufs 的呢?可以回憶下,當我們用 Docker pull 時候的場景:

我們下載一個 Docker 鏡像的時候,爲什麼會很多層?而且每一層的 id 是對應了什麼內容?


Docker的鏡像存儲即用到了 aufs 技術。Docker 鏡像的每一層都是隻讀的,如果需要對鏡像增加內容,Docker 會使用 aufs 掛載一個 branch,指向diff 的目錄,同時會給這一層 layer 設定一個唯一 id,這也就是爲什麼我們 pull 一個鏡像的時候,會有多個帶 id 的 layer 會被下載。


在 /var/lib/docker/aufs 目錄下,可以找到 /diff,/layers/, /mnt 目錄, 

/diff 管理 Docker 鏡像每一層的內容。

/layer 管理Docker 鏡像的元數據,層級關係。

/mnt 管理掛載點,通常對應一個鏡像,或者 layer,用於描述一個容器鏡像的所有層級內容。


Runc


之前提到,爲了防止 Docker 這項開源技術被Docker 公司控制,在幾個核心貢獻的廠商,社區推動下成立了 OCI 社區。OCI 社區提供了 runc 的維護,而 runc 是基於 OCI 規範的運行容器的工具。換句話說,你可以通過 runc,提供自己的容器實現,而不需要依賴 Docker。當然,Docker 的發行版底層也是用的 runc。在 Docker 宿主機上執行 runc,你會發現它的大多數命令和 Docker 命令類似,感興趣的讀者可以自己實踐如何用 runc 啓動容器。



總結


至此,本文總結了 Docker 的發展歷史,以及底層的實現,希望能夠激發大家對 Docker 底層技術瞭解的興趣。




參考資料

  • https://docs.docker.com/storage/storagedriver/aufs-driver/#how-the-aufs-storage-driver-works

  • https://github.com/opencontainers/runc

  • http://www.sel.zju.edu.cn/?p=840



文章作者:王青



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