Apache RocketMQ 的 Service Mesh 開源之旅
作者 | 淩楚 阿裡巴巴開發工程師
導讀:自 19 年底開始,支援 Apache RocketMQ 的 Network Filter 曆時 4 個月的 Code Review(Pull Request),于本月正式合入 CNCF Envoy 官方社群(RocketMQ Proxy Filter 官方文檔),這使得 RocketMQ 成為繼 Dubbo 之後,國内第二個成功進入 Service Mesh 官方社群的中間件産品。
Service Mesh 下的消息收發
主要流程如下圖:
圖 1
簡述一下 Service Mesh 下 RocketMQ 消息的發送與消費過程:
Pilot 擷取到 Topic 的路由資訊并通過 xDS 的形式下發給資料平面/Envoy ,Envoy 會代理 SDK 向 Broker/Nameserver 發送的所有的網絡請求;
發送時,Envoy 通過 request code 判斷出請求為發送,并根據 topic 和 request code 選出對應的 CDS,然後通過 Envoy 提供的負載均衡政策選出對應的 Broker 并發送,這裡會使用資料平面的 subset 機制來確定選出的 Broker 是可寫的;
消費時,Envoy 通過 request code 判斷出請求為消費,并根據 topic 和 request code 選出對應的 CDS,然後和發送一樣選出對應的 Broker 進行消費(與發送類似,這裡也會使用 subset 來確定選出的 Broker 是可讀的),并記錄相應的中繼資料,當消息消費 SDK 發出 ACK 請求時會取出相應的中繼資料資訊進行比對,再通過路由來準确将 ACK 請求發往上次消費時所使用的 Broker。
RocketMQ Mesh 化所遭遇的難題
Service Mesh 常常被稱為下一代微服務,這一方面揭示了在早期 Mesh 化浪潮中微服務是絕對的主力軍,另一方面,微服務的 Mesh 化也相對更加便利,而随着消息隊列和一些資料庫産品也逐漸走向 Service Mesh,各個産品在這個過程中也會有各自的問題亟需解決,RocketMQ 也沒有例外。
有狀态的網絡模型
RocketMQ 的網絡模型比 RPC 更加複雜,是一套有狀态的網絡互動,這主要展現在兩點:
RocketMQ 目前的網絡調用高度依賴于有狀态的 IP;
原生 SDK 中消費時的負載均衡使得每個消費者的狀态不可以被忽略。
對于前者,使得現有的 SDK 完全無法使用分區順序消息,因為發送請求和消費請求 RPC 的内容中并不包含 IP/(BrokerName + BrokerId) 等資訊,導緻使用了 Mesh 之後的 SDK 不能保證發送和消費的 Queue 在同一台 Broker 上,即 Broker 資訊本身在 Mesh 化的過程中被抹除了。當然這一點,對于隻有一台 Broker 的全局順序消息而言是不存在的,因為資料平面在負載均衡的時候并沒有其他 Broker 的選擇,是以在路由層面上,全局順序消息是不存在問題的。
對于後者,RocketMQ 的 Pull/Push Consumer 中 Queue 是負載均衡的基本機關,原生的 Consumer 中其實是要感覺與自己處于同一 ConsumerGroup 下消費同一 Topic 的 Consumer 數目的,每個 Consumer 根據自己的位置來選擇相應的 Queue 來進行消費,這些 Queue 在一個 Topic-ConsumerGroup 映射下是被每個 Consumer 獨占的,而這一點在現有的資料平面是很難實作的,而且,現有資料平面的負載均衡沒法做到 Queue 粒度,這使得 RocketMQ 中的負載均衡政策已經不再适用于 Service Mesh 體系下。
此時我們将目光投向了 RocketMQ 為支援 HTTP 而開發的 Pop 消費接口,在 Pop 接口下,每個 Queue 可以不再是被目前 Topic-ConsumerGroup 的 Consumer 獨占的,不同的消費者可以同時消費一個 Queue 裡的資料,這為我們使用 Envoy 中原生的負載均衡政策提供了可能。
圖 2
圖 2 右側即為 Service Mesh 中 Pop Consumer 的消費情況,在 Envoy 中我們會忽略掉 SDK 傳來的 Queue 資訊。
彈内海量的 Topic 路由資訊
在集團内部,Nameserver 中儲存着上 GB 的 Topic 路由資訊,在 Mesh 中,我們将這部分抽象成 CDS,這使得對于無法預先知道應用所使用的 Topic 的情形而言,控制平面隻能全量推送 CDS,這無疑會給控制平面帶來巨大的穩定性壓力。
在 Envoy 更早期,是完全的全量推送,在資料平面剛啟動時,控制平面會下發全量的 xDS 資訊,之後控制平面則可以主動控制資料的下發頻率,但是無疑下發的資料依舊是全量的。後續 Envoy 支援了部分的 delta xDS API,即可以下發增量的 xDS 資料給資料平面,這當然使得對于已有的 sidecar,新下發的資料量大大降低,但是 sidecar 中擁有的 xDS 資料依然是全量的,對應到 RocketMQ ,即全量的 CDS 資訊都放在記憶體中,這是我們不可接受的。于是我們希望能夠有 on-demand CDS 的方式使得 sidecar 可以僅僅擷取自己想要的 CDS 。而此時正好 Envoy 支援了 delta CDS,并僅支援了這一種 delta xDS。其實此時擁有 delta CDS 的 xDS 協定本身已經提供了 on-demand CDS 的能力,但是無論是控制平面還是資料平面并沒有暴露這種能力,于是在這裡對 Envoy 進行了修改并暴露了相關接口使得資料平面可以主動向控制平面發起對指定 CDS 的請求,并基于 delta gRPC 的方式實作了一個簡單的控制平面。Envoy 會主動發起對指定 CDS 資源的請求,并提供了相應的回調接口供資源傳回時進行調用。
對于 on-demand CDS 的叙述對應到 RocketMQ 的流程中是這樣的,當 GetTopicRoute 或者 SendMessage 的請求到達 Envoy 時,Envoy 會 hang 住這個流程并發起向控制平面中相應 CDS 資源的請求并直到資源傳回後重新開機這個流程。
關于 on-demand CDS 的修改,之前還向社群發起了 Pull Request ,現在看來當時的想法還是太不成熟了。原因是我們這樣的做法完全忽略了 RDS 的存在,而将 CDS 和 Topic 實作了強綁定,甚至名稱也一模一樣,關于這一點,社群的 Senior Maintainer @htuch 對我們的想法進行了反駁,大意就是實際上的 CDS 資源名可能帶上了負載均衡方式,inbound/outbound 等各種 prefix 和 suffix,不能直接等同于 Topic 名,更重要的是社群賦予 CDS 本身的定義是脫離于業務的,而我們這樣的做法過于 tricky ,是與社群的初衷背道而馳的。
是以我們就需要加上 RDS 來進行抽象,RDS 通過 topic 和其他資訊來定位到具體所需要的 CDS 名,由于作為資料平面,無法預先在代碼層面就知道所需要找的 CDS 名,那麼如此一來,通過 CDS 名來做 on-demand CDS 就更無從談起了,是以從這一點出發隻能接受全量方案,不過好在這并不會影響代碼貢獻給社群。
route_config:
name: default_route
routes:
- match:
exact: meshtopic:
headers:
-
name: code
exact_match: 105
上面可以看到對于 topic 名為 mesh 的請求會被 RDS 路由到 foo-v145-acme-tau-beta-lambda 這個 CDS 上,事先我們隻知道 topic 名,無法知道被比對到的 CDS 資源名。route: cluster: foo-v145-acme-tau-beta-lambda
-
如今站在更高的視角,發現這個錯誤很簡單,但是其實這個問題我們直到後續 code review 時才及時糾正,确實可以更早就做得更好。
不過從目前社群的動态來看,on-demand xDS 或許已經是一個 roadmap,起碼目前 xDS 已經全系支援 delta ,VHDS 更是首度支援了 on-demand 的特性。
Mesh 為 RocketMQ 帶來了什麼?
A service mesh is a dedicated infrastructure layer for handling service-to-service communication. It’s responsible for the reliable delivery of requests through the complex topology of services that comprise a modern, cloud native application. In practice, the service mesh is typically implemented as an array of lightweight network proxies that are deployed alongside application code, without the application needing to be aware.
這是 Service Mesh 這個詞的創造者 William Morgan 對其做出的定義,概括一下:作為網絡代理,并對使用者透明,承擔作為基礎設施的職責。
圖 3
這裡的職責在 RocketMQ 中包括服務發現、負載均衡、流量監控等職責,使得調用方和被代理方的職責大大降低了。
當然目前的 RocketMQ Filter 為了保證相容性做出了很多讓步,比如為了保證 SDK 可以成功擷取到路由,将路由資訊聚合包裝成了 TopicRouteData 傳回給了 SDK ,但是在理想情況下,SDK 本身已經不需要關心路由了,純為 Mesh 情景設計的 SDK 是更加精簡的,不再會有消費側 Rebalance,發送和消費的服務發現,甚至在未來像消息體壓縮和 schema 校驗這些功能 SDK 和 Broker 或許都可以不用再關心,來了就發送/消費,發送/消費完就走或許才是 RocketMQ Mesh 的終極形态。
圖 4
What's Next ?
目前 RocketMQ Filter 具備了普通消息的發送和 Pop 消費能力,但是如果想要具備更加完整的産品形态,功能上還有一些需要補充:
支援 Pull 請求:現在 Envoy Proxy 隻接收 Pop 類型的消費請求,之後會考慮支援普通的 Pull 類型,Envoy 會将 Pull 請求轉換成 Pop 請求,進而做到讓使用者無感覺;
支援全局順序消息:目前在 Mesh 體系下,雖然全局順序消息的路由不存在問題,但是如果多個 Consumer 同時消費全局順序消息,其中一個消費者突然下線導緻消息沒有 ACK 而會導緻另一個消費者的消息産生亂序,這一點需要在 Envoy 中進行保證;
Broker 側的 Proxy:對 Broker 側的請求也進行代理和排程。
蜿蜒曲折的社群曆程
起初,RocketMQ Filter 的初次 Pull Request 就包含了目前幾乎全部的功能,導緻了一個超過 8K 行的超大 PR,感謝@天千 在 Code Review 中所做的工作,非常專業,幫助了我們更快地合入社群。
另外,Envoy 社群的 CI 實在太嚴格了,嚴格要求 97% 以上的單測行覆寫率,Bazel 源碼級依賴,純靜态連結,本身無 cache 編譯 24 邏輯核心 CPU 和 load 均打滿至少半個小時才能編完,社群的各種 CI 跑完一次則少說兩三個小時,多則六七個小時,并對新送出的代碼有着極其嚴苛的文法和 format 要求,這使得在 PR 中修改一小部分代碼就可能帶來大量的單測變動和 format 需求,不過好的是單測可以很友善地幫助我們發現一些記憶體 case 。客觀上來說,官方社群以這麼高的标準來要求 contributors 确實可以很大程度上控制住代碼品質,我們在補全單測的過程中,還是發現并解決了不少自身的問題,總得來說還是有一定必要的,畢竟對于 C++ 代碼而言,一旦生産環境出問題,調試和追蹤起來會困難得多。
最後,RocketMQ Filter 的代碼由我和@叔田 共同完成,對于一個沒什麼開源經驗的我來說,為這樣的熱門社群貢獻代碼是一次非常寶貴的經曆,同時也感謝叔田在此過程中給予的幫助和建議。
相關連結
Official docs for RocketMQ filter
Pull request of RocketMQ filter
RocketMQ filter's issue
On-demand CDS pull request for Envoy
First version of RocketMQ filter's proposal
阿裡巴巴雲原生中間件團隊招人啦
這裡有足夠多的業務場景、足夠大的消息生态、足夠深的分布式技術等着大家前來探索,如果你滿足:
至少精通一種程式設計語言,Java 或 C++;
深入了解分布式存儲理論,微服務優化實踐;
計算機理論基礎紮實,例如對作業系統原理、TCP/IP 等有比較深入的了解;
具有獨立設計一款生産環境高可用高可靠的中間件能力,例如 RocketMQ;
熟悉高并發、分布式通信、存儲、開源中間件軟體等相關技術者更佳。
歡迎加入并參與新一代雲原生中間件建設!履歷送出位址:[email protected]。
課程推薦
為了更多開發者能夠享受到 Serverless 帶來的紅利,這一次,我們集結了 10+ 位阿裡巴巴 Serverless 領域技術專家,打造出最适合開發者入門的 Serverless 公開課,讓你即學即用,輕松擁抱雲計算的新範式——Serverless。
點選即可免費觀看課程:
https://developer.aliyun.com/learning/roadmap/serverless原文位址
https://my.oschina.net/u/3874284/blog/4279489