全面升級 —— Apache RocketMQ 5.0 SDK 的新面貌

阿里雲開發者 發表於 2021-11-18
簡介:長久以來,RocketMQ 易於部署、高效能、高可用的架構,支撐了數十年來集團內外海量的業務場景。時至今日,為了迎接如今雲原生時代的新挑戰,我們重磅推出了 RocketMQ 5.0 新架構。

作者 | 凌楚

引言

長久以來,RocketMQ 易於部署、高效能、高可用的架構,支撐了數十年來集團內外海量的業務場景。時至今日,為了迎接如今雲原生時代的新挑戰,我們重磅推出了 RocketMQ 5.0 新架構。

在 5.0 新架構中,我們更新了整個 RocketMQ 的網路拓撲模型,著眼於將更上層的業務邏輯從 broker 中剝離到無狀態的 proxy ,這樣獨立的計算節點可以無損地承擔日後的升級釋出任務,與此同時將 broker 解放出來承擔純粹的儲存任務,為未來打造更強的訊息儲存引擎做好鋪墊。通訊層方面,出於標準化,多語言的考慮我們摒棄了 RocketMQ 使用多年的 RemotingCommand 協議,採用了 gRPC 來實現客戶端與服務端之間的通訊邏輯。

針對於使用者側,我們希望儘可能少的叨擾客戶進行升級,維持邏輯輕量,易於維護,可觀測性良好,能夠可以達到“一次性把事情做對”。

 title=

目前,保證了介面完全相容的,基於 RocketMQ 5.0 的商業化版本 Java SDK 已經在公有云 release 完成,開源版本也即將 release。SDK 將同時支援雲上 proxy 架構的雲上版本和開源版本的 Broker。下面將展開敘述 RocketMQ 5.0 新架構下的 SDK 做了哪些迭代與演進。

全面非同步化

1、非同步的初衷

由於涉及諸多的網路 IO,因此 RocketMQ 對訊息傳送開放了同步和非同步兩套 API 提供給使用者使用。舊有架構從 API 針對於同步和非同步維護了兩套類似的業務邏輯,非常不利於迭代。考慮到這一點,此次新架構 SDK 希望在底層就可以將它們統一起來。

 title=

以訊息傳送為例,一個完整的訊息傳送鏈路包括獲取:

  1. 獲取 topic 對應的路由;
  2. 根據路由選擇對應的分割槽;
  3. 傳送訊息到指定的分割槽,如果傳送到該分割槽失敗,則對下一個分割槽進行傳送重試直到達到最大重試次數;如果傳送成功,則返回傳送結果。

其中從遠端獲取 topic 對應的路由是一個重 IO 操作,而傳送訊息本身也是一個重 IO 操作。在以往的傳送實現中,即使是非同步傳送,對於路由的獲取也是同步的,路由的獲取本身並沒有計入使用者的傳送耗時中,使用者本身是可以自主設定訊息傳送的超時時間的,而由於本身訊息的傳送是同步的,無法做到超時時間的精準控制,而在使用非同步 Future 之後,可以非常方便地通過控制 Future 的超時時間來做到。

2、非同步統一所有實現

本質上 RocketMQ 裡所有的重 IO 操作都可以通過非同步來進行統一。得益於 gRPC 本身提供了基於 Future 的 stub,我們將網路層的 Future 一層層串聯到最終的業務層。當使用者需要同步 API 時,則進行同步等待;當使用者需要非同步 API 時,則在最外層的 Future 新增回撥進行監聽。

實際上基於 Future 設計的思想是貫穿整個客戶端實現的。譬如,訊息消費也是通過唯一的基於 Future 的實現來完成的:

