天天看點

阿裡的 RocketMQ 如何讓雙十一峰值之下 0 故障?雲原生化實踐性能優化全新的消費模型 —— POP 消費

阿裡的 RocketMQ 如何讓雙十一峰值之下 0 故障?雲原生化實踐性能優化全新的消費模型 —— POP 消費

作者 | 愈安

來源 |

阿裡巴巴雲原生公衆号

2020 年雙十一交易峰值達到 58.3 W 筆/秒,消息中間件 RocketMQ 繼續數年 0 故障絲般順滑地完美支援了整個集團大促的各類業務平穩。2020 年雙十一大促中,消息中間件 RocketMQ 發生了以下幾個方面的變化:

  • 雲原生化實踐:完成運維層面的雲原生化改造,實作 Kubernetes 化。
  • 性能優化:消息過濾優化交易叢集性能提升 30%。
  • 全新的消費模型:對于延遲敏感業務提供新的消費模式,降低因釋出、重新開機等場景下導緻的消費延遲。

雲原生化實踐

1. 背景

Kubernetes 作為目前雲原生化技術棧實踐中重要的一環,其生态已經逐漸建立并日益豐富。目前,服務于集團内部的 RocketMQ 叢集擁有巨大的規模以及各種曆史因素,是以在運維方面存在相當一部分痛點,我們希望能夠通過雲原生技術棧來嘗試找到對應解決方案,并同時實作降本提效,達到無人值守的自動化運維。

消息中間件早在 2016 年,通過内部團隊提供的中間件部署平台實作了容器化和自動化釋出,整體的運維比 2016 年前已經有了很大的提高,但是作為一個有狀态的服務,在運維層面仍然存在較多的問題。

中間件部署平台幫我們完成了資源的申請,容器的建立、初始化、鏡像安裝等一系列的基礎工作,但是因為中間件各個産品都有自己不同的部署邏輯,是以在應用的釋出上,就是各應用自己的定制化了。中間件部署平台的開發也不完全了解集團内 RocketMQ 的部署過程是怎樣的。

是以在 2016 年的時候,部署平台需要我們去親自實作消息中間件的應用釋出代碼。雖然部署平台大大提升了我們的運維效率,甚至還能實作一鍵釋出,但是這樣的方案也有不少的問題。比較明顯的就是,當我們的釋出邏輯有變化的時候,還需要去修改部署平台對應的代碼,需要部署平台更新來支援我們,用最近比較流行的一個說法,就是相當不雲原生。

同樣在故障機替換、叢集縮容等操作中,存在部分人工參與的工作,如切流,堆積資料的确認等。我們嘗試過在部署平台中內建更多消息中間件自己的運維邏輯,不過在其他團隊的工程裡寫自己的業務代碼,确實也是一個不太友好的實作方案,是以我們希望通過 Kubernetes 來實作消息中間件自己的 operator 。我們同樣希望利用雲化後雲盤的多副本能力來降低我們的機器成本并降低主備運維的複雜程度。

經過一段時間的跟進與探讨,最終再次由内部團隊承擔了建設雲原生應用運維平台的任務,并依托于中間件部署平台的經驗,借助雲原生技術棧,實作對有狀态應用自動化運維的突破。

2. 實作

阿裡的 RocketMQ 如何讓雙十一峰值之下 0 故障?雲原生化實踐性能優化全新的消費模型 —— POP 消費

整體的實作方案如上圖所示,通過自定義的 CRD 對消息中間件的業務模型進行抽象,将原有的在中間件部署平台的業務釋出部署邏輯下沉到消息中間件自己的 operator 中,托管在内部 Kubernetes 平台上。該平台負責所有的容器生産、初始化以及集團内一切線上環境的基線部署,屏蔽掉 IaaS 層的所有細節。

Operator 承擔了所有的建立叢集、擴容、縮容、遷移的全部邏輯,包括每個 pod 對應的 brokerName 自動生成、配置檔案,根據叢集不同功能而配置的各種開關,中繼資料的同步複制等等。同時之前一些人工的相關操作,比如切流時候的流量觀察,下線前的堆積資料觀察等也全部內建到了 operator 中。當我們有需求重新修改各種運維邏輯的時候,也再也不用去依賴通用的具體實作,修改自己的 operator 即可。

最後線上的實際部署情況去掉了圖中的所有的 replica 備機。在 Kubernetes 的理念中,一個叢集中每個執行個體的狀态是一緻的,沒有依賴關系,而如果按照消息中間件原有的主備成對部署的方案,主備之間是有嚴格的對應關系,并且在上下線釋出過程中有嚴格的順序要求,這種部署模式在 Kubernetes 的體系下是并不提倡的。若依然采用以上老的架構方式,會導緻執行個體控制的複雜性和不可控性,同時我們也希望能更多的遵循 Kubernetes 的運維理念。

雲化後的 ECS 使用的是高速雲盤,底層将對資料做了多備份,是以資料的可用性得到了保障。并且高速雲盤在性能上完全滿足 MQ 同步刷盤,是以,此時就可以把之前的異步刷盤改為同步,保證消息寫入時的不丢失問題。雲原生模式下,所有的執行個體環境均是一緻性的,依托容器技術和 Kubernetes 的技術,可實作任何執行個體挂掉(包含當機引起的挂掉),都能自動自愈,快速恢複。

