ZStack的架構使得其中99%的任務能被異步執行。基于這點,ZStack中單一的管理節點可以管理幾千臺物理服務器,上萬臺虛擬機,處理成千上萬個并發任務。
動機
對于管理大量硬件和虛擬機的公有云而言,可拓展性是一個IaaS軟件必須解決的關鍵問題之一。對于一個大概擁有5萬臺物理服務器的中型數據中心,預計可能有150萬臺虛擬機,1萬名用戶。雖然用戶開關虛擬機的頻率不會像刷朋友圈一樣頻繁,但是在某一時刻,IaaS系統可能有成千上萬個任務要處理,這些任務可能來自API也可能來自內部組件。在糟糕的情況下,用戶為了創建一臺新的虛擬機可能需要等待一個小時,因為系統同時被5000個任務阻塞,然而線程池僅有1000條線程。
問題
首先,我們非常不贊同一些文章里面描寫的關于“一些基礎配套設施,尤其是數據庫和消息代理(message brokers)限制了IaaS的可拓展性”的觀點。首先,對于數據庫而言,IaaS軟件的數據量相比facebook和twitter而言只能勉強算中小型,facebook和twitter的數據量是萬億級別,IaaS軟件只處于百萬級別(對于一些非常大型的數據中心),而facebook和twitter依舊堅強的使用Mysql作為他們主要的數據庫。其次,對于消息代理而言,ZStack使用的rabbitmq相對Apache Kafka或ZeroMQ是一個中型的消息代理,但是它依然可以維持平均每秒5萬條消息的吞吐量,對于IaaS軟件內部通信而言這不就足夠了么?我們認為足夠了。
限制IaaS可拓展性的主要原因在于:任務執行緩慢。IaaS軟件上的任務運行非常緩慢,通常一項任務完成需要花費幾秒甚至幾分鐘。所以當整個系統被緩慢的任務填滿的時候,新任務的延遲非常大是很正常的。執行緩慢的任務通常是由一個很長的任務路徑組成的,比如,創建一個虛擬機,需要經過身份驗證服務?調度器?鏡像服務?存儲服務?網絡服務?虛擬機管理程序,每一個服務可能會花費幾秒甚至幾分鐘去引導外部硬件完成一些操作,這極大的延長了任務執行的時間。
其實,IaaS 伸縮性問題的根源在于:任務處理慢。確實是,在 IaaS 軟件系統中任務處理非常慢,慢到要有幾秒甚至是幾分鐘才能完成。因此,當系統中全是這種慢慢處理的任務時候,當然就帶來了新任務的巨大的延遲。而這種慢處理的任務源于任務路徑過長。舉例說明,創建虛擬機,一般要經過以下路徑 身份服務(service)-->規劃器(scheduler)-> 圖象服務(service)->存儲服務->網絡服務->系統管理(Hypervisor); 每個服務都會花費幾秒甚至幾分鐘來操作外部硬件,這就導致了超長的任務處理時長。
同步 vs 異步
傳統的IaaS軟件使用同步的方式執行任務,他們通常給每一個任務安排一個線程,這個線程只有在之前的任務執行完畢時才會開始執行下一個任務。因為任務執行緩慢,當達到一個任務并發的高峰時,系統會因為線程池容量不足,運行非常緩慢,新來的任務只能被放在隊列中等待被執行。
為了解決這個問題,一個直觀的想法是提高線程池容量,但是這個想法在實際中是不可行的,即使現代操作系統允許一個應用程序擁有成千上萬條線程,沒有操作系統可以非常有效率的調度他們。隨后有一個想法是把線程分發出去,讓不同的操作系統上相似的軟件分布式的處理線程,因為每一個軟件都有它自己的線程池,這樣最終增加了整個系統的線程容量。然而,分發會帶來一定的開銷,它增加了管理的復雜度,同時集群軟件在軟件設計層面依舊是一個挑戰。最后,IaaS軟件自身變成云的瓶頸,而其他的基礎設施包括數據庫,消息代理和外部的系統(比如成千臺物理服務器)都足夠去處理更多的并發任務。
ZStack通過異步架構來解決這個問題,如果我們把目光投向IaaS軟件和數據中心的設備之間的關系,我們會發現IaaS軟件實際上扮演著一個協調者的角色,它負責協調外部系統但并不做任何真正耗時的操作。舉個例子,存儲系統可以分配磁盤容量,鏡像系統可以下載鏡像模板,虛擬機管理程序可以創建虛擬機。IaaS軟件所做的工作是做決策然后把子任務分配給不同的外部系統。比如,對于KVM,KVM主機需要執行諸如準備磁盤、準備網絡、創建虛擬機等子任務。創建一臺虛擬機可能需要花費5s,IaaS軟件花費時間為0.5s,剩下的4.5s被KVM主機占用,ZStack的異步架構使IaaS管理軟件不用等待4.5s,它只需要花費0.5s的時間選擇讓哪一臺主機處理這個任務,然后把任務分派給那個主機。一旦主機完成了它的任務,它將結果通知給IaaS軟件。通過異步架構,一個只有100條線程容量的線程池可以處理上千數的并發任務。
ZStack 的異步方法
異步操作在計算機科學中是非常常見的操作,異步I/O,AJAX等都是一些眾所周知的例子。然而,把所有的業務邏輯都建立在異步操作的基礎上,尤其是對于IaaS這種非常典型的集成軟件,是存在很多挑戰的。
最大的挑戰是必須讓所有組件都異步,并不只是一部分組件異步。舉個例子,如果你在其他服務都是同步的條件下,建立一個異步的存儲服務,整個系統性能并不會提升。因為在異步的調用存儲服務時,調用的服務自身如果是同步的,那么調用的服務必須等待存儲服務完成,才能進行下一步操作,這會使得整個工作流依舊是處于同步狀態。
圖:線程中,業務流程服務要調用存儲服務,直到存儲服務返回了,線程才能結束。 雖然,存儲服務通過異步方式和外部存儲系統交互。
ZStack's 異步架構包含三部分:
- 異步消息
- 異步方法
- 異步 HTTP 調用
1. 異步消息
ZStack 使用 RabbitMQ 作為消息總線以便連接各個服務。當某個服務調用另一個服務時,源服務發消息給目的服務并注冊一個回調函數,然后馬上返回;一旦目的服務完成了任務,它就會通過觸發回調函數來回復任務結果。
AttachNicToVmOnHypervisorMsg amsg = new AttachNicToVmOnHypervisorMsg();
amsg.setVmUuid(self.getUuid());
amsg.setHostUuid(self.getHostUuid());
amsg.setNics(msg.getNics());
bus.makeTargetServiceIdByResourceUuid(amsg, HostConstant.SERVICE_ID, self.getHostUuid());
bus.send(amsg, new CloudBusCallBack(msg) {
@Override
public void run(MessageReply reply) {
AttachNicToVmReply r = new AttachNicToVmReply();
if (!reply.isSuccess()) {
r.setError(errf.instantiateErrorCode(VmErrors.ATTACH_NETWORK_ERROR, r.getError()));
}
bus.reply(msg, r);
}
});
單個服務也可以發送一串消息給其他服務 ,并異步的等待回復。
final ImageInventory inv = ImageInventory.valueOf(ivo);
final List<DownloadImageMsg> dmsgs = CollectionUtils.transformToList(msg.getBackupStorageUuids(), new Function<DownloadImageMsg, String>() {
@Override
public DownloadImageMsg call(String arg) {
DownloadImageMsg dmsg = new DownloadImageMsg(inv);
dmsg.setBackupStorageUuid(arg);
bus.makeTargetServiceIdByResourceUuid(dmsg, BackupStorageConstant.SERVICE_ID, arg);
return dmsg;
}
});
bus.send(dmsgs, new CloudBusListCallBack(msg) {
@Override
public void run(List<MessageReply> replies) {
/* do something */
}
}
更進一步,也能發送具有一定并行性的消息串。 比如,一串十個的消息,能夠兩兩發送,第三,第四個消息只有第一,第二個消息收到后在一起發出。
final List<ConnectHostMsg> msgs = new ArrayList<ConnectHostMsg>(hostsToLoad.size());
for (String uuid : hostsToLoad) {
ConnectHostMsg connectMsg = new ConnectHostMsg(uuid);
connectMsg.setNewAdd(false);
connectMsg.setServiceId(serviceId);
connectMsg.setStartPingTaskOnFailure(true);
msgs.add(connectMsg);
}
bus.send(msgs, HostGlobalConfig.HOST_LOAD_PARALLELISM_DEGREE.value(Integer.class), new CloudBusSteppingCallback() {
@Override
public void run(NeedReplyMessage msg, MessageReply reply) {
/* do something */
}
});
2. 異步方法
ZStack 服務,就像以上段一所示,它們之間通過異步消息通信; 對于服務內部,一系列的互相關聯的組件,插件是通過異步方法調用來交互的。
protected void startVm(final APIStartVmInstanceMsg msg, final SyncTaskChain taskChain) {
startVm(msg, new Completion(taskChain) {
@Override
public void success() {
VmInstanceInventory inv = VmInstanceInventory.valueOf(self);
APIStartVmInstanceEvent evt = new APIStartVmInstanceEvent(msg.getId());
evt.setInventory(inv);
bus.publish(evt);
taskChain.next();
}
@Override
public void fail(ErrorCode errorCode) {
APIStartVmInstanceEvent evt = new APIStartVmInstanceEvent(msg.getId());
evt.setErrorCode(errf.instantiateErrorCode(VmErrors.START_ERROR, errorCode));
bus.publish(evt);
taskChain.next();
}
});
}
同樣, 回調也能包含返回值:
public void createApplianceVm(ApplianceVmSpec spec, final ReturnValueCompletion<ApplianceVmInventory> completion) {
CreateApplianceVmJob job = new CreateApplianceVmJob();
job.setSpec(spec);
if (!spec.isSyncCreate()) {
job.run(new ReturnValueCompletion<Object>(completion) {
@Override
public void success(Object returnValue) {
completion.success((ApplianceVmInventory) returnValue);
}
@Override
public void fail(ErrorCode errorCode) {
completion.fail(errorCode);
}
});
} else {
jobf.execute(spec.getName(), OWNER, job, completion, ApplianceVmInventory.class);
}
}
3. 異步HTTP調用
ZStack 使用了很多代理來管理外部系統。 例如: 管理 KVM 主機的代理,管理 Console Proxy 的代理,管理虛擬路由的代理等等。這些代理都是構建在 Python CherryPy 上的輕量級的 Web 服務器。因為,沒有類似 HTML5 中的 Web Sockets 技術就不能實現雙向通信,ZStack 就為每個請求,放置了一個回調 URL 在 HTTP 的包頭 。這樣,任務結束后,代理就能夠發送應答給調用者的 URL。
RefreshFirewallCmd cmd = new RefreshFirewallCmd();
List<ApplianceVmFirewallRuleTO> tos = new RuleCombiner().merge();
cmd.setRules(tos);
resf.asyncJsonPost(buildUrl(ApplianceVmConstant.REFRESH_FIREWALL_PATH), cmd, new JsonAsyncRESTCallback<RefreshFirewallRsp>(msg, completion) {
@Override
public void fail(ErrorCode err) {
/* handle failures */
}
@Override
public void success(RefreshFirewallRsp ret) {
/* do something */
}
@Override
public Class<RefreshFirewallRsp> getReturnClass() {
return RefreshFirewallRsp.class;
}
});
通過這三個異步方式,ZStack 已經構建了一個分層架構,保證所有組件能夠實現異步操作。
總結
為了解決由緩慢且并發的任務引起的IaaS軟件可拓展性受限的問題,我們演示了ZStack的異步架構。我們使用模擬器進行測試后發現,一個具有1000條線程的ZStack管理節點可以輕松處理創建100萬臺虛擬機時產生的10000個并發任務。雖然單一管理節點的拓展性已經可以滿足大多數云的負載需要,考慮到系統需要高可用性以及承受巨大的負載量(10萬個并發任務),我們需要一組管理節點來滿足這些需求,如需了解ZStack的無狀態服務,請閱讀下一篇“ZStack可拓展性秘密武器2:無狀態服務”。