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