轉載請注明出處:葡萄城官網,葡萄城為開發者提供專業的開發工具、解決方案和服務,賦能開發者。
第一章:Docker與k8s的恩怨情仇(一)—成為PaaS前浪的Cloud Foundry
第二章:Docker與k8s的恩怨情仇(二)—用最簡單的技術實現“容器”
第四章:Docker與k8s的恩怨情仇(四)-雲原生時代的閉源落幕
Docker與k8s的恩怨情仇(五)——Kubernetes的創新
Docker與k8s的恩怨情仇(六)—— “容器編排”上演“終結者”大片
上一節我們為大家介紹了Cloud Foundry等最初的PaaS平台如何解決容器問題,本文將為大家展示Docker如何解決Cloud Foundry遭遇的一致性和復用性兩個問題,並對比分析Docker和傳統虛擬機的差異。
Docker相比於Cloud Foundry的改進
利用“Mount Namespace”解決一致性問題
在本系列文章的第一節中,我們提到Docker通過Docker 鏡像(Docker Image)功能迅速取代了Cloud Foundry,那這個Docker鏡像到底是什么呢,如何通過為不同的容器使用不同的文件系統以解決一致性問題?先賣個關子,我們先來看看上一節中說過隔離功能和Namespace機制。
Mount Namespace,這個名字中的“Mount”可以讓我們想到這個機制是與文件掛載內容相關的。Mount Namespace是用來隔離進程的掛載目錄的,讓我們可以通過一個“簡單”的例子來看看它是怎么工作的。
(用C語言開發出未實現文件隔離的容器)
上面是一個簡單的的C語言代碼,內容只包括兩個邏輯:
1.在main函數中創建了一個子進程,並且傳遞了一個參數CLONE_NEWNS,這個參數就是用來實現Mount Namespace的;
2.在子進程中調用了/bin/bash命令運行了一個子進程內部的shell。
讓我們編譯並且執行一下這個程序:
gcc -o ns ns.c
./ns
這樣我們就進入了這個子進程的shell中。在這里,我們可以運行ls /tmp查看該目錄的結構,並和宿主機進行一下對比:
(容器內外的/tmp目錄)
我們會發現兩邊展示的數據居然是完全一樣的。按照上一部分Cpu Namespace的結論,應該分別看到兩個不同的文件目錄才對。為什么?
容器內外的文件夾內容相同,是因為我們修改了Mount Namespace。Mount Namespace修改的是進程對文件系統“掛載點”的認知,意思也就是只有發生了掛載這個操作之后生成的所有目錄才會是一個新的系統,而如果不做掛載操作,那就和宿主機的完全一致。
如何解決這個問題,實現文件隔離呢?我們只需要在創建進程時,在聲明Mount Namespace之外,告訴進程需要進行一次掛載操作就可以了。簡單修改一下新進程的代碼,然后運行查看:
(實現文件隔離的代碼和執行效果)
此時文件隔離成功,子進程的/tmp已經被掛載進了tmpfs(一個內存盤)中了,這就相當於創建了完全一個新的tmp環境,因此子進程內部新創建的目錄宿主機中已經無法看到。
上面這點簡單的代碼就是來自Docker鏡像的實現。Docker鏡像在文件操作上本質是對rootfs的一次封裝,Docker將一個應用所需操作系統的rootfs通過Mount Namespace進行封裝,改變了應用程序和操作系統的依賴關系,即原本應用程序是在操作系統內運行的,而Docker把“操作系統”封裝變成了應用程序的依賴庫,這樣就解決了應用程序運行環境一致性的問題。不論在哪里,應用所運行的系統已經成了一個“依賴庫”,這樣就可以對一致性有所保證。
利用“層”解決復用性問題
在實現文件系統隔離,解決一致性問題后,我們還需要面對復用性的問題。在實際使用過程中,我們不大可能每做一個鏡像就掛載一個新的rootfs,費時費力,不帶任何程序的“光盤”也需要占用很大磁盤空間來實現這些內容的掛載。
因此,Docker鏡像使用了另一個技術:UnionFS以及一個全新的概念:層(layer),來優化每一個鏡像的磁盤空間占用,提升鏡像的復用性。
我們先簡單看一下UnionFS是干什么的。UnionFS是一個聯合掛載的功能,它可以將多個路徑下的文件聯合掛載到同一個目錄下。舉個“栗子”,現在有一個如下的目錄結構:
(使用tree命令,查看包含A和B兩個文件夾)
A目錄下有a和x兩個文件,B目錄下有b和x兩個文件,通過UnionFS的功能,我們可以將這兩個目錄掛載到C目錄下,效果如下圖所示:
mount -t aufs -o dirs=./a:./b none ./C
(使用tree命令查看聯合掛載的效果)
最終C目錄下的x只有一份,並且如果我們對C目錄下的a、b、x修改,之前目錄A和B中的文件同樣會被修改。而Docker正是用了這個技術,對其鏡像內的文件進行了聯合掛載,比如可以分別把/sys,/etc,/tmp目錄一起掛載到rootfs中形成一個在子進程看起來就是一個完整的rootfs,但沒有占用額外的磁盤空間。
在此基礎上,Docker還自己創新了一個層的概念。首先,它將系統內核所需要的rootfs內的文件掛載到了一個“只讀層”中,將用戶的應用程序、系統的配置文件等之類可以修改的文件掛載到了“可讀寫層”中。在容器啟動時,我們還可以將初始化參數掛載到了專門的“init層”中。容器啟動的最后階段,這三層再次被聯合掛載,最終形成了容器中的rootfs。
(Docker的只讀層、可讀寫層和init層)
從上面的描述中,我們可以了解到只讀層最適合放置的是固定版本的文件,代碼幾乎不會改變,才能實現最大程度的復用。比如活字格公有雲是基於.net core開發的,我們將其用到的基礎環境等都會設計在了只讀層,每次獲取最新鏡像時,因為每一份只讀層都是完全一樣的,所以完全不用下載。
Docker的“層”解釋了為什么Docker鏡像只在第一次下載時那么慢,而之后的鏡像都很快,並且明明每份鏡像看起來都幾百兆,但是最終機器上的硬盤缺沒有占用那么多的原因。更小的磁盤空間、更快的加載速度,讓Docker的復用性有了非常顯著的提升。
Docker容器創建流程
上面介紹的是Docker容器的整個原理。我們結合上一篇文章,可以總結一下Docker創建容器的過程其實是:
- 啟用Linux Namespace配置;
- 設置指定的Cgroups參數;
- 進程的根目錄
- 聯合掛載各層文件
題外:Docker與傳統虛擬機的區別
其實Docker還做了很多功能,比如權限配置,DeviceMapper等等,這里說的僅僅是一個普及性質的概念性講解,底層的各種實現還有很復雜的概念。具體而言,容器和傳統的虛擬機有啥區別?
其實容器技術和虛擬機是實現虛擬化技術的兩種手段,只不過虛擬機是通過Hypervisor控制硬件,模擬出一個GuestOS來做虛擬化的,其內部是一個幾乎真實的虛擬操作系統,內部外部是完全隔離的。而容器技術是通過Linux操作系統的手段,通過類似於Docker Engine這樣的軟件對系統資源進行的一次隔離和分配。它們之間的對比關系大概如下:
(Docker vs 虛擬機)
虛擬機是物理隔離,相比於Docker容器來說更加安全,但也會帶來一個結果:在沒有優化的情況下,一個運行CentOS 的 KVM 虛擬機啟動后自身需要占用100~200MB內存。此外,用戶應用也運行在虛擬機里面,應用系統調用宿主機的操作系統不可避免需要經過虛擬化軟件的攔截和處理,本身會帶來性能損耗,尤其是對計算資源、網絡和磁盤I/O的損耗非常大。
但容器與之相反,容器化之后的應用依然是一個宿主機上的普通進程,這意味着因為虛擬化而帶來的損耗並不存在;另一方面使用Namespace作為隔離手段的容器並不需要單獨的Guest OS,這樣一來容器額外占用的資源內容幾乎可以忽略不計。
所以,對於更加需要進行細粒度資源管理的PaaS平台而言,這種“敏捷”和“高效”的容器就成為了其中的佼佼者。看起來解決了一切問題的容器。難道就沒有缺點嗎?
其實容器的弊端也特別明顯。首先由於容器是模擬出來的隔離性,所以對Namespace模擬不出來的資源:比如操作系統內核就完全無法隔離,容器內部的程序和宿主機是共享操作系統內核的,也就是說,一個低版本的Linux宿主機很可能是無法運行高版本容器的。還有一個典型的栗子就是時間,如果容器中通過某種手段修改了系統時間,那么宿主機的時間一樣會改變。
另一個弊端是安全性。一般的企業,是不會直接把容器暴露給外部用戶直接使用的,因為容器內可以直接操作內核代碼,如果黑客可以通過某種手段修改內核程序,那就可以黑掉整個宿主機,這也是為什么我們自己的項目從剛開始自己寫Docker到最后棄用的直接原因。現在一般解決安全性的方法有兩個:一個是限制Docker內進程的運行權限,控制它值能操作我們想讓它操作的系統設備,但是這需要大量的定制化代碼,因為我們可能並不知道它需要操作什么;另一個方式是在容器外部加一層虛擬機實現的沙箱,這也是現在許多頭部大廠的主要實現方式。
小結
Docker憑借一致性、復用性的優勢戰勝了前輩Cloud Foundry。本文介紹了Docker具體對容器做的一點改變,同時也介紹了容器的明顯缺點。下一篇文章,我們會為大家介紹Docker又是如何落寞,而后Docker時代,誰又是時代新星。敬請期待。