Apache RocketMQ 的 Service Mesh 开源之旅
作者 | 凌楚 阿里巴巴開發(fā)工程師
導讀:自 19 年底開始,支持 Apache RocketMQ 的 Network Filter 歷時 4 個月的 Code Review(Pull Request),于本月正式合入 CNCF Envoy 官方社區(qū)(RocketMQ Proxy Filter 官方文檔),這使得 RocketMQ 成為繼 Dubbo 之后,國內(nèi)第二個成功進入 Service Mesh 官方社區(qū)的中間件產(chǎn)品。
Service Mesh 下的消息收發(fā)
主要流程如下圖:
圖 1簡述一下 Service Mesh 下 RocketMQ 消息的發(fā)送與消費過程:
- Pilot 獲取到 Topic 的路由信息并通過 xDS 的形式下發(fā)給數(shù)據(jù)平面/Envoy ,Envoy 會代理 SDK 向 Broker/Nameserver 發(fā)送的所有的網(wǎng)絡請求;
- 發(fā)送時,Envoy 通過 request code 判斷出請求為發(fā)送,并根據(jù) topic 和 request code 選出對應的 CDS,然后通過 Envoy 提供的負載均衡策略選出對應的 Broker 并發(fā)送,這里會使用數(shù)據(jù)平面的 subset 機制來確保選出的 Broker 是可寫的;
- 消費時,Envoy 通過 request code 判斷出請求為消費,并根據(jù) topic 和 request code 選出對應的 CDS,然后和發(fā)送一樣選出對應的 Broker 進行消費(與發(fā)送類似,這里也會使用 subset 來確保選出的 Broker 是可讀的),并記錄相應的元數(shù)據(jù),當消息消費 SDK 發(fā)出 ACK 請求時會取出相應的元數(shù)據(jù)信息進行比對,再通過路由來準確將 ACK 請求發(fā)往上次消費時所使用的 Broker。
RocketMQ Mesh 化所遭遇的難題
Service Mesh 常常被稱為下一代微服務,這一方面揭示了在早期 Mesh 化浪潮中微服務是絕對的主力軍,另一方面,微服務的 Mesh 化也相對更加便利,而隨著消息隊列和一些數(shù)據(jù)庫產(chǎn)品也逐漸走向 Service Mesh,各個產(chǎn)品在這個過程中也會有各自的問題亟需解決,RocketMQ 也沒有例外。
有狀態(tài)的網(wǎng)絡模型
RocketMQ 的網(wǎng)絡模型比 RPC 更加復雜,是一套有狀態(tài)的網(wǎng)絡交互,這主要體現(xiàn)在兩點:
- RocketMQ 目前的網(wǎng)絡調(diào)用高度依賴于有狀態(tài)的 IP;
- 原生 SDK 中消費時的負載均衡使得每個消費者的狀態(tài)不可以被忽略。
對于前者,使得現(xiàn)有的 SDK 完全無法使用分區(qū)順序消息,因為發(fā)送請求和消費請求 RPC 的內(nèi)容中并不包含 IP/(BrokerName + BrokerId) 等信息,導致使用了 Mesh 之后的 SDK 不能保證發(fā)送和消費的 Queue 在同一臺 Broker 上,即 Broker 信息本身在 Mesh 化的過程中被抹除了。當然這一點,對于只有一臺 Broker 的全局順序消息而言是不存在的,因為數(shù)據(jù)平面在負載均衡的時候并沒有其他 Broker 的選擇,因此在路由層面上,全局順序消息是不存在問題的。
對于后者,RocketMQ 的 Pull/Push Consumer 中 Queue 是負載均衡的基本單位,原生的 Consumer 中其實是要感知與自己處于同一 ConsumerGroup 下消費同一 Topic 的 Consumer 數(shù)目的,每個 Consumer 根據(jù)自己的位置來選擇相應的 Queue 來進行消費,這些 Queue 在一個 Topic-ConsumerGroup 映射下是被每個 Consumer 獨占的,而這一點在現(xiàn)有的數(shù)據(jù)平面是很難實現(xiàn)的,而且,現(xiàn)有數(shù)據(jù)平面的負載均衡沒法做到 Queue 粒度,這使得 RocketMQ 中的負載均衡策略已經(jīng)不再適用于 Service Mesh 體系下。
此時我們將目光投向了 RocketMQ 為支持 HTTP 而開發(fā)的 Pop 消費接口,在 Pop 接口下,每個 Queue 可以不再是被當前 Topic-ConsumerGroup 的 Consumer 獨占的,不同的消費者可以同時消費一個 Queue 里的數(shù)據(jù),這為我們使用 Envoy 中原生的負載均衡策略提供了可能。
圖 2圖 2 右側(cè)即為 Service Mesh 中 Pop Consumer 的消費情況,在 Envoy 中我們會忽略掉 SDK 傳來的 Queue 信息。
彈內(nèi)海量的 Topic 路由信息
在集團內(nèi)部,Nameserver 中保存著上 GB 的 Topic 路由信息,在 Mesh 中,我們將這部分抽象成 CDS,這使得對于無法預先知道應用所使用的 Topic 的情形而言,控制平面只能全量推送 CDS,這無疑會給控制平面帶來巨大的穩(wěn)定性壓力。
在 Envoy 更早期,是完全的全量推送,在數(shù)據(jù)平面剛啟動時,控制平面會下發(fā)全量的 xDS 信息,之后控制平面則可以主動控制數(shù)據(jù)的下發(fā)頻率,但是無疑下發(fā)的數(shù)據(jù)依舊是全量的。后續(xù) Envoy 支持了部分的 delta xDS API,即可以下發(fā)增量的 xDS 數(shù)據(jù)給數(shù)據(jù)平面,這當然使得對于已有的 sidecar,新下發(fā)的數(shù)據(jù)量大大降低,但是 sidecar 中擁有的 xDS 數(shù)據(jù)依然是全量的,對應到 RocketMQ ,即全量的 CDS 信息都放在內(nèi)存中,這是我們不可接受的。于是我們希望能夠有 on-demand CDS 的方式使得 sidecar 可以僅僅獲取自己想要的 CDS 。而此時正好 Envoy 支持了 delta CDS,并僅支持了這一種 delta xDS。其實此時擁有 delta CDS 的 xDS 協(xié)議本身已經(jīng)提供了 on-demand CDS 的能力,但是無論是控制平面還是數(shù)據(jù)平面并沒有暴露這種能力,于是在這里對 Envoy 進行了修改并暴露了相關(guān)接口使得數(shù)據(jù)平面可以主動向控制平面發(fā)起對指定 CDS 的請求,并基于 delta gRPC 的方式實現(xiàn)了一個簡單的控制平面。Envoy 會主動發(fā)起對指定 CDS 資源的請求,并提供了相應的回調(diào)接口供資源返回時進行調(diào)用。
對于 on-demand CDS 的敘述對應到 RocketMQ 的流程中是這樣的,當 GetTopicRoute 或者 SendMessage 的請求到達 Envoy 時,Envoy 會 hang 住這個流程并發(fā)起向控制平面中相應 CDS 資源的請求并直到資源返回后重啟這個流程。
關(guān)于 on-demand CDS 的修改,之前還向社區(qū)發(fā)起了 Pull Request ,現(xiàn)在看來當時的想法還是太不成熟了。原因是我們這樣的做法完全忽略了 RDS 的存在,而將 CDS 和 Topic 實現(xiàn)了強綁定,甚至名稱也一模一樣,關(guān)于這一點,社區(qū)的 Senior Maintainer [@htuch ]() 對我們的想法進行了反駁,大意就是實際上的 CDS 資源名可能帶上了負載均衡方式,inbound/outbound 等各種 prefix 和 suffix,不能直接等同于 Topic 名,更重要的是社區(qū)賦予 CDS 本身的定義是脫離于業(yè)務的,而我們這樣的做法過于 tricky ,是與社區(qū)的初衷背道而馳的。
因此我們就需要加上 RDS 來進行抽象,RDS 通過 topic 和其他信息來定位到具體所需要的 CDS 名,由于作為數(shù)據(jù)平面,無法預先在代碼層面就知道所需要找的 CDS 名,那么如此一來,通過 CDS 名來做 on-demand CDS 就更無從談起了,因此從這一點出發(fā)只能接受全量方案,不過好在這并不會影響代碼貢獻給社區(qū)。
route_config:name: default_routeroutes:- match:topic:exact: meshheaders:- name: codeexact_match: 105route:cluster: foo-v145-acme-tau-beta-lambda上面可以看到對于 topic 名為 mesh 的請求會被 RDS 路由到 foo-v145-acme-tau-beta-lambda 這個 CDS 上,事先我們只知道 topic 名,無法知道被匹配到的 CDS 資源名。
如今站在更高的視角,發(fā)現(xiàn)這個錯誤很簡單,但是其實這個問題我們直到后續(xù) code review 時才及時糾正,確實可以更早就做得更好。
不過從目前社區(qū)的動態(tài)來看,on-demand xDS 或許已經(jīng)是一個 roadmap,起碼目前 xDS 已經(jīng)全系支持 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 這個詞的創(chuàng)造者 William Morgan 對其做出的定義,概括一下:作為網(wǎng)絡代理,并對用戶透明,承擔作為基礎(chǔ)設(shè)施的職責。
圖 3這里的職責在 RocketMQ 中包括服務發(fā)現(xiàn)、負載均衡、流量監(jiān)控等職責,使得調(diào)用方和被代理方的職責大大降低了。
當然目前的 RocketMQ Filter 為了保證兼容性做出了很多讓步,比如為了保證 SDK 可以成功獲取到路由,將路由信息聚合包裝成了 TopicRouteData 返回給了 SDK ,但是在理想情況下,SDK 本身已經(jīng)不需要關(guān)心路由了,純?yōu)?Mesh 情景設(shè)計的 SDK 是更加精簡的,不再會有消費側(cè) Rebalance,發(fā)送和消費的服務發(fā)現(xiàn),甚至在未來像消息體壓縮和 schema 校驗這些功能 SDK 和 Broker 或許都可以不用再關(guān)心,來了就發(fā)送/消費,發(fā)送/消費完就走或許才是 RocketMQ Mesh 的終極形態(tài)。
圖 4What's Next ?
目前 RocketMQ Filter 具備了普通消息的發(fā)送和 Pop 消費能力,但是如果想要具備更加完整的產(chǎn)品形態(tài),功能上還有一些需要補充:
- 支持 Pull 請求:現(xiàn)在 Envoy Proxy 只接收 Pop 類型的消費請求,之后會考慮支持普通的 Pull 類型,Envoy 會將 Pull 請求轉(zhuǎn)換成 Pop 請求,從而做到讓用戶無感知;
- 支持全局順序消息:目前在 Mesh 體系下,雖然全局順序消息的路由不存在問題,但是如果多個 Consumer 同時消費全局順序消息,其中一個消費者突然下線導致消息沒有 ACK 而會導致另一個消費者的消息產(chǎn)生亂序,這一點需要在 Envoy 中進行保證;
- Broker 側(cè)的 Proxy:對 Broker 側(cè)的請求也進行代理和調(diào)度。
蜿蜒曲折的社區(qū)歷程
起初,RocketMQ Filter 的初次 Pull Request 就包含了當前幾乎全部的功能,導致了一個超過 8K 行的超大 PR,感謝@天千 在 Code Review 中所做的工作,非常專業(yè),幫助了我們更快地合入社區(qū)。
另外,Envoy 社區(qū)的 CI 實在太嚴格了,嚴格要求 97% 以上的單測行覆蓋率,Bazel 源碼級依賴,純靜態(tài)鏈接,本身無 cache 編譯 24 邏輯核心 CPU 和 load 均打滿至少半個小時才能編完,社區(qū)的各種 CI 跑完一次則少說兩三個小時,多則六七個小時,并對新提交的代碼有著極其嚴苛的語法和 format 要求,這使得在 PR 中修改一小部分代碼就可能帶來大量的單測變動和 format 需求,不過好的是單測可以很方便地幫助我們發(fā)現(xiàn)一些內(nèi)存 case 。客觀上來說,官方社區(qū)以這么高的標準來要求 contributors 確實可以很大程度上控制住代碼質(zhì)量,我們在補全單測的過程中,還是發(fā)現(xiàn)并解決了不少自身的問題,總得來說還是有一定必要的,畢竟對于 C++ 代碼而言,一旦生產(chǎn)環(huán)境出問題,調(diào)試和追蹤起來會困難得多。
最后,RocketMQ Filter 的代碼由我和@叔田 共同完成,對于一個沒什么開源經(jīng)驗的我來說,為這樣的熱門社區(qū)貢獻代碼是一次非常寶貴的經(jīng)歷,同時也感謝叔田在此過程中給予的幫助和建議。
總結(jié)
以上是生活随笔為你收集整理的Apache RocketMQ 的 Service Mesh 开源之旅的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 带着问题学 Kubernetes 架构!
- 下一篇: 从原理上搞懂如何设置线程池参数大小?