我們與Docker編排的故事
兩周前,在 nanit.com ,我們面臨一個困難的選擇。我們已經知道我們的基礎設施在未來會嚴重的依賴Docker,但是仍然沒有決定選擇什么工具來進行容器編排。
編排是一個大詞,對于在之前沒有聽過Docker容器的人來說,可能聽起來十分模糊,所以讓我們來闡釋在Docker的世界里,編排到底是什么意思。
運營的改革
所以我們為什么需要編排?編排到底指的是什么東西?這是一個隨著Docker的發展而出現的一個新理念,還是在這之前已經存在但只是到了現在才開始變得必要?
在Docker之前的日子里,我們有物理的或者虛擬的機器。每一個機器(物理的或者虛擬的)經常只負責一個功能。比如,讓我們看一個WEB站點的典型基礎設施:

[圖1:經典的網站架構]
我們這里看到的是一個在處于前端的負載均衡器,后面有三個web服務器。服務器與持久化型數據庫(MySQL,Postgres等等)和鍵值型 (KV)存儲服務進行通信。這里每一個機器都負責一個單獨的,已經定義好的功能。每一個都有一個IP地址和端口,能讓我們與運行在上面的服務進行通信。另 外一個值得注意的事情是每一個機器的大小(size):一個鍵值型的存儲可能需要很多內存,較少的CPU,但是我們的WEB服務器可能有更密集的CPU消 耗。為了讓各個服務能夠以快速且一致的方式對外提供服務,我們需要在每一個機器上進行資源配置。
同樣一個網站,在Docker的年代里可能是這樣的:

[圖2:Docker的網站架構]
我們仍然有相同的組件,但是現在他們組合的方式不一樣了。同一個機器上可能存在兩個甚至更多,完全不一樣的容器。一個容器可能存在于容器1,在之 后會被移動到容器2。機器或者主機的概念再也不存在了。我們不再有一個機器是專門來分配給數據庫,或者一個機器分給負載均衡器。我們而是有一個資源池(內 存/cpu/網絡),這是一個我們集群所有運行的機器的資源總量。這給我們管理基礎設施的方式帶來了一些有意思的變化:
- 1, 服務發現 :如果現在沒有機器的概念了,那我們該如何識別服務呢?我們怎么告訴我們的Web容器來連接到可能處于不同位置的DB容器,它可能在同一個機器上,在另外一個機器上,或者不斷地在機器之前移動來移動去?在我們不知道負載均衡器處于哪一個機器上的時候,我們 www.nanit.com 的DNS記錄該指向哪里呢?
- 2, 高可用 :我們該如何保證我們的服務,保持運行著并且保持著一定的容量(capacity),比如,我們想我們的容器上至少運行三個web服務器。
- 3, 資源管理 :我們該怎么保證集群的資源池的使用處于合理利用狀態?我們該怎么保證一個即將創建(spawned)的容器有足夠的的資源來有效地運行服務?我們也得保證我們沒有過載一個單獨的機器,同時并且我們沒有冷落其他機器?
- 4, 端口管理 :假如兩個需要在同一個主機上占用同一個端口會怎樣?比如:Web服務器和負載均衡器都需要占用端口80。我們需要保證我們不在同一個機器上同時運行兩個服務。或者,我們可以找到另外一個解決方案,可以讓門在同一個機器上和諧共處,即使他們需要占用同一個端口?
我想簡單的討論一下這些話題,并且講下對Docker的影響是什么,和可用的解決方案。
服務發現
服務發現在Docker早出現之前就已經有現成的解決方案。它們主要是基于機器/主機的范式。如果我們需要一個服務(比如Redis)有一個不變的端點,我們可以:
給這個實例設置一個不變的IP地址。如果這個實例出現故障那就另起一個新的有相同IP地址的實例。幾乎所有的云提供商今天都有這樣的解決方案(比 如AWS上的Elastic IP)。這個解決方案在Docker時代的沒有關聯,因為一個單獨的IP或者機器可以服務很多不同的組件。
使用一個外部的服務發現服務,如Consul。每一個機器運行著一個代理,然后有一個Consul服務器負責掌管著每一個服務的狀態和端點。服務發現通過對每一個服務進行DNS解析來完成。有很多這種方法的改良來和Docker集成,但是要達到完美運行需要大量的的設置。
每一個服務使用一個不變的主機名,或者IP,然后將相關的運行著服務的服務放在其后。每一個啟動的機器向合適的負載均衡器注冊。一個例子是AWS的Auto Scaling Groups,其正是完全按照這個方案運作的。
正如我們所見,服務發現已經有一些知名的解決方案,但是沒有一個對于Docker可以拿來即用。AWS的Auto Scaling Groups針對的是虛擬機,而不是Docker容器,然而雖然Consul可以和Docker一起運行,但是給人感覺從一開始就不是設計來這樣玩的。
今天的Docker編排工具通常附帶了一些服務發現的能力。
高可用
高可用和冗余,和服務發現,也是在Docker時代之前就存在的問題。例如,AWS的Auto Scaling Groups保證你至少有X個機器運行并保持運特定的服務。它甚至可以通過API或者預先定義的規則動態的調整運行機器的數量。
盡管這個問題對于基于機器的范式來講是已解決問題,Docker讓HA和冗余變得有一點更加有意思:我們通過運行更多的Docker容器來進行伸 縮,而不是啟動更多的機器。我么恩可以使用已有的同一個資源池來進行伸縮。這意味著我們的服務伸縮和我們的集群資源伸縮分離開了,向上伸縮或者向下伸縮不 會一定暗示調整資源的數量。我們需要分別伸縮服務和資源。
一些Docker編排的工具有基本的容器橫向伸縮的特性。伸縮集群 - 調整對于編排器可用的資源 - 和容器管理是分開完成的。
資源管理
在Docker之前的日子里,我們通過啟動新的機器來啟動新的服務實例。我們有一些映射記錄來從每一個服務到它的CPU/內存的需求,因此我們能確切知道我們需要啟動那種機器。
有Docker了就不是這種情況了:我們需要把我們的容器調度到已存在的機器上。我們的資源池或多或少的靜止的并且編排工具需要把容器集以優化的 方式適應到機器的集合中。這聽起來只是一個簡單的優化問題,但是實際上不是因為容器集在給定的時間是動態變化的,一方面需要將容器平等地分散開來,一方面 需要留下足夠的資源以防我們想啟動一個需要大量資源的容器,我們需要對此做出權衡。
讓我們把圖標2中的基礎設施拿來作為例子看一下。假設這是每一個服務所必須的資源表格:
Web - 3
Load Balancer - 1
Persistent DB - 2
KV DB - 1
三個機器的每一個都有完全一樣的資源容量,都是6。我們有至少兩個明顯的方法來布局這些容器:
- 松散布局(Sparce Layout):這正如圖標2展示的一樣:

