ZStack的伸縮性秘密武器(三):無鎖架構

ZStack的伸縮性秘密武器(三):無鎖架構

5 人贊了文章

在IaaS(Infrastructure as a Service,即基礎設施即服務)軟體里許多任務要順序的執行;例如,當一個起動虛擬機的任務正在運行時,一個結束些虛擬機的任務則必有等待之前的開始任務結束才行。另一方面,一些任務以需要並發的同時運行;例如,在同一主機上20個創建虛擬機的任務能同時運行。同步和並行在一個分散式系統中是不好控的並且常常需要一個同步軟體。針對這個挑戰,ZStack提供了一個基於隊列的無鎖架構,允許任務很容易的來控制它們的並行級別,從一個同步到N個並行都行。

動機

一個好的IasS軟體在任務的同步及並行上需要有精細的控制。大多數情況下,任務之間有依賴關係需要以某一順序來執行;例如,一個刪除卷的任務不能被執行,如果另一個在此卷上做快照備份的任務正在執行中。有時,任務要並發執行來提升性能;例如,在同一台主機上十個創建虛擬機的任務同時執行一點問題也沒有。當然,沒有正常的控制,並行任務也會損壞系統;例如,1000個同時執行的創建虛擬機的任務雖不會使系統掛掉但至少導致系統有段時間沒有響應。這種同步開發問題在多線程環境是很複雜的,在分散式環境就顯得更加複雜了。

問題

教科書告訴我們,鎖和信號量是同步和並行的答案;在分散式系統中,處理同步和並行的最直接的想法是,使用某種分散式的協調軟體,像 Apache ZooKeeper ,或者在 Redis之上構建的類似軟體。 分散式協調軟體的使用概況,例如, ZooKeeper,像下面這樣:

問題是,對於鎖或信號量, 線程需要等待其它線程釋放它們正在使用的鎖或信號量。在ZStack 的伸縮性秘密(第一部分)非同步架構(ZStacks Scalability Secrets Part 1: Asynchronous Architectue) 一文中,我們解釋了,ZStack是一種非同步軟體,線程不會因等待其它線程的完成而阻塞;因此,鎖和信號量不是可行的選項。同時,我們也關心使用分散式協調軟體的複雜性和拓展性,想像一下,一個滿載100,000個需要鎖的任務的系統,這既不容易,也不易拓展。

同步的vs. 同步化的:在 ZStack 的伸縮性秘密(第一部分)非同步架構(ZStacks Scalability Secrets Part 1: Asynchronous Architecture)一文中, 我們討論了 同步的 vs. 非同步的,在本文中,我們將會討論 同步的 vs. 並行的。「同步的」和「同步化的」有時候是可互換的使用,但是它們是不同的。在我們的場景中,「同步的」是在討論,關於執行一個任務是否會阻塞線程的問題;「同步化的」是在討論,關於一個任務是否排它的執行的問題。如果一個任務在完成前,一直佔據一個線程的所有時間,這就是一個同步的任務;如果一個任務不能和其它任務在同一時間執行,這就是一個同步化的任務。

無鎖架構的基礎

使用一致性哈希演算法,來保證同一個服務實例能夠處理所有到達同一資源的消息,這就是無鎖架構的基礎。通過這種方法聚集到達某一節點的消息,可以減少從分散式系統到多線程環境的同步,並行化的複雜性(更多細節見ZStack的伸縮性秘密(第二部分):無狀態服務)。

工作隊列:傳統解決方案

注意:在深入了解細節之前,請注意,我們即將要談論的隊列,和在 ZStack 的伸縮性秘密(第二部分)無狀態服務(ZStacks Scalability Secrets Part 2: Stateless Services)一文中提到的RabbitMQ消息隊列,沒有任何關聯。消息隊列是RabbitMQ的術語;ZStack的隊列則是內部數據結構。

在ZStack中的任務是由消息驅動的,聚合消息讓相關的任務可以在同樣的節點執行,減輕了經典的線程池並發編程的壓力。為了避免鎖競爭,ZStack使用工作隊列替代鎖和信號量。同步化的任務可以一個接一個的執行,它們由基於內存的工作隊列維護:

注意:工作隊列可以同時執行同步化的和並行的任務。如果並行級別為1,那麼隊列就是同步化的;如果並行級別大於1,那麼隊列是並行的;如果並行級別為0,那麼隊列就是無限並行的。

基於內存的同步隊列

在Zstack中有兩種工作隊列;一種是同步隊列,任務返回結果才認定為結束(通常使用Java Runnable介面來實現):