解決了資料的可靠性和服務的可用性後,整個雲原生化後的架構可以變得更加簡單,隻有 broker 的概念,再無主備之分。

3. 大促驗證

阿裡的 RocketMQ 如何讓雙十一峰值之下 0 故障?雲原生化實踐性能優化全新的消費模型 —— POP 消費

上圖是 Kubernetes 上線後雙十一大促當天的發送 RT 統計,可見大促期間的發送 RT 較為平穩,整體符合預期,雲原生化實踐完成了關鍵性的裡程碑。

性能優化

RocketMQ 至今已經連續七年 0 故障支援集團的雙十一大促。自從 RocketMQ 誕生以來,為了能夠完全承載包括集團業務中台交易消息等核心鍊路在内的各類關鍵業務,複用了原有的上層協定邏輯,使得各類業務方完全無感覺的切換到 RocketMQ 上,并同時充分享受了更為穩定和強大的 RocketMQ 消息中間件的各類特性。

目前,申請訂閱業務中台的核心交易消息的業務方一直都在不斷持續增加,并且随着各類業務複雜度提升,業務方的消息訂閱配置也變得更加複雜繁瑣,進而使得交易叢集的進行過濾的計算邏輯也變得更為複雜。這些業務方部分沿用舊的協定邏輯(Header 過濾),部分使用 RocketMQ 特有的 SQL 過濾。

2. 主要成本

目前集團内部 RocketMQ 的大促機器成本絕大部分都是交易消息相關的叢集,在雙十一零點峰值期間,交易叢集的峰值和交易峰值成正比,疊加每年新增的複雜訂閱帶來了額外 CPU 過濾計算邏輯,交易叢集都是大促中機器成本增長最大的地方。

3. 優化過程

由于曆史原因,大部分的業務方主要還是使用 Header 過濾,内部實作其實是aviator 表達式。仔細觀察交易消息叢集的業務方過濾表達式,可以發現絕大部分都指定類似MessageType == xxxx這樣的條件。翻看 aviator 的源碼可以發現這樣的條件最終會調用 Java 的字元串比較String.compareTo()。

由于交易消息包括大量不同業務的 MessageType,光是有記錄的起碼有幾千個,随着交易業務流程複雜化,MessageType 的增長更是繁多。随着交易峰值的提高,交易消息峰值正比增長,疊加這部分更加複雜的過濾,持續增長的将來,交易叢集的成本極可能和交易峰值指數增長,是以決心對這部分進行優化。

原有的過濾流程如下,每個交易消息需要逐個比對不同 group 的訂閱關系表達式,如果符合表達式,則選取對應的 group 的機器進行投遞。如下圖所示:

阿裡的 RocketMQ 如何讓雙十一峰值之下 0 故障?雲原生化實踐性能優化全新的消費模型 —— POP 消費

對此流程進行優化的思路需要一定的靈感,在這裡借助資料庫索引的思路:原有流程可以把所有訂閱方的過濾表達式看作資料庫的記錄,每次消息過濾就相當于一個帶有特定條件的資料庫查詢,把所有比對查詢(消息)的記錄(過濾表達式)選取出來作為結果。為了加快查詢結果,可以選擇 MessageType 作為一個索引字段進行索引化,每次查詢變為先比對 MessageType 主索引,然後把比對上主索引的記錄再進行其它條件(如下圖的 sellerId 和 testA )比對,優化流程如下圖所示:

阿裡的 RocketMQ 如何讓雙十一峰值之下 0 故障?雲原生化實踐性能優化全新的消費模型 —— POP 消費

以上優化流程确定後,要關注的技術點有兩個:

  • 如何抽取每個表達式中的 MessageType 字段?
  • 如何對 MessageType 字段進行索引化?

對于技術點 1 ,需要針對 aviator 的編譯流程進行 hook ,深入 aviator 源碼後,可以發現 aviator 的編譯是典型的Recursive descent,同時需要考慮到提取後父表達式的短路問題。

在編譯過程中針對 messageType==XXX 這種類型進行提取後,把原有的 message==XXX 轉變為 true/false 兩種情況,然後針對 true、false 進行表達式的短路即可得出表達式優化提取後的情況。例如:

表達式:
messageType=='200-trade-paid-done' && buyerId==123456
提取為兩個子表達式:
子表達式1(messageType==200-trade-paid-done):buyerId==123456 
子表達式2(messageType!=200-trade-paid-done):false           
  • 具體到 aviator 的實作裡,表達式編譯會把每個 token 建構一個 List ,類似如下圖所示(為友善了解,綠色方框的是 token ,其它框表示表達式的具體條件組合):
阿裡的 RocketMQ 如何讓雙十一峰值之下 0 故障?雲原生化實踐性能優化全新的消費模型 —— POP 消費

提取了 messageType ,有兩種情況:

  • 情況一:messageType == '200-trade-paid-done',則把之前 token 的位置合并成true,然後進行表達式短路計算,最後優化成 buyerId==123456 ,具體如下:
