做者 | 愈安
來源|阿里巴巴雲原生公衆號git
2020 年雙十一交易峯值達到 58.3W 筆/秒,消息中間件 RocketMQ 繼續數年 0 故障絲般順滑地完美支持了整個集團大促的各種業務平穩。今年雙十一大促中,消息中間件 RocketMQ 發生瞭如下幾個方面的變化:github
雲原生化實踐。完成運維層面的雲原生化改造,實現 Kubernetes 化。算法
性能優化。消息過濾優化交易集羣性能提高 30%。數據庫
Kubernetes 做爲目前雲原生化技術棧實踐中重要的一環,其生態已經逐步創建並日益豐富。目前,服務於集團內部的 RocketMQ 集羣擁有巨大的規模以及各類歷史因素,所以在運維方面存在至關一部分痛點,咱們但願可以經過雲原生技術棧來嘗試找到對應解決方案,並同時實現降本提效,達到無人值守的自動化運維。性能優化
消息中間件早在 2016 年,經過內部團隊提供的中間件部署平臺實現了容器化和自動化發佈,總體的運維比 2016 年前已經有了很大的提升,可是做爲一個有狀態的服務,在運維層面仍然存在較多的問題。架構
中間件部署平臺幫咱們完成了資源的申請,容器的建立、初始化、鏡像安裝等一系列的基礎工做,可是由於中間件各個產品都有本身不一樣的部署邏輯,因此在應用的發佈上,就是各應用本身的定製化了。中間件部署平臺的開發也不徹底瞭解集團內 RocketMQ 的部署過程是怎樣的。運維
所以在 2016 年的時候,部署平臺須要咱們去親自實現消息中間件的應用發佈代碼。雖然部署平臺大大提高了咱們的運維效率,甚至還能實現一鍵發佈,可是這樣的方案也有很多的問題。比較明顯的就是,當咱們的發佈邏輯有變化的時候,還須要去修改部署平臺對應的代碼,須要部署平臺升級來支持咱們,用最近比較流行的一個說法,就是至關不雲原生。異步
一樣在故障機替換、集羣縮容等操做中,存在部分人工參與的工做,如切流,堆積數據的確認等。咱們嘗試過在部署平臺中集成更多消息中間件本身的運維邏輯,不過在其餘團隊的工程裏寫本身的業務代碼,確實也是一個不太友好的實現方案,所以咱們但願經過 Kubernetes 來實現消息中間件本身的 operator 。咱們一樣但願利用雲化後雲盤的多副本能力來下降咱們的機器成本並下降主備運維的複雜程度。ide
通過一段時間的跟進與探討,最終再次由內部團隊承擔了建設雲原生應用運維平臺的任務,並依託於中間件部署平臺的經驗,藉助雲原生技術棧,實現對有狀態應用自動化運維的突破。性能
總體的實現方案如上圖所示,經過自定義的 CRD 對消息中間件的業務模型進行抽象,將原有的在中間件部署平臺的業務發佈部署邏輯下沉到消息中間件本身的 operator 中,託管在內部 Kubernetes 平臺上。該平臺負責全部的容器生產、初始化以及集團內一切線上環境的基線部署,屏蔽掉 IaaS 層的全部細節。
Operator 承擔了全部的新建集羣、擴容、縮容、遷移的所有邏輯,包括每一個 pod 對應的 brokerName 自動生成、配置文件,根據集羣不一樣功能而配置的各類開關,元數據的同步複製等等。同時以前一些人工的相關操做,好比切流時候的流量觀察,下線前的堆積數據觀察等也所有集成到了 operator 中。當咱們有需求從新修改各類運維邏輯的時候,也不再用去依賴通用的具體實現,修改本身的 operator 便可。
最後線上的實際部署狀況去掉了圖中的全部的 replica 備機。在 Kubernetes 的理念中,一個集羣中每一個實例的狀態是一致的,沒有依賴關係,而若是按照消息中間件原有的主備成對部署的方案,主備之間是有嚴格的對應關係,而且在上下線發佈過程當中有嚴格的順序要求,這種部署模式在 Kubernetes 的體系下是並不提倡的。若依然採用以上老的架構方式,會致使實例控制的複雜性和不可控性,同時咱們也但願能更多的遵循 Kubernetes 的運維理念。
雲化後的 ECS 使用的是高速雲盤,底層將對數據作了多備份,所以數據的可用性獲得了保障。而且高速雲盤在性能上徹底知足 MQ 同步刷盤,所以,此時就能夠把以前的異步刷盤改成同步,保證消息寫入時的不丟失問題。雲原生模式下,全部的實例環境均是一致性的,依託容器技術和 Kubernetes 的技術,可實現任何實例掛掉(包含宕機引發的掛掉),都能自動自愈,快速恢復。
解決了數據的可靠性和服務的可用性後,整個雲原生化後的架構能夠變得更加簡單,只有 broker 的概念,再無主備之分。
上圖是 Kubernetes 上線後雙十一大促當天的發送 RT 統計,可見大促期間的發送 RT 較爲平穩,總體符合預期,雲原生化實踐完成了關鍵性的里程碑。
RocketMQ 至今已經連續七年 0 故障支持集團的雙十一大促。自從 RocketMQ 誕生以來,爲了可以徹底承載包括集團業務中臺交易消息等核心鏈路在內的各種關鍵業務,複用了原有的上層協議邏輯,使得各種業務方徹底無感知的切換到 RocketMQ 上,並同時充分享受了更爲穩定和強大的 RocketMQ 消息中間件的各種特性。
當前,申請訂閱業務中臺的核心交易消息的業務方一直都在不斷持續增長,而且隨着各種業務複雜度提高,業務方的消息訂閱配置也變得更加複雜繁瑣,從而使得交易集羣的進行過濾的計算邏輯也變得更爲複雜。這些業務方部分沿用舊的協議邏輯(Header過濾),部分使用 RocketMQ 特有的 SQL 過濾。
目前集團內部 RocketMQ 的大促機器成本絕大部分都是交易消息相關的集羣,在雙十一零點峯值期間,交易集羣的峯值和交易峯值成正比,疊加每一年新增的複雜訂閱帶來了額外 CPU 過濾計算邏輯,交易集羣都是大促中機器成本增加最大的地方。
因爲歷史緣由,大部分的業務方主要仍是使用 Header 過濾,內部實現實際上是 aviator 表達式( https://github.com/killme2008/aviatorscript )。仔細觀察交易消息集羣的業務方過濾表達式,能夠發現絕大部分都指定相似 MessageType == xxxx 這樣的條件。翻看 aviator 的源碼能夠發現這樣的條件最終會調用 Java 的字符串比較 String.compareTo()。
因爲交易消息包括大量不一樣業務的 MessageType,光是有記錄的起碼有幾千個,隨着交易業務流程複雜化,MessageType 的增加更是繁多。隨着交易峯值的提升,交易消息峯值正比增加,疊加這部分更加複雜的過濾,持續增加的未來,交易集羣的成本很可能和交易峯值指數增加,所以決心對這部分進行優化。
原有的過濾流程以下,每一個交易消息須要逐個匹配不一樣 group 的訂閱關係表達式,若是符合表達式,則選取對應的 group 的機器進行投遞。以下圖所示:
對此流程進行優化的思路須要必定的靈感,在這裏藉助數據庫索引的思路:原有流程能夠把全部訂閱方的過濾表達式看做數據庫的記錄,每次消息過濾就至關於一個帶有特定條件的數據庫查詢,把全部匹配查詢(消息)的記錄(過濾表達式)選取出來做爲結果。爲了加快查詢結果,能夠選擇 MessageType 做爲一個索引字段進行索引化,每次查詢變爲先匹配 MessageType 主索引,而後把匹配上主索引的記錄再進行其它條件(以下圖的 sellerId 和 testA )匹配,優化流程以下圖所示:
以上優化流程肯定後,要關注的技術點有兩個:
技術點 1:如何抽取每一個表達式中的 MessageType 字段?
對於技術點 1 ,須要針對 aviator 的編譯流程進行 hook ,深刻 aviator 源碼後,能夠發現 aviator 的編譯是典型的 Recursive descent :http://en.wikipedia.org/wiki/Recursive_descent_parser,同時須要考慮到提取後父表達式的短路問題。
在編譯過程當中針對 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
提取了 messageType ,有兩種狀況:
這樣就完成 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 計算便可過濾掉絕大部分不適合的表達式,大大提升了過濾效率。
該優化最主要下降了 CPU 計算邏輯,根據優化先後的性能狀況對比,咱們發現不一樣的交易集羣中的訂閱方訂閱表達式複雜度越高,優化效果越好,這個是符合咱們的預期的,其中最大的 CPU 優化有 32% 的提高,大大下降了本年度 RocketMQ 的部署機器成本。
RocketMQ 的 PULL 消費對於機器異常 hang 時並不十分友好。若是遇到客戶端機器hang住,但處於半死不活的狀態,與 broker 的心跳沒有斷掉的時候,客戶端 rebalance 依然會分配消費隊列到 hang 機器上,而且 hang 機器消費速度很慢甚至沒法消費的時候,這樣會致使消費堆積。另外相似還有服務端 Broker 發佈時,也會因爲客戶端屢次 rebalance 致使消費延遲影響等沒法避免的問題。以下圖所示:
當 Pull Client 2 發生 hang 機器的時候,它所分配到的三個 Broker 上的 Q2 都出現嚴重的紅色堆積。對於此,咱們增長了一種新的消費模型——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 再標記消息消費結果,若是超時沒響應或者消費失敗,再會進行重試。
POP 消費的架構圖如上圖所示。Broker 對於每次 POP 的請求,都會有如下三個操做:
對應的隊列進行加鎖,而後從 store 層獲取該隊列的消息。
而後寫入 CK 消息,代表獲取的消息要被 POP 消費。
CK 消息其實是記錄了 POP 消息具體位點的定時消息,當客戶端超時沒響應的時候,CK 消息就會從新被 broker 消費,而後把 CK 消息的位點的消息寫入重試隊列。若是 broker 收到客戶端的消費結果的 Ack ,刪除對應的 CK 消息,而後根據具體結果判斷是否須要重試。
從總體流程可見,POP 消費並不須要 reblance ,能夠避免 rebalance 帶來的消費延時,同時客戶端能夠消費 broker 的全部隊列,這樣就能夠避免機器 hang 而致使堆積的問題。