在IaaS(Infrastructure as a Service,即基礎設施即服務)軟件里許多任務要順序的執(zhí)行;例如,當一個起動虛擬機的任務正在運行時,一個結束些虛擬機的任務則必有等待之前的開始任務結束才行。另一方面,一些任務以需要并發(fā)的同時運行;例如,在同一主機上20個創(chuàng)建虛擬機的任務能同時運行。同步和并行在一個分布式系統(tǒng)中是不好控的并且常常需要一個同步軟件。針對這個挑戰(zhàn),ZStack提供了一個基于隊列的無鎖架構,允許任務很容易的來控制它們的并行級別,從一個同步到N個并行都行。
動機
一個好的IasS軟件在任務的同步及并行上需要有精細的控制。大多數情況下,任務之間有依賴關系需要以某一順序來執(zhí)行;例如,一個刪除卷的任務不能被執(zhí)行,如果另一個在此卷上做快照備份的任務正在執(zhí)行中。有時,任務要并發(fā)執(zhí)行來提升性能;例如,在同一臺主機上十個創(chuàng)建虛擬機的任務同時執(zhí)行一點問題也沒有。當然,沒有正常的控制,并行任務也會損壞系統(tǒng);例如,1000個同時執(zhí)行的創(chuàng)建虛擬機的任務雖不會使系統(tǒng)掛掉但至少導致系統(tǒng)有段時間沒有響應。這種同步開發(fā)問題在多線程環(huán)境是很復雜的,在分布式環(huán)境就顯得更加復雜了。
問題
教科書告訴我們,鎖和信號量是同步和并行的答案;在分布式系統(tǒng)中,處理同步和并行的最直接的想法是,使用某種分布式的協(xié)調軟件,像 Apache ZooKeeper ,或者在 Redis之上構建的類似軟件。 分布式協(xié)調軟件的使用概況,例如, ZooKeeper,像下面這樣:
問題是,對于鎖或信號量, 線程需要等待其它線程釋放它們正在使用的鎖或信號量。在ZStack 的伸縮性秘密(第一部分)異步架構(ZStack's Scalability Secrets Part 1: Asynchronous Architectue) 一文中,我們解釋了,ZStack是一種異步軟件,線程不會因等待其它線程的完成而阻塞;因此,鎖和信號量不是可行的選項。同時,我們也關心使用分布式協(xié)調軟件的復雜性和拓展性,想象一下,一個滿載100,000個需要鎖的任務的系統(tǒng),這既不容易,也不易拓展。
同步的vs. 同步化的:在 ZStack 的伸縮性秘密(第一部分)異步架構(ZStack's Scalability Secrets Part 1: Asynchronous Architecture)一文中, 我們討論了 同步的 vs. 異步的,在本文中,我們將會討論 同步的 vs. 并行的。“同步的”和“同步化的”有時候是可互換的使用,但是它們是不同的。在我們的場景中,“同步的”是在討論,關于執(zhí)行一個任務是否會阻塞線程的問題;“同步化的”是在討論,關于一個任務是否排它的執(zhí)行的問題。如果一個任務在完成前,一直占據一個線程的所有時間,這就是一個同步的任務;如果一個任務不能和其它任務在同一時間執(zhí)行,這就是一個同步化的任務。
無鎖架構的基礎
使用一致性哈希算法,來保證同一個服務實例能夠處理所有到達同一資源的消息,這就是無鎖架構的基礎。通過這種方法聚集到達某一節(jié)點的消息,可以減少從分布式系統(tǒng)到多線程環(huán)境的同步,并行化的復雜性(更多細節(jié)見ZStack的伸縮性秘密(第二部分):無狀態(tài)服務)。
工作隊列:傳統(tǒng)解決方案
注意:在深入了解細節(jié)之前,請注意,我們即將要談論的隊列,和在 ZStack 的伸縮性秘密(第二部分)無狀態(tài)服務(ZStack's Scalability Secrets Part 2: Stateless Services)一文中提到的RabbitMQ消息隊列,沒有任何關聯(lián)。消息隊列是RabbitMQ的術語;ZStack的隊列則是內部數據結構。
在ZStack中的任務是由消息驅動的,聚合消息讓相關的任務可以在同樣的節(jié)點執(zhí)行,減輕了經典的線程池并發(fā)編程的壓力。為了避免鎖競爭,ZStack使用工作隊列替代鎖和信號量。同步化的任務可以一個接一個的執(zhí)行,它們由基于內存的工作隊列維護:
注意:工作隊列可以同時執(zhí)行同步化的和并行的任務。如果并行級別為1,那么隊列就是同步化的;如果并行級別大于1,那么隊列是并行的;如果并行級別為0,那么隊列就是無限并行的。
基于內存的同步隊列
在Zstack中有兩種工作隊列;一種是同步隊列,任務返回結果才認定為結束(通常使用Java Runnable接口來實現(xiàn)):
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;
} });
強調: 在同步隊列中,工作線程繼續(xù)讀取下個Runnable,只要前一個Runnable.run()方法返回結果,并且直接隊列為空了才返回線程池。因為任務在執(zhí)行時會取得工作線程,隊列是同步的.
基于內存的異步隊列
另一種是異常工作隊列,當它發(fā)出一個完成通知才認為結束:
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) 方法可能在做一些異步 操作后立即返回;例如,發(fā)送消息和一個注冊的回調函數.在run()方法返回值后,工作線程回到線程池中;但是,之前的任務可能還沒完成,沒有任務能夠被處理,直到之前的任務發(fā)出一個通知(如調用SyncTaskChain.next())。因為任務不會阻塞工作線程等待其完成,隊列是異步的。
基于數據庫的異步隊列
基于內存的工作隊列簡單快速,它滿足了在單一管理節(jié)點99%的同步和并行的需要; 然而,與創(chuàng)建資源相關的任務,可能需要在不同管理節(jié)點之間做同步。一致性哈希環(huán)基于資源UUID來工作,如果資源未被創(chuàng)建,它將無法得知哪個節(jié)點應該處理這個創(chuàng)建的工作。在大多數情況下,如果要創(chuàng)建的資源不依賴于其它未完成的任務,ZStack會選擇,此創(chuàng)建任務的提交者所在的本地節(jié)點,來完成這個工作。不幸的是,這些不間斷的任務依賴于名為虛擬路由VM的特殊資源; 例如,如果使用同樣的L3網絡的多個用戶VM,由運行于不同管理節(jié)點的任務創(chuàng)建而成,同時在L3網絡上并無虛擬路由VM,那么創(chuàng)建虛擬路由VM的任務則可能由多個管理節(jié)點提交。在這種情況下,由于存在分布式同步的問題,ZStack使用基于數據庫的作業(yè)隊列,這樣來自不同管理節(jié)點的任務就可以實現(xiàn)全局同步。
數據庫作業(yè)隊列只有異步的形式;也就是說,只有前一個任務發(fā)出一個完成通知后,下一個任務才能執(zhí)行。
注意: 由于任務存儲在數據庫之中,所以數據庫作業(yè)隊列的速度比較慢;幸運的是,只有創(chuàng)建虛擬路由VM的任務需要它。
限制
雖然基于無鎖架構的隊列可以處理99.99%的時間同步,但是有一個爭用條件從一致的散列算法中產生:一個新加入的節(jié)點將分擔一部分相鄰節(jié)點的工作量,這就是一致的散列環(huán)的擴張的結果。
在這個例子中,在三個節(jié)點加入后,以前的目標定位從節(jié)點2轉到了節(jié)點3;在此期間,如果對于資源的一個舊任務依舊工作在節(jié)點2上,但是對于相同資源的任務提交到節(jié)點3,這就會造成爭用狀態(tài)。然而,這種狀況并不是你想像中的那么壞。首先,沖突任務很少地存在規(guī)則的系統(tǒng)中,比如,一個健全的 UI 不允許你阻止一個正在運行的 VM。然后,每一個 ZStack 資源都有狀態(tài),一個開始就處于問題狀態(tài)的任務會出現(xiàn)錯誤;比如,如果一個 VM 是停止狀態(tài),一個附加任務量的任務就會立刻出錯。第三,代理--大多數任務的傳送地,有額外的附加機制;比如,虛擬路由器代理會同步所有的修改 DHCP 配置文件的請求,即使我們已經有了虛擬路由器在管理節(jié)點端的工作隊列。最后,提前規(guī)劃你的操作是持續(xù)管理云的關鍵;操作團隊可以在推出云之前快速產生足夠的管理節(jié)點;如果他們真的需要動態(tài)添加一個新的節(jié)點,這樣做的時候,工作量還是比較小的。
總結
在這篇文章里,展示了建立在基于內存工作隊列和基于數據庫的無鎖結構。沒有涉及復雜的分布式協(xié)作軟件,ZStack 盡可能地在爭用條件下的屏蔽任務中配合提升性能。