《深入剖析Kubernetes》總結二:學學容器基礎

Linux容器實現手段:Linux Namespace 、Linux Cgroups ,基於 rootfs 的文件系統

Mac容器,Windows容器實現手段:基於虛擬化技術

Linux容器的實現手段

容器其實是一種沙盒技術,能夠像一個集裝箱一樣,把你的應用“裝”起來,使應用與應用之間因爲有了邊界而不至於相互干擾;
而被裝進集裝箱的應用,也可以被方便地搬來搬去;
容器的本質:進程

容器技術的核心功能,就是通過約束和修改進程的動態表現,從而爲其創造出一個“邊界”,Cgroups技術是用來製造約束的主要手段,Namespace 技術則是用來修改進程視圖的主要方法。

  • Namespace

在創建容器進程時,指定了這個進程所需要啓用的一組 Namespace 參數,比如clone()可以在參數中指定 CLONE_NEWPID參數:

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

這樣容器就只能“看”到當前 Namespace 所限定的資源、文件、設備、狀態,或者配置,比如Mount Namespace 裏掛載的目錄和文件, Network Namespace 裏的網絡設備等,而對於宿主機以及其他不相關的程序,它就完全看不到了。

Namespace 的隔離機制相比於虛擬化技術有很多優點,比如不需要運行一個完整的OS 才能執行用戶的應用進程,相對更加高性能和敏捷,而不足之處則是隔離得不徹底:多個容器之間使用的是同一個 宿主機的操作系統內核,在 Windows 宿主機上運行 Linux 容器,或者在低版本的 Linux 宿主機上運行高版本的 Linux 容器,都是行不通的;其次,在 Linux 內核中,有很多資源和對象是不能被 Namespace 化的,最典型的例子就是時間,如果容器中的程序使用settimeofday系統調用修改了時間,整個宿主機的時間都會被隨之修改

  • Cgroups

Cgroups則是 Linux 內核中用來爲進程設置資源限制的一個重要功能,其最主要的作用,就是限制一個進程組能夠 使用的資源上限,包括 CPU、內存、磁盤、網絡帶寬等等,也就是協調容器佔用的資源(此外,Cgroups 還能夠對進程進行優先級設置、審計,以及將進程掛起和恢復等操作)

在 Linux 中,Cgroups 給用戶暴露出來的操作接口是文件系統/sys/fs/cgroup,其不同的子目錄(子系統,如cpuset、cpu、memory)代表可以進行限制的資源類型,這些子目錄下的文件就可以看到具體可以被限制的方法,可以通過修改這些文件的內容來設置限制

而對於 Docker來說,只需要在每個子系統下面爲每個容器創建一個控制組(即創建一個新目錄),然後在啓動容器進程之後,把這個進程的PID填寫到對應控制組的 tasks 文件中就可以了。

Cgroups 對資源的限制能力也有很多不完善的地方,比如/proc 文件系統的問題:
/proc 目錄存儲的是記錄當前內核運行狀態的一系列特殊文件,用戶可以通過訪問這些文件,查看系統以及當前正在運行的進程的信息,比如 CPU 使用情況、內存佔用 率等,這些文件也是 top 指令查看系統信息的主要數據來源;
但是,如果在容器裏執行 top 指令,就會發現它顯示的信息是宿主機的 CPU 和內存數據,而不是當前容器的數據;
原因:/proc 文件系統並不知道用戶通過 Cgroups 給這個容器做了什麼樣 的資源限制,即/proc 文件系統不瞭解 Cgroups 限制的存在;
修復:生產環境中,使用lxcfs來修復

容器鏡像

  • Mount Namespace

首先看看Mount Namespace 跟其他 Namespace 的使用略有不同的地方:它對容器進程視圖的改變,一定是伴隨着掛載操作(mount)才能生效;
也就是說即使開啓了 Mount Namespace,容器進程看到的文件系統也跟宿主機完全一 樣;
因爲Mount Namespace 修改的是容器進程對文件系統“掛載點”的認知,這也就意味着,只有在“掛載”這個操作發生之後,進程的視圖纔會被改變,而在此之前,新創建的容器會直接繼承宿主機的各個掛載點;
因此創建新進程時,除了聲明要啓用 Mount Namespace 之外,還要告訴容器進程,有哪些目錄需要重新掛載

