都2020年了,容器,或者說docker容器這個概念,從事互聯網行業的開發者應該都不會感到陌生。無論大廠還是小廠的應用部署現在都首選docker容器。
但是docker雖好,卻並非萬能。docker本身,其實僅僅是提供了一種沙盒的機制,對不同應用進行隔離。鏡像是它出彩的一個設計,可以讓開發者們快速部署應用。但這對大型應用管理來說,是遠遠不夠的。開發者們在意識到這個問題后,提出了編排這個概念,從而引發的新的紛爭。。。
本篇文章從容器的歷史開始說起,然后介紹編排領域,swarm和k8s的紛爭,最后討論基於容器的系統設計模式,這個設計模式參考自google的論文,當然是基於k8s的啦~
PS:容器並非只有docker,但本篇暫略去了它們的差異,大部分情況下這兩個詞可以等價。
從容器說起
背景
在虛擬機和雲計算較為成熟的時候,各家公司想在雲服務器上部署應用,通常都是像部署物理機那樣使用腳本或手動部署,但由於本地環境和雲環境不一致,往往會出現各種小問題。
這時候有個叫Paas的項目,就是專注於解決本地環境與雲端環境不一致的問題,並且提供了應用托管的功能。簡單得說,就是在雲服務器上部署Paas對應的服務端,然后本機就能一鍵push,將本地應用部署到雲端機器。然后由於雲服務器上,一個Paas服務端,會接收多個用戶提交的應用,所以其底層提供了一套隔離機制,為每個提交的應用創建一個沙盒,每個沙盒之間彼此隔離,互不干涉。
看看,這個沙盒是不是和docker很類似呢?實際上,容器技術並不是docker的專屬,docker只是眾多實現容器技術中的一個而已。那為什么后來docker會變得如日中天呢?還是得從Paas說起。
Paas的本質就是通過一套打包(本地)-分發(雲)的機制,幫助用戶將應用分發到大規模的集群中,容器技術只是其中比較底層的一部分而已。聽起來很完美,但問題恰恰就出現在這個打包功能上。打包功能比較繁瑣,要為每個應用,語言,版本都打一個包,重點是打包過程常常出現問題,很可能本地運行得好好的,打包到Paas上就出現問題,而且這種問題無跡可尋,只能通過試錯解決。換句話說,Paas確實可以讓你體驗到一鍵部署的快感,但在這之前,你要先體驗打包過程的萬千痛苦。
這個讓用戶痛苦萬分的打包,docker的一個小創新的卻能夠解決,那就是鏡像。鏡像本身也是一種打包機制,並且這個鏡像通常包含完整的操作系統,可以盡可能還原本地環境,同時你的應用還包含在這里面。
通過鏡像這種東西,你可以方便得在本地開發,然后將鏡像上傳到雲端服務器部署,且基本不需要或者只需要少量修改就可以使雲端服務器擁有和本地一樣的應用環境,然后可以通過這個鏡像建立彼此隔離的沙盒環境,以部署自己的多個應用。
但是,docker雖然解決了Paas打包難的問題,但Paas原本的大規模集群部署的能力,卻是docker的弱項,甚至docker本身並沒有這方面的功能。
才有了后來提出的容器編排概念,Swarm和K8s就是圍繞這塊而起的紛爭,當然那是另外一個故事了。
docker實現原理
說完了docker實現原理,接下來就來看看docker底層是如何實現沙盒隔離機制的。
說起docker,很多人都會將它與虛擬機進行比較,基本都會引用下面這張圖:
其中左邊是虛擬機的結構,右邊是docker容器的結構,但這張圖其實不是那么准確。在虛擬機中,通過Hypervisor對硬件資源進行虛擬化,在這部分硬件資源上安裝操作系統,從而可以讓上層的虛擬機和底層的宿主機相互隔離。但docker是沒有這種功能的,我們在docker容器中看到的與宿主機相互隔離的沙盒環境(文件系統,資源,進程環境等),本質上是通過Linux的Namespace機制,CGroups(Control Groups)和Chroot等功能實現的。實際上Docker依舊是運行在宿主機上的一個進程(進程組),只是通過一些障眼法讓docker以為自己是一個獨立環境。接下來我們簡單介紹下這部分內容。
如果在一個docker容器里面,使用ps命令查看進程,可能只會看到如下的輸出:
/ # ps
PID USER TIME COMMAND
1 root 0:00 /bin/bash
10 root 0:00 ps
在容器中執行ps,只會看到1號進程/bin/bash和10號進程ps。前面有說到,docker容器本身只是Linux中的一個進程(組),也就是說在宿主機上,這個/bin/bash的pid可能是100或1000,那為什么在docker里面看到的這個/bin/bash進程的pid是1呢?答案是linux提供的Namespace機制,將/bin/bash這個進程的進程空間隔離開了。
具體的做法呢,就是在創建進程的時候添加一個可選的參數,比如下面這樣:
int pid = clone(main_function, stack_size, CLONE_NEWPID | SIGCHLD, NULL);
那樣后,創建的線程就會有一個新的命名空間,在這個命名空間中,它的pid就是1,當然在宿主機的真實環境中,它的pid還是原來的值。上面的這個例子,其實只是pid Namespace(進程命名空間),除此之外,還有network Namespace(網絡命名空間),mount Namespace(文件命名空間,就是將整個容器的根目錄root掛載到一個新的目錄中,然后在其中放入內核文件看起來就像一個新的系統了)等,用以將整個容器和實際宿主機隔離開來。而這其實也就是容器基礎的基礎實現了。
但是,上述各種Namespace其實還不夠,還有一個比較大的問題,那就是系統資源的隔離,比如要控制一個容器的CPU資源使用率,內存占用等,否則一個容器就吃盡系統資源,其他容器怎么辦。
而Linux實現資源隔離的方法就是Cgroups,具體的使用方法就不多介紹。Cgroups主要是提供文件接口,即通過修改 /sys/fs/cgroup/下面的文件信息,比如給出pid,CPU使用時間限制等就能限制一個容器所使用的資源。
所以,docker本身只是linux中的一個進程,通過Namespace和cgroup將它隔離成一個個單獨的沙盒。明白這點,就會明白docker的一些特性,比如說太過依賴內核的程序在docker上可能執行會出問題,比如無法在低版本的宿主機上安裝高本版的docker等,因為本質上還是執行在宿主機的內核上。
對了,還有mac和windows系統,這些是怎么實現的呢?很簡單,它們的docker都是建立在虛擬化的linux上的,所以其實還是linux。
說完容器,接下來就開始介紹編排了。
編排之爭
這里我們主要會介紹編排這個概念,以及從這個概念起引發的docker swarm和k8s的紛爭。
docker本身只是提供打包-部署的功能,它並沒有提供分布式集群(大規模集群)管理的功能,這其實是原本Paas項目的主要領域。而編排才是容器技術的核心魅力所在,沒有編排,容器就只是一個沙箱工具。所以從docker成熟以后,你會發現它的主要發力點是在編排,也就是swarm項目上,不過這個docker的親兒子,swarm編排工具,卻敗給了橫空出世的k8s。
為什么會這樣?
先說說什么是容器的編排,說簡單些就是對(docker)容器的配置,運行時候的行為的管理。
那么docker swarm是怎么進行編排的呢?這其實還涉及到另一個項目,docker-compose,這兩個項目與docker Machine合稱為docker三劍客(怎么聽起來有點low)。
前面說到編排就是對容器的配置和運行行為進行管理,那么很自然的想法就是將這些配置和行為的定義都寫到一個配置文件里面,比如用戶需要運行容器A,容器B,容器C。那么我們可以將這幾個容器相關的配置和關聯,比如網絡,磁盤,啟動副本,出錯行為等配置,還有容器間的協作方式(啟動順序等)都寫到一個配置文件。最后通過一條命令,加載並執行這個配置文件,就能夠實現容器的編排了。
swarm做的事情很簡單,有時候簡單不一定是好事,因為那意味着難以滿足業界復雜的需求。比如它在處理有狀態服務上的無力,又比如它難以處理多個服務間復雜的關系(處理服務的順序是不夠的)。這時候,脫胎於Borg的kubernetes(k8s)出現在人們的面前。它身上,沉淀着google數十年的經驗,可以說它就是那個站在巨人肩膀上的寵兒。那么相比於swarm,它的優勢到底在哪里呢?
答案在他的設計上,這個說起來得詳細介紹k8s才能說明白。從設計上說,k8s整體是基於API設計,即整體架構中涉及的組件都可插拔,以容器舉例,在k8s中,容器是可替換的,只要滿足對應的接口設計的標准即可,docker是其中一種方案,而其他容器技術也是可選方案。和容器類似的還有網絡插件,volume插件等等。有關k8s的詳細內容這里不多介紹,有興趣的童鞋可以參考以下文檔:
即整個設計是以集群管理為核心,整體架構都是松散的,可插拔的。而swarm則是以docker為核心,兩者設計就存在本質上的區別。
而后在容器的基礎上,k8s添加了另一層的封裝,即Pod,所謂Pod,是一組相同或功能類似的容器所組成的組(task group)。為什么要有Pod呢?還記得容器的本質是什么嗎,是操作系統中的一組進程,從某種程度上來說,從進程這個層次來進行管理,有些繁雜了。比如Linux都有進程組這個概念管理相同或彼此聯系的一些進程(比如一個功能由多個進程協作完成,這多個進程構成一個進程組)。
在容器編排中,往往多個容器間也會有類似進程和進程組的關系(這里就不舉例了,很多分布式組件都有這種情況),所以需要一個更高層次的抽象來幫助我們對容器進行管理。在k8s中,承擔類似進程組的就是Pod,Pod是一種邏輯上的概念,同時Pod也是k8s中最小的調度單位,同組Pod中的容器都是共享volumns。
有了Pod,也就是組這個概念后,就能夠更加方便對不同服務進行管理。但是它還有個更重要的意義,那就是基於容器的系統設計模式。
基於容器的分布式系統設計之道
讓我們回到1980年,假設你是一個寫慣了C的程序員,你接觸到一個名叫面向對象的編程概念,你會怎么看待這個東西呢?能夠想象這個東西在30年后會占據編程領域的大半壁江山嗎?
而如今,docker容器(或者說Pod)就是一種類似OOP的東西,核心都是通過模塊化封裝,將不同的東西相互隔離,讓它們相互配合,完成某些事情。
從這個角度,或許就能明白為什么前面說到的,容器價值不高,真正有價值的是編排。因為我們同樣不會覺得一個java object有多大價值,OOP的編程思想,及其衍生的設計模式才是精髓。
那么從分布式系統的設計模式的角度來說,容器可以有多少種分類呢?和分布式系統的搭建模式類似,有三種。
- 單容器模式(single-container patterns for container management)
- 單節點協作模式(single-node patterns of closely cooperating containers)
- 和多節點協作模式(multi-node patterns)
PS:這部分內容多參考自google的Design patterns for container-based distributed systems,想看原味論文的童鞋請戳最下方的鏈接。
單容器模式和單節點協作模式看起來相似,但實際是完全不同的東西。
單容器模式,簡單說就是在傳統Docker的基礎上(傳統docker的行為比較簡單,只有run(),pause(),stop()),提供更加豐富的功能和生命周期的管理。說得更簡單點,使用k8s管理單個docker服務。
我們主要介紹單節點協作模式和多節點協作模式。
單節點協作模式
單節點協作模式,簡單說就是在一個分布式的容器服務環境中,通過一個單節點的服務輔助進行管理的這類模式。在這種模式中,需要依賴於k8s中,Pod這個概念的抽象,Pod即task group,一組相同或類似服務的容器的集合。
主要有以下幾種設計模式。
Sidecar pattern(邊車模式)
邊車,這個詞可能很多人沒聽過(包括我了解這個東西之前)。我們先來貼一下邊車的圖,
邊車就是摩托車旁邊的那個小車,在某些環境下(比賽,我猜的),旁邊車上的人可以給車手遞水、食物等操作。
邊車模式也是類似的,即在主服務(的容器,main container)身邊提供一個輔助容器,幫助主服務做一些臟活累活。
比如一個web應用,它會將日志信息寫入到磁盤中,這時候我們就可以新增加一個日志采集的邊車
,協助web服務完成日志采集的工作。就像下面這樣:
還是挺好理解的,這樣的好處,相信了解過設計模式的童鞋隨隨便便就能列舉幾個,不過這里還是從容器的角度詳細介紹下:
- 容器是資源分配的一個單元。將
邊車
服務分離后,可以更加靈活地通過cgroup配置資源,或者一些動態調節資源的操作(比如忙時給web服務更多資源而邊車
更少資源)。 - 容器是最小的打包單位。有助於不同服務的責任划分和測試。
- 容器可以是復用的單位,比如可以將日志服務用語其他服務。
- 提供了錯誤邊界,可以使系統可以正常降級,比如日志服務出錯,不會導致web服務出錯。
- 容器是最小的部署單位,可以為每個服務升級,和回滾。但這也可能是缺點,因為服務一多難以管理。
因為分離所以多了這些好處,聽起來還是蠻誘人的~
Ambassador pattern(外交官模式)
外交官模式,提供一個容器作為代理與主服務(main container)通信。
就相當於在通信口出做多一層代理,比如主服務以為是與一個本地redis通信,但實際上代理會真正與一個redis集群交互。
外交官模式的好處是,讓主服務與外部組件之間相互隔離。只通過代理的話,那么外部組件可以無縫進行替換,而這一切主服務都是無感知的。然后是方便測試和復用,其實就是服務之間解耦的好處啦,和上面邊車模式是有點類似的。
Adapter pattern(適配器模式)
前面說的兩種模式,主要是為了幫助主服務(main coninter)更專注於自己的職責。而適配器模式則是為了方便其他組件。
舉個例子,假設你有多個服務(web,數據庫,緩存服務等),然后需要一個監控監控這幾個組件是否正常。正常情況下,需要讓監控系統獲取不同服務之間的指標信息,然后才能進行監控。
但這樣的問題是,如果增加或減少服務,那么對監控系統來說會很麻煩。適配器模式能夠解決這種困擾。
如果多個服務,web,數據庫,緩存等都提供一個統一的對外接口,那么我們就能夠使用一個適配器容器
,統一獲取這些服務的指標信息,然后由監控系統通過這個適配器容器
統一獲取所有的指標信息。如下圖所示。
OK,那么以上就是單節點協作情況下的三種設計模式,下面再看看多節點的協作模式。
多節點協作模式
這部分內容會比較簡單一些,這里就不花太多篇幅進行講述。
除了單節點上的協作容器,模塊化容器還使構建協作的多節點分布式應用程序變得更加容易。不過這部分內容聽起來很高大上,但其實是很好理解的東西。
比如分布式領域的zookeeper,大家應該都不陌生,在論文中,這種多個節點提供領導者選舉的模式,被稱為領導者選舉模式
。同樣的,kafka這類消息隊列,被稱之為工作隊列模式(rk queue pattern)
。而最后一種,則是類似spark的,master worker計算模式,即將一個計算任務分布到多個其他計算節點的這種方式,稱之為Scatter/gather pattern
模式。
列舉的幾種模式都是通過多個節點協作,並且通過暴露接口提供對外服務。不過其實基本就是常見的使用容器搭建分布式服務的方式,如果使用過docker來搭建hadoop這一套東西,那么對所謂的多節點協作模式肯定不會陌生。
想想也是,如果真的將分布式系統當做一個工程項目,那么這些多節點的部署模式確實需要一個名分。這可以算是一個典型的實踐先於理論,理論總結實踐的例子吧。(不過我還是覺得這部分內容有水論文的嫌疑)
那么關於容器的分布式系統設計的內容就先到這吧,有興趣看原論文的童鞋可以翻到最下。
小結
OK,本文主要介紹了docker容器的發家歷史,然后介紹容器編排的重要性,並簡單說了為什么swarm會在編排的戰爭中輸給了k8s。最后則從容器編排這個概念延伸到基於容器技術的設計模式,三種模式中,單節點協作模式算是比較新穎,還是有些啟發價值的。
以上~
參考文章: