阿里的 RocketMQ 如何让双十一峰值之下0故障
作者?|?愈安
來源 |?阿里巴巴中間件
頭圖?|?下載于視覺中國
2020 年的雙十一交易峰值達到 58.3 W筆/秒,消息中間件 RocketMQ 繼續(xù)數(shù)年 0 故障絲般順滑地完美支持了整個集團大促的各類業(yè)務平穩(wěn)。相比往年,消息中間件 RocketMQ 發(fā)生了以下幾個方面的變化:
云原生化實踐。完成運維層面的云原生化改造,實現(xiàn) Kubernetes 化。
性能優(yōu)化。消息過濾優(yōu)化交易集群性能提升 30%。
全新的消費模型。對于延遲敏感業(yè)務提供新的消費模式,降低因發(fā)布、重啟等場景下導致的消費延遲。
云原生化實踐
? 背景?
Kubernetes 作為目前云原生化技術(shù)棧實踐中重要的一環(huán),其生態(tài)已經(jīng)逐步建立并日益豐富。目前,服務于集團內(nèi)部的 RocketMQ 集群擁有巨大的規(guī)模以及各種歷史因素,因此在運維方面存在相當一部分痛點,我們希望能夠通過云原生技術(shù)棧來嘗試找到對應解決方案,并同時實現(xiàn)降本提效,達到無人值守的自動化運維。
消息中間件早在 2016 年,通過內(nèi)部團隊提供的中間件部署平臺實現(xiàn)了容器化和自動化發(fā)布,整體的運維比 2016 年前已經(jīng)有了很大的提高,但是作為一個有狀態(tài)的服務,在運維層面仍然存在較多的問題。
中間件部署平臺幫我們完成了資源的申請,容器的創(chuàng)建、初始化、鏡像安裝等一系列的基礎(chǔ)工作,但是因為中間件各個產(chǎn)品都有自己不同的部署邏輯,所以在應用的發(fā)布上,就是各應用自己的定制化了。中間件部署平臺的開發(fā)也不完全了解集團內(nèi) RocketMQ 的部署過程是怎樣的。
因此在 2016 年的時候,部署平臺需要我們?nèi)ビH自實現(xiàn)消息中間件的應用發(fā)布代碼。雖然部署平臺大大提升了我們的運維效率,甚至還能實現(xiàn)一鍵發(fā)布,但是這樣的方案也有不少的問題。比較明顯的就是,當我們的發(fā)布邏輯有變化的時候,還需要去修改部署平臺對應的代碼,需要部署平臺升級來支持我們,用最近比較流行的一個說法,就是相當不云原生。
同樣在故障機替換、集群縮容等操作中,存在部分人工參與的工作,如切流,堆積數(shù)據(jù)的確認等。我們嘗試過在部署平臺中集成更多消息中間件自己的運維邏輯,不過在其他團隊的工程里寫自己的業(yè)務代碼,確實也是一個不太友好的實現(xiàn)方案,因此我們希望通過 Kubernetes 來實現(xiàn)消息中間件自己的 operator 。我們同樣希望利用云化后云盤的多副本能力來降低我們的機器成本并降低主備運維的復雜程度。
經(jīng)過一段時間的跟進與探討,最終再次由內(nèi)部團隊承擔了建設(shè)云原生應用運維平臺的任務,并依托于中間件部署平臺的經(jīng)驗,借助云原生技術(shù)棧,實現(xiàn)對有狀態(tài)應用自動化運維的突破。
??實現(xiàn)
整體的實現(xiàn)方案如上圖所示,通過自定義的 CRD 對消息中間件的業(yè)務模型進行抽象,將原有的在中間件部署平臺的業(yè)務發(fā)布部署邏輯下沉到消息中間件自己的 operator 中,托管在內(nèi)部 Kubernetes 平臺上。該平臺負責所有的容器生產(chǎn)、初始化以及集團內(nèi)一切線上環(huán)境的基線部署,屏蔽掉 IaaS 層的所有細節(jié)。
Operator 承擔了所有的新建集群、擴容、縮容、遷移的全部邏輯,包括每個 pod 對應的 brokerName 自動生成、配置文件,根據(jù)集群不同功能而配置的各種開關(guān),元數(shù)據(jù)的同步復制等等。同時之前一些人工的相關(guān)操作,比如切流時候的流量觀察,下線前的堆積數(shù)據(jù)觀察等也全部集成到了 operator 中。當我們有需求重新修改各種運維邏輯的時候,也再也不用去依賴通用的具體實現(xiàn),修改自己的 operator 即可。
最后線上的實際部署情況去掉了圖中的所有的 replica 備機。在 Kubernetes 的理念中,一個集群中每個實例的狀態(tài)是一致的,沒有依賴關(guān)系,而如果按照消息中間件原有的主備成對部署的方案,主備之間是有嚴格的對應關(guān)系,并且在上下線發(fā)布過程中有嚴格的順序要求,這種部署模式在 Kubernetes 的體系下是并不提倡的。若依然采用以上老的架構(gòu)方式,會導致實例控制的復雜性和不可控性,同時我們也希望能更多的遵循 Kubernetes 的運維理念。
云化后的 ECS 使用的是高速云盤,底層將對數(shù)據(jù)做了多備份,因此數(shù)據(jù)的可用性得到了保障。并且高速云盤在性能上完全滿足 MQ 同步刷盤,因此,此時就可以把之前的異步刷盤改為同步,保證消息寫入時的不丟失問題。云原生模式下,所有的實例環(huán)境均是一致性的,依托容器技術(shù)和 Kubernetes 的技術(shù),可實現(xiàn)任何實例掛掉(包含宕機引起的掛掉),都能自動自愈,快速恢復。
解決了數(shù)據(jù)的可靠性和服務的可用性后,整個云原生化后的架構(gòu)可以變得更加簡單,只有 broker 的概念,再無主備之分。
??大促驗證
上圖是 Kubernetes 上線后雙十一大促當天的發(fā)送 RT 統(tǒng)計,可見大促期間的發(fā)送 RT 較為平穩(wěn),整體符合預期,云原生化實踐完成了關(guān)鍵性的里程碑。
性能優(yōu)化
??背景
RocketMQ 至今已經(jīng)連續(xù)七年 0 故障支持集團的雙十一大促。自從 RocketMQ 誕生以來,為了能夠完全承載包括集團業(yè)務中臺交易消息等核心鏈路在內(nèi)的各類關(guān)鍵業(yè)務,復用了原有的上層協(xié)議邏輯,使得各類業(yè)務方完全無感知的切換到 RocketMQ 上,并同時充分享受了更為穩(wěn)定和強大的 RocketMQ 消息中間件的各類特性。
當前,申請訂閱業(yè)務中臺的核心交易消息的業(yè)務方一直都在不斷持續(xù)增加,并且隨著各類業(yè)務復雜度提升,業(yè)務方的消息訂閱配置也變得更加復雜繁瑣,從而使得交易集群的進行過濾的計算邏輯也變得更為復雜。這些業(yè)務方部分沿用舊的協(xié)議邏輯(Header過濾),部分使用 RocketMQ 特有的 SQL 過濾。
??主要成本
目前集團內(nèi)部 RocketMQ 的大促機器成本絕大部分都是交易消息相關(guān)的集群,在雙十一零點峰值期間,交易集群的峰值和交易峰值成正比,疊加每年新增的復雜訂閱帶來了額外 CPU 過濾計算邏輯,交易集群都是大促中機器成本增長最大的地方。
??優(yōu)化過程
由于歷史原因,大部分的業(yè)務方主要還是使用Header過濾,內(nèi)部實現(xiàn)其實是aviator表達式( https://github.com/killme2008/aviatorscript )。仔細觀察交易消息集群的業(yè)務方過濾表達式,可以發(fā)現(xiàn)絕大部分都指定類似 MessageType == xxxx 這樣的條件。翻看aviator的源碼可以發(fā)現(xiàn)這樣的條件最終會調(diào)用Java的字符串比較 String.compareTo()。
由于交易消息包括大量不同業(yè)務的MessageType,光是有記錄的起碼有幾千個,隨著交易業(yè)務流程復雜化,MessageType的增長更是繁多。隨著交易峰值的提高,交易消息峰值正比增長,疊加這部分更加復雜的過濾,持續(xù)增長的將來,交易集群的成本極可能和交易峰值指數(shù)增長,因此決心對這部分進行優(yōu)化。
原有的過濾流程如下,每個交易消息需要逐個匹配不同group的訂閱關(guān)系表達式,如果符合表達式,則選取對應的group的機器進行投遞。如下圖所示:
對此流程進行優(yōu)化的思路需要一定的靈感,在這里借助數(shù)據(jù)庫索引的思路:原有流程可以把所有訂閱方的過濾表達式看作數(shù)據(jù)庫的記錄,每次消息過濾就相當于一個帶有特定條件的數(shù)據(jù)庫查詢,把所有匹配查詢(消息)的記錄(過濾表達式)選取出來作為結(jié)果。為了加快查詢結(jié)果,可以選擇 MessageType 作為一個索引字段進行索引化,每次查詢變?yōu)橄绕ヅ?MessageType 主索引,然后把匹配上主索引的記錄再進行其它條件(如下圖的 sellerId 和 testA )匹配,優(yōu)化流程如下圖所示:
以上優(yōu)化流程確定后,要關(guān)注的技術(shù)點有兩個:
1. 如何抽取每個表達式中的 MessageType 字段?
2. 如何對 MessageType 字段進行索引化?
對于技術(shù)點 1 ,需要針對 aviator 的編譯流程進行 hook ,深入 aviator 源碼后,可以發(fā)現(xiàn) aviator 的編譯是典型的 Recursive descent :
?http://en.wikipedia.org/wiki/Recursive_descent_parser?
同時需要考慮到提取后父表達式的短路問題。
在編譯過程中針對 messageType==XXX 這種類型進行提取后,把原有的 message==XXX 轉(zhuǎn)變?yōu)?true/false 兩種情況,然后針對 true、false 進行表達式的短路即可得出表達式優(yōu)化提取后的情況。例如:
表達式:messageType=='200-trade-paid-done' && buyerId==123456提取為兩個子表達式:子表達式1(messageType==200-trade-paid-done):buyerId==123456 子表達式2(messageType!=200-trade-paid-done):false具體到 aviator 的實現(xiàn)里,表達式編譯會把每個 token 構(gòu)建一個 List ,類似如下圖所示(為方便理解,綠色方框的是 token ,其它框表示表達式的具體條件組合):
提取了 messageType ,有兩種情況:
情況一:messageType == '200-trade-paid-done',則把之前 token 的位置合并成true,然后進行表達式短路計算,最后優(yōu)化成 buyerId==123456 ,具體如下:
情況二:messageType != '200-trade-paid-done',則把之前 token 的位置合并成 false ,表達式短路計算后,最后優(yōu)化成 false ,具體如下:
這樣就完成 messageType 的提取。這里可能有人就有一個疑問,為什么要考慮到上面的情況二,messageType != '200-trade-paid-done',這是因為必須要考慮到多個條件的時候,比如:
(messageType=='200-trade-paid-done' && buyerId==123456) || (messageType=='200-trade-success' && buyerId==3333)
就必須考慮到不等于的情況了。同理,如果考慮到多個表達式嵌套,需要逐步進行短路計算。但整體邏輯是類似的,這里就不再贅述。
說完技術(shù)點1,我們繼續(xù)關(guān)注技術(shù)點2,考慮到高效過濾,直接使用 HashMap 結(jié)構(gòu)進行索引化即可,即把 messageType 的值作為 HashMap 的 key ,把提取后的子表達式作為 HashMap 的 value ,這樣每次過濾直接通過一次 hash 計算即可過濾掉絕大部分不適合的表達式,大大提高了過濾效率。
??優(yōu)化效果
該優(yōu)化最主要降低了 CPU 計算邏輯,根據(jù)優(yōu)化前后的性能情況對比,我們發(fā)現(xiàn)不同的交易集群中的訂閱方訂閱表達式復雜度越高,優(yōu)化效果越好,這個是符合我們的預期的,其中最大的 CPU 優(yōu)化有 32% 的提升,大大降低了本年度 RocketMQ 的部署機器成本。
全新的消費模型 —— POP 消費
??背景
RocketMQ的PULL消費對于機器異常hang時并不十分友好。如果遇到客戶端機器hang住,但處于半死不活的狀態(tài),與broker的心跳沒有斷掉的時候,客戶端rebalance依然會分配消費隊列到hang機器上,并且hang機器消費速度很慢甚至無法消費的時候,這樣會導致消費堆積。另外類似還有服務端Broker發(fā)布時,也會由于客戶端多次rebalance導致消費延遲影響等無法避免的問題。如下圖所示:
當Pull Client 2發(fā)生hang機器的時候,它所分配到的三個Broker上的Q2都出現(xiàn)嚴重的紅色堆積。對于此,我們增加了一種新的消費模型——POP消費,能夠解決此類穩(wěn)定性問題。如下圖所示:
POP消費中,三個客戶端并不需要rebalance去分配消費隊列,取而代之的是,它們都會使用POP請求所有的broker獲取消息進行消費。broker內(nèi)部會把自身的三個隊列的消息根據(jù)一定的算法分配給請求的POP Client。即使Pop Client 2出現(xiàn)hang,但內(nèi)部隊列的消息也會讓Pop Client1 和Pop Client2進行消費。這樣就hang機器造成的避免了消費堆積。
??實現(xiàn)
POP 消費和原來 PULL 消費對比,最大的一點就是弱化了隊列這個概念,PULL 消費需要客戶端通過 rebalance 把 broker 的隊列分配好,從而去消費分配到自己專屬的隊列,新的 POP 消費中,客戶端的機器會直接到每個 broker 的隊列進行請求消費, broker 會把消息分配返回給等待的機器。隨后客戶端消費結(jié)束后返回對應的 Ack 結(jié)果通知 broker,broker 再標記消息消費結(jié)果,如果超時沒響應或者消費失敗,再會進行重試。
POP 消費的架構(gòu)圖如上圖所示。Broker 對于每次 POP 的請求,都會有以下三個操作:
1. 對應的隊列進行加鎖,然后從 store 層獲取該隊列的消息;
2. 然后寫入 CK 消息,表明獲取的消息要被 POP 消費;
3. 最后提交當前位點,并釋放鎖。
CK 消息實際上是記錄了 POP 消息具體位點的定時消息,當客戶端超時沒響應的時候,CK 消息就會重新被 broker 消費,然后把 CK 消息的位點的消息寫入重試隊列。如果 broker 收到客戶端的消費結(jié)果的 Ack ,刪除對應的 CK 消息,然后根據(jù)具體結(jié)果判斷是否需要重試。
從整體流程可見,POP 消費并不需要 reblance ,可以避免 rebalance 帶來的消費延時,同時客戶端可以消費 broker 的所有隊列,這樣就可以避免機器 hang 而導致堆積的問題。
更多閱讀推薦
開源的新型云原生事件驅(qū)動架構(gòu)實踐解析
從 Serverfull 到 Serverless,發(fā)生了什
私有云OS賽道,反而越開放越好?
亂中有變,云原生從“大爆發(fā)”說起
疫情中的2021,云原生會走向哪里
分布式架構(gòu)的王者?Kubernetes憑什么
總結(jié)
以上是生活随笔為你收集整理的阿里的 RocketMQ 如何让双十一峰值之下0故障的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 又一低代码平台火了!15 分钟小白轻松开
- 下一篇: “精耕细作”桌面云市场的锐捷,重磅发布三