攜程基於 BookKeeper 的延遲消息架構落地實踐

作者簡介

本文作者 magiccao、littleorca,來自攜程消息隊列團隊。目前主要從事消息中間件的開發與彈性架構演進工作,同時對網絡 / 性能優化、應用監控與雲原生等領域保持關注。

一、背景

QMQ 延遲消息是以服務形式獨立存在的一套不侷限於消息廠商實現的解決方案,其架構如下圖所示。

QMQ 延遲消息服務架構

延遲消息從生產者投遞至延遲服務後,堆積在服務器本地磁盤中。當延遲消息調度時間過期後,延遲服務轉發至實時 Broker 供消費方消費。延遲服務採用主從架構,其中,Zone 表示一個可用區(一般可以理解成一個 IDC),爲了保證單可用區故障後,歷史投遞的待調度消息正常調度,master 和 slave 會跨可用區部署。

1.1 痛點

此架構主要存在如下幾點問題:

a)服務具有狀態,無法彈性擴縮容;

b)主節點故障後,需要主從切換(自動或手動);

c)缺少一致性協調器保障數據的一致性。

如果將消息的業務層和存儲層分離出來,各自演進協同發展,各自專注在擅長的領域。這樣,消息業務層可以做到無狀態化,輕鬆完成容器化改造,具備彈性擴縮容能力;存儲層引入分佈式文件存儲服務,由存儲服務來保證高可用與數據一致性。

1.2 分佈式文件存儲選型

對於存儲服務的選型,除了基本的高可用於數據一致性特點外,還有至關重要的一點:高容錯與低運維成本特性。分佈式系統最大的特點自然是對部分節點故障的容忍能力,畢竟任何硬件或軟件故障是不可百分百避免的。因此,高容錯與低運維成本將成爲我們選型中最爲看重的。

2016 年由雅虎開源貢獻給 Apache 的 Pulsar,因其雲原生、低延遲分佈式消息隊列與流式處理平臺的標籤,在開源社區引發轟動與追捧。在對其進行相關調研後,發現恰好 Pulsar 也是消息業務與存儲分離的架構,而存儲層則是另一個 Apache 開源基金會的 BookKeeper。

二、BookKeeper

BookKeeper 作爲一款可伸縮、高容錯、低延遲的分佈式強一致存儲服務已被部分公司應用於生產環境部署使用,最佳實踐案例包括替代 HDFS 的 namenode、Pulsar 的消息存儲與消費進度持久化以及對象存儲。

2.1 基本架構

BookKeeper 基本架構

Zookeeper 集羣用於存儲節點發現與元信息存儲,提供強一致性保證;

Bookie 存儲節點,提供數據的存儲服務。寫入和讀取過程中,Bookie 節點間彼此無須通信。Bookie 啓動時將自身註冊到 Zookeeper 集羣,暴露服務;

Client 屬於胖客戶端類型,負責與 Zookeeper 集羣和 BookKeeper 集羣直接通信,且根據元信息完成多副本的寫入,保證數據可重複讀。

2.2 基本特性

a)基本概念

Entry:數據載體的基本單元

Ledger:entry 集合的抽象,類似文件

Bookie:ledger 集合的抽象,物理存儲節點

Ensemble:ledger 的 bookie 集合

b)數據讀寫

BookKeeper 數據讀寫

bookie 客戶端通過創建而持有一個 ledger 後便可以進行 entry 寫入操作,entry 以帶狀方式分佈在 enemble 的 bookie 中。entry 在客戶端進行編號,每條 entry 會根據設置的副本數(Qw)要求判定寫入成功與否;

bookie 客戶端通過打開一個已創建的 ledger 進行 entry 讀取操作,entry 的讀取順序與寫入保持一致,默認從第一個副本中讀取,讀取失敗後順序從下一個副本重試。

c)數據一致性

持有可寫 ledger 的 bookie 客戶端稱爲 Writer,通過分佈式鎖機制確保一個 ledger 全局只有一個 Writer,Writer 的唯一性保證了數據寫入一致性。Writer 內存中維護一個 LAC(Last Add Confirmed),當滿足 Qw 要求後,更新 LAC。LAC 隨下一次請求或定時持久化在 bookie 副本中,當 ledger 關閉時,持久化在 Metadata Store(zookeeper 或 etcd)中;