如果想要在創建新容器時,容器進程看到的文件系統就是一個獨立的隔離環境,而不是繼承自宿主機的文件系統,則可以在容器進程啓動之前重新掛載它的整個根目錄“/”。而由於Mount Namespace的存在,這個掛載就對宿主機不可見了;
chroot命令可以實現這個功能,Mount Namespace 則是基於對 chroot 的不斷改良才被髮明出來的,它也是 Linux 操作系統裏的第一個 Namespace;
爲了能夠讓容器的這個根目錄看起來更“真實”,一般會在這個容器的根目錄下掛載 一個完整操作系統的文件系統,比如 Ubuntu16.04 的 ISO。這樣,在容器啓動之後,在容器裏通過執行 “ls /” 查看根目錄下的內容,就是 Ubuntu 16.04 的所有目錄和文件。 而這個掛載在容器根目錄上、用來爲容器進程提供隔離後執行環境的文件系統,就是所謂的“容器鏡像”,或者叫作:rootfs(根文件系統)。

到了這裏,就可以總結Docker容器創建的流程了:

  1. 啓用 Linux Namespace 配置;
  2. 設置指定的 Cgroups 參數;
  3. 切換進程的根目錄(優先使用 pivot_root 系統調用,如果系統不支持,纔會使用 chroot)

不過雖然有了獨立的操作系統的文件、配置、目錄,但是內核卻是共享的,它對於該機器上的所有容器來說是一個“全局變量”,牽一髮而動全身,也算是一個缺陷

  • 一致性

協同開發時,能以增量的方式去做這些修改,而不是每次都生成一個新的rootfs,避免碎片化,類似git

前提技術:聯合文件系統將多個不同位置的目錄聯合掛載(union mount)到同一個目錄下的能力

引入層(layer)得概念,用戶製作鏡像的每一步操作,都會生成一個層,也就是一個增量 rootfs。

例子:比如Ubuntu,拉一個鏡像下來,假如由五個層組成,那麼這五個層就是五個增量 rootfs,每一層都是 Ubuntu操作系統文件與目錄的一部分,層的目錄爲 /var/lib/docker/aufs/diff
而在使用鏡像時,Docker會把這些增量聯合掛載在一個統一的掛載點 /var/lib/docker/aufs/mnt/上,這個目錄裏面正是一個完整的 Ubuntu 操作系統

組成:
在這裏插入圖片描述
第一部分,只讀層。

它是這個容器的 rootfs 最下面的五層,對應的正是 ubuntu:latest 鏡像的五層。

可以看到,它 們的掛載方式都是隻讀的(ro+wh,即 readonly+whiteout),這些層,都以增量的方式分別包含了 Ubuntu 操作系統的一部分。

第二部分,可讀寫層。

它是這個容器的 rootfs 最上面的一層(6e3be5d2ecccae7cc),它的掛載方式爲:rw,即 read write。

在沒有寫入文件之前,這個目錄是空的,而一旦在容器裏做了寫操作,你修改產生的內容就會以增量的方式出現在這個層中。

如果要刪除只讀層裏的一個文件呢,AuFS 會在可讀寫層創建一個 whiteout 文件,把只讀層裏的文 件“遮擋”起來。 比如要刪除只讀層裏一個名叫 foo 的文件,那麼這個刪除操作實際上是在可讀寫層創建了 一個名叫.wh.foo 的文件。這樣,當這兩個層被聯合掛載之後,foo 文件就會被.wh.foo 文 件“遮擋”起來,“消失”了。這個功能,就是“ro+wh”的掛載方式,即只讀 +whiteout 的 含義。