thdf.syncSubmit(new SyncTask<Object>() { @Override public String getSyncSignature() { return "api.worker"; } @Override public int getSyncLevel() { return apiWorkerNum; } @Override public String getName() { return "api.worker"; } @Override public Object call() throws Exception { if (msg.getClass() == APIIsReadyToGoMsg.class) { handle((APIIsReadyToGoMsg) msg); } else { try { dispatchMessage((APIMessage) msg); } catch (Throwable t) { bus.logExceptionWithMessageDump(msg, t); bus.replyErrorByMessageType(msg, errf.throwableToInternalError(t)); } } /* When method call() returns, the next task will be proceeded immediately */ return null; } });

強調: 在同步隊列中,工作線程繼續讀取下個Runnable,只要前一個Runnable.run()方法返回結果,並且直接隊列為空了才返回線程池。因為任務在執行時會取得工作線程,隊列是同步的.

基於內存的非同步隊列

另一種是異常工作隊列,當它發出一個完成通知才認為結束:

thdf.chainSubmit(new ChainTask(msg) { @Override public String getName() { return String.format("start-vm-%s", self.getUuid()); } @Override public String getSyncSignature() { return syncThreadName; } @Override public void run(SyncTaskChain chain) { startVm(msg, chain); /* the next task will be proceeded only after startVm() method calls chain.next() */ } });

強調: 在非同步隊列中,ChainTask.run(SyncTaskChain chain) 方法可能在做一些非同步 操作後立即返回;例如,發送消息和一個註冊的回調函數.在run()方法返回值後,工作線程回到線程池中;但是,之前的任務可能還沒完成,沒有任務能夠被處理,直到之前的任務發出一個通知(如調用SyncTaskChain.next())。因為任務不會阻塞工作線程等待其完成,隊列是非同步的。

基於資料庫的非同步隊列

基於內存的工作隊列簡單快速,它滿足了在單一管理節點99%的同步和並行的需要; 然而,與創建資源相關的任務,可能需要在不同管理節點之間做同步。一致性哈希環基於資源UUID來工作,如果資源未被創建,它將無法得知哪個節點應該處理這個創建的工作。在大多數情況下,如果要創建的資源不依賴於其它未完成的任務,ZStack會選擇,此創建任務的提交者所在的本地節點,來完成這個工作。不幸的是,這些不間斷的任務依賴於名為虛擬路由VM的特殊資源; 例如,如果使用同樣的L3網路的多個用戶VM,由運行於不同管理節點的任務創建而成,同時在L3網路上並無虛擬路由VM,那麼創建虛擬路由VM的任務則可能由多個管理節點提交。在這種情況下,由於存在分散式同步的問題,ZStack使用基於資料庫的作業隊列,這樣來自不同管理節點的任務就可以實現全局同步。

資料庫作業隊列只有非同步的形式;也就是說,只有前一個任務發出一個完成通知後,下一個任務才能執行。

注意: 由於任務存儲在資料庫之中,所以資料庫作業隊列的速度比較慢;幸運的是,只有創建虛擬路由VM的任務需要它。

限制

雖然基於無鎖架構的隊列可以處理99.99%的時間同步,但是有一個爭用條件從一致的散列演算法中產生:一個新加入的節點將分擔一部分相鄰節點的工作量,這就是一致的散列環的擴張的結果。

在這個例子中,在三個節點加入後,以前的目標定位從節點2轉到了節點3;在此期間,如果對於資源的一個舊任務依舊工作在節點2上,但是對於相同資源的任務提交到節點3,這就會造成爭用狀態。然而,這種狀況並不是你想像中的那麼壞。首先,衝突任務很少地存在規則的系統中,比如,一個健全的 UI 不允許你阻止一個正在運行的 VM。然後,每一個 ZStack 資源都有狀態,一個開始就處於問題狀態的任務會出現錯誤;比如,如果一個 VM 是停止狀態,一個附加任務量的任務就會立刻出錯。第三,代理--大多數任務的傳送地,有額外的附加機制;比如,虛擬路由器代理會同步所有的修改 DHCP 配置文件的請求,即使我們已經有了虛擬路由器在管理節點端的工作隊列。最後,提前規劃你的操作是持續管理雲的關鍵;操作團隊可以在推出雲之前快速產生足夠的管理節點;如果他們真的需要動態添加一個新的節點,這樣做的時候,工作量還是比較小的。

總結

在這篇文章里,展示了建立在基於內存工作隊列和基於資料庫的無鎖結構。沒有涉及複雜的分散式協作軟體,ZStack 儘可能地在爭用條件下的屏蔽任務中配合提升性能。


推薦閱讀:

一款成功的全球服遊戲該如何進行架構選型與設計?
為什麼現在黑客不敢攻擊BAT這些大公司?他們比黑客更「野蠻」
贈票|2018人工智慧計算大會報名開啟:算力爆燃,AI進化
UCloud Elasticsearch服務UES應用場景分析
漫談雲計算向邊緣計算轉移的發展之勢

TAG:系統架構 | 雲計算 |