持有可讀 ledger 的 bookie 客戶端稱爲 Reader,一個 ledger 可以有任意多個 Reader。LAC 的強一致性保證了不同 Reader 看到統一的數據視圖,亦可重複讀,從而保證了數據讀取一致性。

d)容錯性

典型故障場景:Writer crash 或 restart、Bookie crash。

Writer 故障,ledger 可能未關閉,導致 LAC 未知。通過 ledger recover 機制,關閉 ledger,修復 LAC;

Bookie 故障,entry 寫入失敗。通過 ensemble replace 機制,更新一條新的 entry 路由信息到 Metadata Store 中,保障了新數據能及時成功寫入。歷史數據,通過 bookie recover 機制,滿足 Qw 副本要求,夯實了歷史數據讀取的可靠性。至於副本所在的所有 bookie 節點全部故障場景,只能等待修復。

e)負載均衡

新擴容進集羣的 bookie,當創建新的 ledger 時,便自動均衡流量。

2.3 同城多中心容災

上海區域(region)存在多個可用區(az,available zone),各可用兩兩間網絡延遲低於 2ms,此種網絡架構下,多副本分散在不同的 az 間是一個可接受的高可用方案。BookKeeper 基於 Zone 感知的 ensemble 替換策略便是應對此種場景的解決方案。

基於 Zone 感知策略的同城多中心容災

開啓 Zone 感知策略有兩個限制條件:a)E % Qw == 0;b)Qw > minNumOfZones。其中 E 表示 ensemble 大小,Qw 表示副本數,minNumOfZones 表示 ensemble 中的最小 zone 數目。

譬如下面的例子:

minNumOfZones = 2
desiredNumZones = 3
E = 6
Qw = 3
[z1, z2, z3, z1, z2, z3]

故障前,每條數據具有三副本,且分佈在三個可用區中;當 z1 故障後,將以滿足 minNumOfZones 限制生成新的 ensemble:[z1, z2, z3, z1, z2, z3] -> [z3, z2, z3, z3, z2, z3]。顯然對於三副本的每條數據仍將分佈在兩個可用區中,仍能容忍一個可用區故障。

DNSResolver

客戶端在挑選 bookie 組成 ensemble 時,需要通過 ip 反解出對應的 zone 信息,需要用戶實現解析器。考慮到 zone 與 zone 間網段是認爲規劃且不重合的,因此,我們落地時,簡單的實現了一個可動態配置生效的子網解析器。示例給出的是 ip 精確匹配的實現方式。

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 解析器示例

數據副本分佈在單 zone

當某些原因(譬如可用區故障演練)導致只有一個可用區可用時,新寫入的數據的全部副本都將落在單可用區,當故障可用區恢復後,仍然有部分歷史數據只存在於單可用區,不滿足多可用區容災的高可用需求。

AutoRecovery 機制中有一個 PlacementPolicy 檢測機制,但缺少恢復機制。於是我們打了個 patch,支持動態機制開啓和關閉此功能。這樣,當可用區故障恢復後可以自動發現和修復數據全部副本分佈在單可用區從而影響數據可用性的問題。

三、彈性架構落地

引入 BookKeeper 後,延遲消息服務的架構相對漂亮不少。消息業務層面和存儲層面完全分離,延遲消息服務本身無狀態化,可以輕易伸縮。當可用區故障後,不再需要主從切換。

延遲消息服務新架構

3.1 無狀態化改造

存儲層分離出去後,業務層實現無狀態化成爲可能。要達成這一目標,還需解決一些問題。我們先看看 BookKeeper 使用上的一些約束:

1)BookKeeper 不支持共享寫入的,也即業務層多個節點如果都寫數據,則各自寫的必然是不同的 ledger;

2)雖然 BookKeeper 允許多讀,但多個應用節點各自讀取的話,進度是相互獨立的,應用必須自行解決進度協調問題。

