轉載請註明出處:葡萄城官網,葡萄城為開發者提供專業的開發工具、解決方案和服務,賦能開發者。
上一節我們為大家介紹了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時代,誰又是時代新星。敬請期待。