阿裡的 RocketMQ 如何讓雙十一峰值之下 0 故障?雲原生化實踐性能優化全新的消費模型 —— POP 消費
  • 情況二:messageType != '200-trade-paid-done',則把之前 token 的位置合并成 false ,表達式短路計算後,最後優化成 false ,具體如下:
阿裡的 RocketMQ 如何讓雙十一峰值之下 0 故障?雲原生化實踐性能優化全新的消費模型 —— POP 消費

這樣就完成 messageType 的提取。這裡可能有人就有一個疑問,為什麼要考慮到上面的情況二,messageType != '200-trade-paid-done',這是因為必須要考慮到多個條件的時候,比如:

(messageType=='200-trade-paid-done' && buyerId==123456) || (messageType=='200-trade-success' && buyerId==3333)

就必須考慮到不等于的情況了。同理,如果考慮到多個表達式嵌套,需要逐漸進行短路計算。但整體邏輯是類似的,這裡就不再贅述。

說完技術點 1,我們繼續關注技術點 2,考慮到高效過濾,直接使用 HashMap 結構進行索引化即可,即把 messageType 的值作為 HashMap 的 key ,把提取後的子表達式作為 HashMap 的 value ,這樣每次過濾直接通過一次 hash 計算即可過濾掉絕大部分不适合的表達式,大大提高了過濾效率。

3. 優化效果

該優化最主要降低了 CPU 計算邏輯,根據優化前後的性能情況對比,我們發現不同的交易叢集中的訂閱方訂閱表達式複雜度越高,優化效果越好,這個是符合我們的預期的,其中最大的 CPU 優化有32%的提升,大大降低了本年度 RocketMQ 的部署機器成本。

全新的消費模型 —— POP 消費

RocketMQ 的 PULL 消費對于機器異常 hang 時并不十分友好。如果遇到用戶端機器 hang 住,但處于半死不活的狀态,與 broker 的心跳沒有斷掉的時候,用戶端 rebalance 依然會配置設定消費隊列到 hang 機器上,并且 hang 機器消費速度很慢甚至無法消費的時候,這樣會導緻消費堆積。另外類似還有服務端 Broker 釋出時,也會由于用戶端多次 rebalance 導緻消費延遲影響等無法避免的問題。如下圖所示:

阿裡的 RocketMQ 如何讓雙十一峰值之下 0 故障?雲原生化實踐性能優化全新的消費模型 —— POP 消費

當 Pull Client 2 發生 hang 機器的時候,它所配置設定到的三個 Broker 上的 Q2 都出現嚴重的紅色堆積。對于此,我們增加了一種新的消費模型 —— POP 消費,能夠解決此類穩定性問題。如下圖所示:

阿裡的 RocketMQ 如何讓雙十一峰值之下 0 故障?雲原生化實踐性能優化全新的消費模型 —— POP 消費

POP 消費中,三個用戶端并不需要 rebalance 去配置設定消費隊列,取而代之的是,它們都會使用 POP 請求所有的 broker 擷取消息進行消費。broker 内部會把自身的三個隊列的消息根據一定的算法配置設定給請求的 POP Client。即使 Pop Client 2 出現 hang,但内部隊列的消息也會讓 Pop Client1 和 Pop Client2 進行消費。這樣就 hang 機器造成的避免了消費堆積。

POP 消費和原來 PULL 消費對比,最大的一點就是弱化了隊列這個概念,PULL 消費需要用戶端通過 rebalance 把 broker 的隊列配置設定好,進而去消費配置設定到自己專屬的隊列,新的 POP 消費中,用戶端的機器會直接到每個 broker 的隊列進行請求消費, broker 會把消息配置設定傳回給等待的機器。随後用戶端消費結束後傳回對應的 Ack 結果通知 broker,broker 再标記消息消費結果,如果逾時沒響應或者消費失敗,再會進行重試。

阿裡的 RocketMQ 如何讓雙十一峰值之下 0 故障?雲原生化實踐性能優化全新的消費模型 —— POP 消費

POP 消費的架構圖如上圖所示。Broker 對于每次 POP 的請求,都會有以下三個操作:

  • 對應的隊列進行加鎖,然後從 store 層擷取該隊列的消息;
  • 然後寫入 CK 消息,表明擷取的消息要被 POP 消費;
  • 最後送出目前位點,并釋放鎖。

CK 消息實際上是記錄了 POP 消息具體位點的定時消息,當用戶端逾時沒響應的時候,CK 消息就會重新被 broker 消費,然後把 CK 消息的位點的消息寫入重試隊列。如果 broker 收到用戶端的消費結果的 Ack ,删除對應的 CK 消息,然後根據具體結果判斷是否需要重試。

從整體流程可見,POP 消費并不需要 reblance ,可以避免 rebalance 帶來的消費延時,同時用戶端可以消費 broker 的所有隊列,這樣就可以避免機器 hang 而導緻堆積的問題。

阿裡的 RocketMQ 如何讓雙十一峰值之下 0 故障?雲原生化實踐性能優化全新的消費模型 —— POP 消費