/** * Deliver message to listener. * * @param messageExtList message list to consume * @param delay message consumption delay time * @param timeUnit delay time unit. * @return future which contains the consume status. */public ListenableFuture<ConsumeStatus> consume(List<MessageExt> messageExtList, long delay, TimeUnit timeUnit) {    // Omit the implement}

針對於順序訊息消費失敗這種需要本地 suspend 一段時間重新投遞的情況,消費介面增加了延時引數。然而無論是普通訊息還是順序訊息,都只會返回含有消費狀態的 Future 。上層再針對含有消費狀態的 Future 來進行訊息的 ACK/NACK 。特別地,針對於服務端向客戶端投遞特定訊息進行消費驗證的場景,也是呼叫當前 Future 介面,再對消費結果進行包裝向服務端響應消費結果。

RocketMQ 本身的傳送和消費過程中充斥著大量的非同步邏輯,使用 Future 使得大量的介面實現得到了精簡和統一。尤其在我們的基於 gRPC 新架構協議的 IDL 中,為了保持簡單全部都是使用 unary rpc(非流式),使得我們全部可以使用 gRPC 的 Future stub 來完成通訊請求。

可觀測性增強

 title=

上面這張圖來自於 Peter Bourgon 2017 年的一篇重要博文,系統且詳細地闡述了 metrics、tracing 和 logging 三者之間的特徵與定義,以及他們之間的關聯。

  • Metrics:具體聚合同類資料的統計資訊,用於預警和監控。
  • Tracing:關聯和分析同一個呼叫鏈上的後設資料,判斷具體呼叫鏈上的異常和阻塞行為。
  • Logging:記錄離散的事件來分析程式的行為。

雲原生時代,可觀測性是雲產品的核心競爭力之一。因而可觀測性增強的基調是整個新架構開發之初就已經確定的。舊有架構客戶端邏輯複雜的同時,可觀測性的缺失也導致我們在面臨客戶工單時更加缺乏足夠直觀簡便的手段,因此新架構中我們圍繞 Tracing、Logging 和 Metrics 這三個重要方面進行了全方位的可觀察性提升。

1、全鏈路 Tracing

Tracing 體現在訊息中介軟體中,最基本的,即對每條訊息本身的傳送、拉取、消費、ACK/NACK、事務提交、儲存、刪除等過程進行全生命週期的監控記錄,在 RocketMQ 中最基本的實現就是訊息軌跡。

舊有的訊息軌跡採用私有協議進行編解碼,對於訊息生命週期的觀測也僅限於傳送、消費和事務相關等階段。沒有和開源規範進行統一,也不具備訊息自身的軌跡和使用者鏈路的 trace 共享上下文的能力。

新的實現中,擁抱了最新的 CNCF OpenTelemetry 社群協議規範,在客戶端中嵌入了一個 OTLP exporter 將 tracing 資料批量傳送至 proxy,proxy 側的方案則比較多樣了,既可以本身作為一個 collector 將資料進行整合,也可以轉發至其他的 collector,proxy 側也會有相對應的 tracing 資料,會和客戶端上報來的 tracing 資料合併進行處理。

由於採用開源標準的 OTLP exporter 和協議,使得使用者自己定義對應的 collector 地址成為可能。在商業版本中我們將使用者客戶端的 tracing 資料和服務端的 tracing 資料進行收集整合後進行託管儲存,開源版本中使用者也可以自定義自己的 collector 地址將 tracing 資料上報到自己的平臺進行分析和處理。

針對於整個訊息的生命週期,我們重新設計了所有的 span 拓撲模型。以最簡單的訊息傳送、接受、消費和 ACK/NACK 過程為例:

 title=

其中:

  • Prod :Produce, 表示訊息的傳送,即起始時間為訊息開始傳送,結束時間為收到訊息傳送結果(訊息內部重試會單獨進行記錄一條 span);
  • Recv :Receive,表示訊息的接收,即起始時間為客戶端發起接受訊息的請求,結束時間為收到對應的響應;
  • Await :表示訊息到達客戶端直到訊息開始被消費;
  • Proc :Process,表示訊息的消費過程;
  • Ack/Nack 表示訊息被 Ack/Nack 的過程。

這個過程,各個 Span 之間的關係如下:

 title=

商業版的 ONS 在管控側也對新版本 Trace 進行了支援,針對於使用者關心的訊息生產耗時、具體消費狀況、消費耗時、等待耗時,消費次數等給出了更加詳盡的展示。

 title=

通過 SLS 的 trace 服務觀察生產者和消費者 span 的拓撲關係(link 關係沒有進行展示,因此圖中沒有 receive 相關的 span):

 title=

OpenTelemetry 關於 messaging span 相關的 specification 也在社群不斷迭代,這涉及到具體的 tracing 拓撲,span 屬性定義(即 attribute semantic conventions)等等。我們也在第一時間將 RocketMQ 相關的內容向社群 OpenTelemetry specification 發起了初步的 Pull Request,並得到了社群的收錄和肯定。也得益於 OpenTelemetry specification 詳盡和規範的定義,我們在 tracing 資料增加了包括且不限於程式執行時、作業系統環境和版本等(即 resource semantic conventions)大量有利於線上問題發現和排查的資訊。

關於 tracing context propagation ,我們採用了 W3C 的標準對 trace context 進行序列化和反序列化在客戶端和服務端之間來回傳遞,在下個版本中也會提供讓使用者自定義 trace context 的介面,使得使用者可以很方便地關聯 RocketMQ 和自己的 tracing 資料。

新架構中我們針對於訊息生命週期的不同節點,暴露了很多 hook point ,tracing 的邏輯也基於這些 hook point 進行實現,因此也能保持相對獨立。在完整的新架構推向開源之後,整個 tracing 的相關邏輯也會被抽取成專門的 instrumentation library 貢獻給 openTelemetry 社群。

2、準確多樣的 Metrics

Tracing 更多地是從呼叫鏈的角度去觀察訊息的走向,更多的時候對於有共性的資料,我們希望可以有聚合好的 Metrics  和對應 dashboard 可以從更加巨集觀的角度來進行觀測。如果說 tracing 可以幫助更好更快地發現問題和定位問題,那麼 Metrics 則提供了重要的多維觀察和預警手段。

在收集到足夠多的 tracing 資料之後,服務端會對這些資料進行二次聚合,計算得出使用者傳送、等待以及消費時間等資料的百分位數,對很多毛刺問題能很好地做出判斷。

3、規範化的 Logging

我們在開發實踐中嚴格地按照 Trace、Debug、Info、Warn、Error 的級別進行日誌內容的定義,譬如 Trace 級別就會對每個 RPC 請求和響應,每條訊息從進入客戶端到進行記錄,Error 級別的日誌一旦被列印,必然是值得我們和客戶關注的。在去除大量冗餘資訊的同時,關鍵節點,譬如負載均衡,傳送失敗重試等關鍵鏈路也補全了大量資訊,單行日誌的資訊密度大大增加。

另外,關於日誌模組的實現,RocketMQ 原本是自行開發的,相比較於 logback,log4j2 等外部實現而言,功能相對單一,二次開發成本也相對較高。選型時沒有使用 logback 根本上其實只是想要避免與使用者日誌模組衝突的問題,在調研了諸多方案之後,選擇了 shade logback 的方式進行了替換。這裡的 shade 不僅僅只是替換了包名和座標,同時也修改了 logback 官方的日誌配置檔名和諸多內部環境引數。

比如預設配置檔案:

<span class="lake-fontsize-1515">庫</span><span class="lake-fontsize-1515">預設配置檔案</span>
<span class="lake-fontsize-1515">standard logback</span><span class="lake-fontsize-1515">logback.groovy/logback-test.xml/logback.xml</span>
<span class="lake-fontsize-1515">logback for rocketmq</span><span class="lake-fontsize-1515">rocketmq.logback.groovy/rocketmq.logback-test.xml/rocketmq.logback.xml</span>
如果使用者在引用 rocketmq 的同時自己也引入了 logback ,完整的配置檔案和環境引數的隔離保證了兩者是相互獨立的。特別的,由於新架構 SDK 中引入了 gRPC,我們將 gRPC 基於 JUL 的日誌橋接到了 slf4j ,並通過 logback 進行輸出。 ` // redirect JUL logging to slf4j.// see https://github.com/grpc/grpc-...();SLF4JBridgeHandler.install(); ` # 消費模型的更新 RocketMQ 舊有架構的消費模型是非常複雜的。topic 中的訊息本身按照 MessageQueue 進行儲存,消費時客戶端按 MessageQueue 對訊息進行拉取、快取和投遞。 ProcessQueue 與 RocketMQ 中的 MessageQueue 一一對應,也基本上是客戶端消費端邏輯中最為複雜的結構之一。在舊架構的客戶端中,拉取到訊息之後會先將訊息快取到 ProcessQueue 中,當需要消費時,會從 ProcessQueue 中取出對應的訊息進行消費,當消費成功之後再將訊息從 ProcessQueue 中 remove 走。其中重試訊息的傳送,位點的更新在這個過程中穿插。 ## 1、設計思路 在新客戶端中, pop 消費模式的引入使得單獨處理重試訊息和位點更新的邏輯被去除。使用者的消費行為變為 1. 拉取訊息 2. 消費訊息 3. ACK/NACK 訊息到遠端 因為拉取到的訊息在客戶端記憶體是會先進行快取,因此還要在消費和拉取的過程中計算訊息快取的大小來對程式進行保護,因此新客戶端中每個 ProcessQueue 分別維護了兩個佇列:cached messages 和 pending messages 。訊息在到達客戶端之後會先放在 cached messages 裡,準備消費時會從 cached messages 移動到 pending messages 中,當訊息消費結束並被 Ack 之後則會從 pending messages 中移除。  title= 新架構的客戶端精簡了 ProcessQueue 的實現,封裝性也做到了更好。對於消費者而言,最為核心的介面其實只有四個。 ` public interface ProcessQueue { / Try to take messages from cache except FIFO messages. @param batchMaxSize max batch size to take messages. @return messages which have been taken. / List<MessageExt> tryTakeMessages(int batchMaxSize); / Erase messages which haven been taken except FIFO messages. @param messageExtList messages to erase. @param status consume status. / void eraseMessages(List<MessageExt> messageExtList, ConsumeStatus status); / Try to take FIFO message from cache. @return message which has been taken, or {@link Optional#absent()} if no message. / Optional<MessageExt> tryTakeFifoMessage(); / Erase FIFO message which has been taken. @param messageExt message to erase. @param status consume status. */ void eraseFifoMessage(MessageExt messageExt, ConsumeStatus status);} ` 對於普通消費者(非順序消費)而言,ProcessQueue#tryTakeMessages 將從 Cached messages 中取出訊息(取出之後訊息會自動從 Cached messages 移動至Pending messages),當訊息消費結束之後再攜帶好對應的消費結果去呼叫ProcessQueue#eraseMessages ,對於順序消費者而言,唯一不同的是對應的方法呼叫替換成ProcessQueue#tryTakeFifoMessage 和ProcessQueue#eraseFifoMessage 。 而 ProcessQueue#tryTakeMessages 和 ProcessQueue#tryTakeFifoMessage 本身已經包含了消費限流和順序消費時為了保證順序對佇列上鎖的邏輯,即做到了:一旦 ProcessQueue#tryTakeMessages/ProcessQueue#tryTakeFifoMessage 可以取到訊息,那麼訊息一定是滿足被消費條件的。當消費者獲取到消費結果之後,再帶上消費結果執行ProcessQueue#eraseMessage 和ProcessQueue#eraseFifoMessage ,erase 的過程會完成訊息的 ACK/NACK 和順序消費時佇列解鎖的邏輯。  title= 簡化之後,上層的消費邏輯基本上只需要負責往消費執行緒中提交消費任務即可了,任何說得上是 'Process' 的邏輯都在新的 ProcessQueue 完成了閉環。 # 相容性與質量保障 整個新架構的 SDK 依賴了protocol buffers, gRPC-java, openTelemetry-java 等諸多類庫。在簡化 RocketMQ 本身程式碼的同時也帶來了一些相容性問題。RocketMQ 本身保持著對 Java 1.6 的相容性,然而: * gRPC-java 在 2018 年的 1.15 版本之後不再支援 Java1.6; * openTelemetry-java 只支援 Java8 及以上版本。 在此期間,我們也調研了 AWS、Azure 等友商相關 SDK 的現狀,發現放棄對 Java 1.6 的支援已經是業內標準做法。但囿於老客戶固守 Java 1.6 的情況,我們也進行了一些改造: * 對 protocol buffers 的程式碼進行了 Java 1.6 的等義替換,並通過了 protocol buffers 所有的單測; * 對 gRPC 的程式碼進行了 Java 1.6 的等義替換,並通過了 gRPC 所有的單測; * 對於 openTelemetry ,在進行等義替換的同時進行了大量的功能性測試; 單測方面,目前客戶端保證了 75% 以上的行覆蓋率,不過相比較優秀的開源專案還有比較長的距離,這一點我們也會在後續的迭代中不斷完善。 # 最後 RocketMQ 5.0 是自開源以來架構升級最大的一次版本,具體實現過程還有非常多的細節沒有披露,礙於篇幅無法面面俱到,後續開源過程中也歡迎大家在社群中提出更多更寶貴的意見。 ## 相關連結 * Pull Requesthttps://github.com/open-telemetry/opentelemetry-specification/pull/1904 * Metrics, Tracing, and Logginghttps://peter.bourgon.org/blog/2017/02/21/metrics-tracing-and-logging.html * Apache rocketmq apishttps://github.com/apache/rocketmq-apis * OpenTelemetry specificationhttps://github.com/open-telemetry/opentelemetry-specification 阿里巴巴雲原生訊息中介軟體團隊招聘中,強烈歡迎大家自薦和推薦! 有意者請聯絡: @凌楚 ([email protected])  @塵央 ([email protected]) 點選下方連結,檢視更多招聘詳情! https://job.alibaba.com/zhaopin/position\_detail.htm?spm=a2obv.11410903.0.0.674944f6oxzDCj&positionId=134677 > 版權宣告:本文內容由阿里雲實名註冊使用者自發貢獻,版權歸原作者所有,阿里雲開發者社群不擁有其著作權,亦不承擔相應法律責任。具體規則請檢視《阿里雲開發者社群使用者服務協議》和《阿里雲開發者社群智慧財產權保護指引》。如果您發現本社群中有涉嫌抄襲的內容,填寫侵權投訴表單進行舉報,一經查實,本社群將立刻刪除涉嫌侵權內容。

相關文章