所以可讀寫層的作用,就是專門用來存放修改 rootfs 後產生的增量,無論是 增、刪、改,都發生在這裏;
使用完了這個被修改過的容器之後,還可以使用 docker commit 和 push 指令,保存這個被修改過的可讀寫層,並上傳到 Docker Hub 上,供其他人 使用;
而與此同時,原先的只讀層裏的內容則不會有任何變化,這就是增量 rootfs 的好處。

第三部分,Init 層。

它是一個以“-init”結尾的層,夾在只讀層和讀寫層之間,是 Docker 項目單獨生成的一 個內部層,專門用來存放 /etc/hosts、/etc/resolv.conf 等信息

需要這樣一層的原因是,這些文件本來屬於只讀的 Ubuntu 鏡像的一部分,但是用戶往往需要 在啓動容器時寫入一些指定的值比如 hostname,所以就需要在可讀寫層對它們進行修改。 可是,這些修改往往只對當前的容器有效,我們並不希望執行 docker commit 時,把這些信息 連同可讀寫層一起提交掉。 所以,Docker 做法是,在修改了這些文件之後,以一個單獨的層掛載了出來。而用戶執行 docker commit 只會提交可讀寫層,所以是不包含這些內容的

最終,這 7 個層都被聯合掛載到 /var/lib/docker/aufs/mnt 目錄下,表現爲一個完整的 Ubuntu 操作系統供容器使用。

容器的其他原理

docker exec:一個進程的每種 Linux Namespace,都在它對應的 /proc/[進程號]/ns 下有一個對應的虛擬文件,並且鏈接到一個真實的 Namespace 文件上,也就是說有一個可以得到所有 Linux Namespace 的文件,那麼一個進程就可以通過選擇加入到某個進程已有的 Namespace 當中,從而達到“進 入”這個進程所在容器的目的,此操作爲setns()

docker commit:在容器運行起來後,把最上層的“可讀寫層”,加上原先容器鏡像的只讀層,打包組成了一個新的鏡像,只讀層在宿主機上是共享的,不會佔用額外的空間;而由於使用了聯合文件系統,在容器裏對鏡像 rootfs 所做的任何修改,都會被操作系統先複製到這個可讀寫層,然後再修改,這就是Copy-on-Write

Volume 機制:允許將宿主機上指定的目錄或者文件,掛載到容器裏面進行讀取和修改操作;可以解決兩個問題:容器裏進程新建的文件,怎麼才能讓宿主機獲取到? 宿主機上的文件和目錄,怎麼才能讓容器裏的進程訪問到?;
工作流程:在 rootfs 準備好之後,在執行 chroot 之前(執行 chroot(或者 pivot_root)之前,容器進程一直可以看到宿主機上的整個文件系統),把 Volume 指定的宿主機目錄 (比如 /home 目錄),掛載到指定的容器目錄(比如 /test 目錄)在宿主機上對應的目錄(即 /var/lib/docker/aufs/mnt/[可讀寫層 ID]/test)上,而且由於執行這個掛載操作時,“容器進程”已經創建了,也就意味着此時 Mount Namespace 已經開啓了。所以,這個掛載事件只在這個容器裏可見,宿主機上是看不見容器內部的這個掛載點的。這就保證了容器的隔離性不會被 Volume 打破;
掛載技術:爲Linux 的綁定掛載(Bind Mount)機制,允許將一個目錄或者文件,而不是整個設備,掛載到一個指定的目錄上,並且在該掛載點上進行的任何操作,只是發生在被掛載的目錄或者文件上,而原掛載點的內容則會被隱藏起來且不受影響;綁定掛載實際上是一個 inode 替換的過程(inode和dentry詳情可見博文https://blog.csdn.net/qq_41594698/article/details/103155873,inode可以理解爲存放文件內容的“對象”,而 dentry,也叫目錄項,就是訪問這個 inode 所使用的“指針”)
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-I5kQidMv-1593925327450)(https://i.loli.net/2020/07/05/sxXigWVqAP6kpJv.png)]

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