[ 圖三:松散布局 ]
在括號里面我們能看到每一個機器的資源使用情況。
這個布局看起來相當好 - 所有的容器都在實例之間分散開來,并且我們不會太拖累一個單獨的實例 - 每一個都有空閑的資源。但是這個設置也有一個問題,假如我們想講我們的web服務伸縮到4個實例應該怎么辦呢?我們沒有一個哪一個實例有至少3個空閑的資 源。所以可能這對于我們的容器不是最好的布局?
- 這可能是另一個方法來安排我們的容器:

[ 圖四:緊密布局(緊湊布局)]
現在機器1和機器2都滿了 - 他們完全沒有空閑的資源。這讓機器3十分的空閑,可以在需要的時候運行每一個服務。但是像這樣布局我們的容器有可能嗎?你能看到這里存在的問題么?
端口,當然是這個問題。在機器2上,我們有兩個web服務,它們都需要占用80端口。這意味著我們不能將兩個web容器放置到同一個機器上?這個問題直接把我們帶到下一節,端口管理。
端口管理
不管你怎么努力,同一個端口不能在同一個機器上同時被兩個進程占用。當你提前知道需要在機器上運行哪些服務的時候,你只需要保證不會發生端口沖突就行。對于Docker的基礎設施,顯然你沒有這種福利待遇 - 服務會更換機器,并且會一直重新調度。
Docker編排框架通常會選取兩種立場之一:第一個是是避免需要占用相同端口的容器調度到相同的機器上。這是一個簡單的解決方案,但是我們看到 這意味著這對我們如何布局容器附加了很緊的約束。這可能會帶來一個十分棘手的場景,編排器僅僅因為端口的限制而不能調度一個容器。
另外一種容器編排工具采取的方法是指定一個分配好的隨機端口到每一個容器上,然后它們自己來路由流量。如果我們將圖標4中的機器2拿來做例子,這 意味著每一個web服務有一個端口打開,這會直接路由到web服務實例。主機上的端口3000會到第一個web服務實例,端口3001會到第二個實例。
總結
如我們所見,我們已經在Docker之前做過編排,但是與Docker編排容器不同,我們過去編排的是虛擬機。一些在編排的世界的問題,如服務發 現和高可用,已經有現成的解決方案,我們只需要對其做一些調整就能和容器和諧共處。而其他一些問題,如資源管理和端口管理,在Docker來到后是全新的 問題,這需要一些創新性的解決方案。
所以編排到底做些什么?他會將我們的容器調度到集群中的機器上,保證分配合理,同事保持該可用,并且有對服務發現基本的支持。我們選擇的編排框架將會決定我們如何管理我們的集群和如何做運維工作。這實質上是我們所有構建的工作所圍繞的核心運維工具。
已經有一些編排的框架了:AWS ECS,Kubernetes,一些Docker原生的解決方案等等。
在下一篇文章里,我們將會深入分析ECS和Kubernetes,并且試圖讓你理解為什么在nanit.com,我們選擇了后者。