上述兩個主要問題,決定我們實現無狀態和彈性擴縮容時,必需自行解決讀寫資源分配的問題。爲此,我們引入了任務協調器。

我們首先將存儲資源進行分片管理,每個分片上都支持讀寫操作,但同一時刻只能有一個業務層節點來讀寫。如果我們把分片看作資源,把業務層節點看作工作者,那麼任務協調器的主要職責爲:

1)在儘可能平均的前提下以粘滯優先的方式把資源分配給工作者;

2)監視資源和工作者的變化,如有增減,重新執行職責 1;

3)在資源不夠用時,根據具體策略配置,添加初始化新的資源。

由於是分佈式環境,協調器自身完成上述職責時需要保證分佈式一致性,當然還要滿足可用性要求。我們選擇了基於 ZooKeeper 進行選主的一主多從式架構。

如圖所示,協調器對等部署在業務層應用節點中。運行時,協調器通過基於 ZooKeeper 的 leader 競選機制決出 leader 節點,並由 leader 節點負責前述任務分配工作。

協調器選舉的實現參考 ZooKeeper 官方文檔,這裏不再贅述。

3.2 持久化數據

原有架構將延遲消息根據調度時間按每 10 分鐘桶存儲在本地,時間臨近的桶加載到內存中,使用 HashedWheelTimer 來調度。該設計存在兩個弊端:

弊端 1 的話,單機本地 10 萬 + 文件還不算多大問題,但改造後這些桶信息以元信息的方式存儲在 ZooKeeper 上,我們的實現方案決定了每個桶至少佔用 3 個 ZooKeeper 節點。假設我們要部署 5 個集羣,平均每個集羣有 10 個分片,每個分片有 10 萬個桶,那使用的 ZooKeeper 節點數量就是 1500 萬起,這個量級是 ZooKeeper 難以承受的。

弊端 2 則無論新老架構,都是個潛在問題。一旦某個 10 分鐘消息量多一些,就可能導致消息延遲。往內存加載時,應該有更細的顆粒度纔好。

基於以上問題分析,我們參考多級時間輪調度的思路,略加變化,設計了一套基於滑動時間分桶的多級調度方案。

nzfAOz

如上表所示,最大的桶是 1 周,其次是 1 天,1 小時,1 分鐘。每個級別覆蓋不同的時間範圍,組合起來覆蓋 2 年的時間範圍理論上只需 286 個桶,相比原來的 10 萬多個桶有了質的縮減。

同時,只有 L0m 這一級調度器需要加載數據到 HashedWheelTimer,故而加載粒度細化到了 1 分鐘,大大減少了因不能完整加載一個桶而導致的調度延遲。

多級調度器以類似串聯的方式協同工作。

每一級調度器收到寫入請求時,首先嚐試委託給其上級(顆粒度更大)調度器處理。如果上級接受,則只需將上級的處理結果向下返回;如果上級不接受,再判斷是否歸屬本級,是的話寫入桶中,否則打回給下級。

每一級調度器都會將時間臨近的桶打開併發送其中的數據到下一級調度器。比如 L1h 發現最小的桶到了預加載時間,則把該桶的數據讀出併發送給 L0m 調度器,最終該小時的數據被轉移到 L0m 並展開爲(最多)60 個分鐘級的桶。

四、未來規劃

目前 bookie 集羣部署在物理機上,集羣新建、擴縮容相對比較麻煩,未來將考慮融入 k8s 體系;bookie 的治理與平臺化也是需要考慮的;我們目前只具備同城多中心容災能力,跨 region 容災以及公 / 私混合雲容災等高可用架構也需要進一步補強。

參考文檔

QMQ 開源地址:

https://github.com/qunarcorp/qmq

Pulsar 官網:

https://pulsar.apache.org/

BookKeeper 官網:

https://bookkeeper.apache.org/

爲什麼選擇 BookKeeper:

https://medium.com/streamnative/why-apache-bookkeeper-part-1-consistency-durability-availability-ac697a3cf7a1

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/lI2aN_JR8mFE9sauh8SQFA