攜程基于BookKeeper的延遲消息架構(gòu)落地實踐
本文作者magiccao、littleorca,來自攜程消息隊列團隊。目前主要從事消息中間件的開發(fā)與彈性架構(gòu)演進工作,同時對網(wǎng)絡(luò)/性能優(yōu)化、應(yīng)用監(jiān)控與云原生等領(lǐng)域保持關(guān)注。
一、背景
QMQ延遲消息是以服務(wù)形式獨立存在的一套不局限于消息廠商實現(xiàn)的解決方案,其架構(gòu)如下圖所示。
QMQ延遲消息服務(wù)架構(gòu)
延遲消息從生產(chǎn)者投遞至延遲服務(wù)后,堆積在服務(wù)器本地磁盤中。當(dāng)延遲消息調(diào)度時間過期后,延遲服務(wù)轉(zhuǎn)發(fā)至實時Broker供消費方消費。延遲服務(wù)采用主從架構(gòu),其中,Zone表示一個可用區(qū)(一般可以理解成一個IDC),為了保證單可用區(qū)故障后,歷史投遞的待調(diào)度消息正常調(diào)度,master和slave會跨可用區(qū)部署。
1.1 痛點
此架構(gòu)主要存在如下幾點問題:
- 服務(wù)具有狀態(tài),無法彈性擴縮容;
- 主節(jié)點故障后,需要主從切換(自動或手動);
- 缺少一致性協(xié)調(diào)器保障數(shù)據(jù)的一致性。
如果將消息的業(yè)務(wù)層和存儲層分離出來,各自演進協(xié)同發(fā)展,各自專注在擅長的領(lǐng)域。這樣,消息業(yè)務(wù)層可以做到無狀態(tài)化,輕松完成容器化改造,具備彈性擴縮容能力;存儲層引入分布式文件存儲服務(wù),由存儲服務(wù)來保證高可用與數(shù)據(jù)一致性。
1.2 分布式文件存儲選型
對于存儲服務(wù)的選型,除了基本的高可用于數(shù)據(jù)一致性特點外,還有至關(guān)重要的一點:高容錯與低運維成本特性。分布式系統(tǒng)最大的特點自然是對部分節(jié)點故障的容忍能力,畢竟任何硬件或軟件故障是不可百分百避免的。因此,高容錯與低運維成本將成為我們選型中最為看重的。
2016年由雅虎開源貢獻(xiàn)給Apache的Pulsar,因其云原生、低延遲分布式消息隊列與流式處理平臺的標(biāo)簽,在開源社區(qū)引發(fā)轟動與追捧。在對其進行相關(guān)調(diào)研后,發(fā)現(xiàn)恰好Pulsar也是消息業(yè)務(wù)與存儲分離的架構(gòu),而存儲層則是另一個Apache開源基金會的BookKeeper。
二、BookKeeper
BookKeeper作為一款可伸縮、高容錯、低延遲的分布式強一致存儲服務(wù)已被部分公司應(yīng)用于生產(chǎn)環(huán)境部署使用,最佳實踐案例包括替代HDFS的namenode、Pulsar的消息存儲與消費進度持久化以及對象存儲。
2.1 基本架構(gòu)
BookKeeper基本架構(gòu)
- Zookeeper集群用于存儲節(jié)點發(fā)現(xiàn)與元信息存儲,提供強一致性保證;
- Bookie存儲節(jié)點,提供數(shù)據(jù)的存儲服務(wù)。寫入和讀取過程中,Bookie節(jié)點間彼此無須通信。Bookie啟動時將自身注冊到Zookeeper集群,暴露服務(wù);
- Client屬于胖客戶端類型,負(fù)責(zé)與Zookeeper集群和BookKeeper集群直接通信,且根據(jù)元信息完成多副本的寫入,保證數(shù)據(jù)可重復(fù)讀。
2.2 基本特性
a)基本概念
- Entry:數(shù)據(jù)載體的基本單元
- Ledger:entry集合的抽象,類似文件
- Bookie:ledger集合的抽象,物理存儲節(jié)點
- Ensemble:ledger的bookie集合
b)數(shù)據(jù)讀寫
BookKeeper數(shù)據(jù)讀寫
bookie客戶端通過創(chuàng)建而持有一個ledger后便可以進行entry寫入操作,entry以帶狀方式分布在enemble的bookie中。entry在客戶端進行編號,每條entry會根據(jù)設(shè)置的副本數(shù)(Qw)要求判定寫入成功與否;
bookie客戶端通過打開一個已創(chuàng)建的ledger進行entry讀取操作,entry的讀取順序與寫入保持一致,默認(rèn)從第一個副本中讀取,讀取失敗后順序從下一個副本重試。
c)數(shù)據(jù)一致性
持有可寫ledger的bookie客戶端稱為Writer,通過分布式鎖機制確保一個ledger全局只有一個Writer,Writer的唯一性保證了數(shù)據(jù)寫入一致性。Writer內(nèi)存中維護一個LAC(Last Add Confirmed),當(dāng)滿足Qw要求后,更新LAC。LAC隨下一次請求或定時持久化在bookie副本中,當(dāng)ledger關(guān)閉時,持久化在Metadata Store(zookeeper或etcd)中;
持有可讀ledger的bookie客戶端稱為Reader,一個ledger可以有任意多個Reader。LAC的強一致性保證了不同Reader看到統(tǒng)一的數(shù)據(jù)視圖,亦可重復(fù)讀,從而保證了數(shù)據(jù)讀取一致性。
d)容錯性
典型故障場景:Writer crash或restart、Bookie crash。
Writer故障,ledger可能未關(guān)閉,導(dǎo)致LAC未知。通過ledger recover機制,關(guān)閉ledger,修復(fù)LAC;
Bookie故障,entry寫入失敗。通過ensemble replace機制,更新一條新的entry路由信息到Metadata Store中,保障了新數(shù)據(jù)能及時成功寫入。歷史數(shù)據(jù),通過bookie recover機制,滿足Qw副本要求,夯實了歷史數(shù)據(jù)讀取的可靠性。至于副本所在的所有bookie節(jié)點全部故障場景,只能等待修復(fù)。
e)負(fù)載均衡
新擴容進集群的bookie,當(dāng)創(chuàng)建新的ledger時,便自動均衡流量。
2.3 同城多中心容災(zāi)
上海區(qū)域(region)存在多個可用區(qū)(az,available zone),各可用兩兩間網(wǎng)絡(luò)延遲低于2ms,此種網(wǎng)絡(luò)架構(gòu)下,多副本分散在不同的az間是一個可接受的高可用方案。BookKeeper基于Zone感知的ensemble替換策略便是應(yīng)對此種場景的解決方案。
基于Zone感知策略的同城多中心容災(zāi)
開啟Zone感知策略有兩個限制條件:a)E % Qw == 0;b)Qw > minNumOfZones。其中E表示ensemble大小,Qw表示副本數(shù),minNumOfZones表示ensemble中的最小zone數(shù)目。
譬如下面的例子:
minNumOfZones = 2
desiredNumZones = 3
E = 6
Qw = 3
[z1, z2, z3, z1, z2, z3]
故障前,每條數(shù)據(jù)具有三副本,且分布在三個可用區(qū)中;當(dāng)z1故障后,將以滿足minNumOfZones限制生成新的ensemble:[z1, z2, z3, z1, z2, z3] -> [z3, z2, z3, z3, z2, z3]。顯然對于三副本的每條數(shù)據(jù)仍將分布在兩個可用區(qū)中,仍能容忍一個可用區(qū)故障。
DNSResolver
客戶端在挑選bookie組成ensemble時,需要通過ip反解出對應(yīng)的zone信息,需要用戶實現(xiàn)解析器。考慮到zone與zone間網(wǎng)段是認(rèn)為規(guī)劃且不重合的,因此,我們落地時,簡單的實現(xiàn)了一個可動態(tài)配置生效的子網(wǎng)解析器。示例給出的是ip精確匹配的實現(xiàn)方式。
public class ConfigurableDNSToSwitchMapping extends AbstractDNSToSwitchMapping {
private final Map<String, String> mappings = Maps.newHashMap();
public ConfigurableDNSToSwitchMapping() {
super();
mappings.put("192.168.0.1", "/z1/192.168.0.1"); // /zone/upgrade domain
mappings.put("192.168.1.1", "/z2/192.168.1.1");
mappings.put("192.168.2.1", "/z3/192.168.2.1");
}
@Override
public boolean useHostName() {
return false;
}
@Override
public List<String> resolve(List<String> names) {
List<String> rNames = Lists.newArrayList();
names.forEach(name -> {
String rName = mappings.getOrDefault(name, "/default-zone/default-upgradedomain");
rNames.add(rName);
});
return rNames;
}
}
可配置化DNS解析器示例
數(shù)據(jù)副本分布在單zone
當(dāng)某些原因(譬如可用區(qū)故障演練)導(dǎo)致只有一個可用區(qū)可用時,新寫入的數(shù)據(jù)的全部副本都將落在單可用區(qū),當(dāng)故障可用區(qū)恢復(fù)后,仍然有部分歷史數(shù)據(jù)只存在于單可用區(qū),不滿足多可用區(qū)容災(zāi)的高可用需求。
AutoRecovery機制中有一個PlacementPolicy檢測機制,但缺少恢復(fù)機制。于是我們打了個patch,支持動態(tài)機制開啟和關(guān)閉此功能。這樣,當(dāng)可用區(qū)故障恢復(fù)后可以自動發(fā)現(xiàn)和修復(fù)數(shù)據(jù)全部副本分布在單可用區(qū)從而影響數(shù)據(jù)可用性的問題。
三、彈性架構(gòu)落地
引入BookKeeper后,延遲消息服務(wù)的架構(gòu)相對漂亮不少。消息業(yè)務(wù)層面和存儲層面完全分離,延遲消息服務(wù)本身無狀態(tài)化,可以輕易伸縮。當(dāng)可用區(qū)故障后,不再需要主從切換。
延遲消息服務(wù)新架構(gòu)
3.1 無狀態(tài)化改造
存儲層分離出去后,業(yè)務(wù)層實現(xiàn)無狀態(tài)化成為可能。要達(dá)成這一目標(biāo),還需解決一些問題。我們先看看BookKeeper使用上的一些約束:
- BookKeeper不支持共享寫入的,也即業(yè)務(wù)層多個節(jié)點如果都寫數(shù)據(jù),則各自寫的必然是不同的ledger;
- 雖然BookKeeper允許多讀,但多個應(yīng)用節(jié)點各自讀取的話,進度是相互獨立的,應(yīng)用必須自行解決進度協(xié)調(diào)問題。
上述兩個主要問題,決定我們實現(xiàn)無狀態(tài)和彈性擴縮容時,必需自行解決讀寫資源分配的問題。為此,我們引入了任務(wù)協(xié)調(diào)器。
我們首先將存儲資源進行分片管理,每個分片上都支持讀寫操作,但同一時刻只能有一個業(yè)務(wù)層節(jié)點來讀寫。如果我們把分片看作資源,把業(yè)務(wù)層節(jié)點看作工作者,那么任務(wù)協(xié)調(diào)器的主要職責(zé)為:
- 在盡可能平均的前提下以粘滯優(yōu)先的方式把資源分配給工作者;
- 監(jiān)視資源和工作者的變化,如有增減,重新執(zhí)行職責(zé)1;
- 在資源不夠用時,根據(jù)具體策略配置,添加初始化新的資源。
由于是分布式環(huán)境,協(xié)調(diào)器自身完成上述職責(zé)時需要保證分布式一致性,當(dāng)然還要滿足可用性要求。我們選擇了基于ZooKeeper進行選主的一主多從式架構(gòu)。
如圖所示,協(xié)調(diào)器對等部署在業(yè)務(wù)層應(yīng)用節(jié)點中。運行時,協(xié)調(diào)器通過基于ZooKeeper的leader競選機制決出leader節(jié)點,并由leader節(jié)點負(fù)責(zé)前述任務(wù)分配工作。
協(xié)調(diào)器選舉的實現(xiàn)參考ZooKeeper官方文檔,這里不再贅述。
3.2 持久化數(shù)據(jù)
原有架構(gòu)將延遲消息根據(jù)調(diào)度時間按每10分鐘桶存儲在本地,時間臨近的桶加載到內(nèi)存中,使用HashedWheelTimer來調(diào)度。該設(shè)計存在兩個弊端:
- 分桶較多(我們支持2年范圍的延遲,理論分桶數(shù)量達(dá)10萬多);
- 單個桶的數(shù)據(jù)(10分鐘)如不能全部加載到內(nèi)存,則由于桶內(nèi)未按調(diào)度時間排序,可能出現(xiàn)未加載的部分包含了調(diào)度時間較早的數(shù)據(jù),等它被加載時已經(jīng)滯后了。
弊端1的話,單機本地10萬+文件還不算多大問題,但改造后這些桶信息以元信息的方式存儲在ZooKeeper上,我們的實現(xiàn)方案決定了每個桶至少占用3個ZooKeeper節(jié)點。假設(shè)我們要部署5個集群,平均每個集群有10個分片,每個分片有10萬個桶,那使用的ZooKeeper節(jié)點數(shù)量就是1500萬起,這個量級是ZooKeeper難以承受的。
弊端2則無論新老架構(gòu),都是個潛在問題。一旦某個10分鐘消息量多一些,就可能導(dǎo)致消息延遲。往內(nèi)存加載時,應(yīng)該有更細(xì)的顆粒度才好。
基于以上問題分析,我們參考多級時間輪調(diào)度的思路,略加變化,設(shè)計了一套基于滑動時間分桶的多級調(diào)度方案。
如上表所示,最大的桶是1周,其次是1天,1小時,1分鐘。每個級別覆蓋不同的時間范圍,組合起來覆蓋2年的時間范圍理論上只需286個桶,相比原來的10萬多個桶有了質(zhì)的縮減。
同時,只有L0m這一級調(diào)度器需要加載數(shù)據(jù)到HashedWheelTimer,故而加載粒度細(xì)化到了1分鐘,大大減少了因不能完整加載一個桶而導(dǎo)致的調(diào)度延遲。
多級調(diào)度器以類似串聯(lián)的方式協(xié)同工作。
每一級調(diào)度器收到寫入請求時,首先嘗試委托給其上級(顆粒度更大)調(diào)度器處理。如果上級接受,則只需將上級的處理結(jié)果向下返回;如果上級不接受,再判斷是否歸屬本級,是的話寫入桶中,否則打回給下級。
每一級調(diào)度器都會將時間臨近的桶打開并發(fā)送其中的數(shù)據(jù)到下一級調(diào)度器。比如L1h發(fā)現(xiàn)最小的桶到了預(yù)加載時間,則把該桶的數(shù)據(jù)讀出并發(fā)送給L0m調(diào)度器,最終該小時的數(shù)據(jù)被轉(zhuǎn)移到L0m并展開為(最多)60個分鐘級的桶。
四、未來規(guī)劃
目前bookie集群部署在物理機上,集群新建、擴縮容相對比較麻煩,未來將考慮融入k8s體系;bookie的治理與平臺化也是需要考慮的;我們目前只具備同城多中心容災(zāi)能力,跨region容災(zāi)以及公/私混合云容災(zāi)等高可用架構(gòu)也需要進一步補強。