本篇文章整理自今日頭條的沈輝在 RocketMQ 開(kāi)發(fā)者沙龍中的演講,主要和大家分享一下,RocketMQ 在微服務(wù)架構(gòu)下的實(shí)踐和容災(zāi)體系建設(shè)。沈輝是今日頭條的架構(gòu)師,主要負(fù)責(zé) RocketMQ 在頭條的落地以及架構(gòu)設(shè)計(jì),參與消息系統(tǒng)的時(shí)間大概一年左右。
以下是本次分享的議題:
頭條的業(yè)務(wù)背景
為什么選擇 RocketMQ
RocketMQ 在頭條的落地實(shí)踐
頭條的容災(zāi)系統(tǒng)建設(shè)
業(yè)務(wù)背景
今日頭條的服務(wù)大量使用微服務(wù),容器數(shù)目巨大,業(yè)務(wù)線繁多, Topic 的數(shù)量也非常多。另外,使用的語(yǔ)言比較繁雜,包括 Python,Go, C++, Java, JS 等,對(duì)于基礎(chǔ)組件的接入,維護(hù) SDK 的成本很高。
引入 RocketMQ 之前采用的消息隊(duì)列是 NSQ 和 kafka , NSQ 是純內(nèi)存的消息隊(duì)列,缺少消息的持久性,不落盤(pán)直接寫(xiě)到 Golang 的 channel 里,在并發(fā)量高的時(shí)候 CPU 利用率非常高,其優(yōu)點(diǎn)是可以無(wú)限水平擴(kuò)展,另外,由于不需要保證消息的有序性,集群?jiǎn)吸c(diǎn)故障對(duì)可用性基本沒(méi)有影響,所以具有非常高的可用性。我們也用到了 Kafka ,它的主要問(wèn)題是在業(yè)務(wù)線和 Topic 繁多,其寫(xiě)入性能會(huì)出現(xiàn)明顯的下降,拆分集群又會(huì)增加額外的運(yùn)維負(fù)擔(dān)。并且在高負(fù)載下,其故障恢復(fù)時(shí)間比較長(zhǎng)。所以,針對(duì)當(dāng)時(shí)的狀況和業(yè)務(wù)場(chǎng)景的需求,我們進(jìn)行了一些調(diào)研,期望選擇一款新的 MQ 來(lái)比較好的解決目前的困境,最終選擇了 RocketMQ 。
為什么選擇 RocketMQ
這是一個(gè)經(jīng)過(guò)阿里巴巴多年雙11驗(yàn)證過(guò)的、可以支持億級(jí)并發(fā)的開(kāi)源消息隊(duì)列,是值得信任的。其次關(guān)注一下他的特性。 RocketMQ 具有高可靠性、數(shù)據(jù)持久性,和 Kafka 一樣是先寫(xiě) PageCache ,再落盤(pán),并且數(shù)據(jù)有多副本;并且它的存儲(chǔ)模型是所有的 Topic 都寫(xiě)到同一個(gè) Commitlog 里,是一個(gè)append only 操作,在海量 Topic 下也能將磁盤(pán)的性能發(fā)揮到極致,并且保持穩(wěn)定的寫(xiě)入時(shí)延。然后就是他的性能,經(jīng)過(guò)我們的 benchmark ,采用一主兩從的結(jié)構(gòu),單機(jī) qps 可以達(dá)到 14w , latency 保持在 2ms 以內(nèi)。對(duì)比之前的 NSQ 和 Kafka , Kafka 的吞吐非常高,但是在多 Topic 下, Kafka 的 PCT99 毛刺會(huì)非常多,而且平均值非常長(zhǎng),不適合在線業(yè)務(wù)場(chǎng)景。另外 NSQ 的消息首先經(jīng)過(guò) Golang 的 channel ,這是非常消耗 CPU 的,在單機(jī) 5~6w 的時(shí)候 CPU 利用率達(dá)到 50~60% ,高負(fù)載下的寫(xiě)延遲不穩(wěn)定。另外 RocketMQ 對(duì)在線業(yè)務(wù)特性支持是非常豐富的,支持 retry , 支持并發(fā)消費(fèi),死信隊(duì)列,延時(shí)消息,基于時(shí)間戳的消息回溯,另外消息體支持消息頭,這個(gè)是非常有用的,可以直接支持實(shí)現(xiàn)消息鏈路追蹤,不然就需要把追蹤信息寫(xiě)到 message 的 body 里;還支持事務(wù)的消息。綜合以上特性最終選擇了 RocketMQ 。
RocketMQ 在頭條的落地實(shí)踐
下面簡(jiǎn)單介紹下,今日頭條的部署結(jié)構(gòu),如圖所示:
由于生產(chǎn)者種類(lèi)繁多,我們傾向于保持客戶端簡(jiǎn)單,因?yàn)橥苿?dòng) SDK 升級(jí)是一個(gè)很沉重的負(fù)擔(dān),所以我們通過(guò)提供一個(gè) Proxy 層,來(lái)保持生產(chǎn)端的輕量。 Proxy 層是由一個(gè)標(biāo)準(zhǔn)的 gRpc 框架實(shí)現(xiàn),也可以用 thrift ,當(dāng)然任何 RPC 都框架都可以實(shí)現(xiàn)。
Producer 的 Proxy 相對(duì)比較簡(jiǎn)單,雖然在 Producer 這邊也集成了很多比如路由管理、監(jiān)控等其他功能, SDK 只需實(shí)現(xiàn)發(fā)消息的請(qǐng)求,所以 SDK 的非常輕量、改動(dòng)非常少,在迭代過(guò)程中也不需要一個(gè)個(gè)推業(yè)務(wù)去升級(jí) SDK 。 SDK 通過(guò)服務(wù)發(fā)現(xiàn)去找到一個(gè) Proxy 實(shí)例,然后建立連接發(fā)送消息, Proxy 的工作是根據(jù) RPC 請(qǐng)求的消息轉(zhuǎn)發(fā)到對(duì)應(yīng)的 Broker 集群上。 Consumer Proxy 實(shí)現(xiàn)的是 pull 和二次 reblance 的邏輯,這個(gè)后面會(huì)講到,相當(dāng)于把 Consumer 的 pull 透?jìng)鹘o Brokerset , Proxy 這邊會(huì)有一個(gè)消息的 cache ,一定程度上降低對(duì) broker page cache 的污染。這個(gè)架構(gòu)和滴滴的 MQ 架構(gòu)有點(diǎn)相似,他們也是之前做了一個(gè) Proxy ,用 thrift 做 RPC ,這對(duì)后端的擴(kuò)容、運(yùn)維、減少 SDK 的邏輯上來(lái)說(shuō)都是很有必要的。
在容器以及微服務(wù)場(chǎng)景下為什么要做這個(gè) Porxy ?
有以下幾點(diǎn)原因: 1、 SDK 會(huì)非常簡(jiǎn)單輕量。
2、很容易對(duì)流量進(jìn)行控制; Proxy 可以對(duì)生產(chǎn)端的流量進(jìn)行控制,比如我們期望某些Broker壓力比較大的時(shí)候,能夠切一些流量或者說(shuō)切流量到另外的機(jī)房,這種流量的調(diào)度,多環(huán)境的支持,再比如有些預(yù)發(fā)布環(huán)境、預(yù)上線環(huán)境的支持,我們 Topic 這邊寫(xiě)入的流量可以在 Proxy 這邊可以很方便的完成控制,不用修改 SDK 。
3,解決連接的問(wèn)題;特別是解決 Python 的問(wèn)題, Python 實(shí)現(xiàn)的服務(wù)如果要獲得高并發(fā)度,一般是采取多進(jìn)程模型,這意味著一個(gè)進(jìn)程一個(gè)連接,特別是對(duì)于部署到 Docker 里的 Python 服務(wù),它可能一個(gè)容器里啟動(dòng)幾百個(gè)進(jìn)程,如果直接連到 Broker ,這個(gè) Broker 上的連接數(shù)可能到幾十上百萬(wàn),此時(shí) CPU 軟中斷會(huì)非常高,導(dǎo)致讀寫(xiě)的延時(shí)的明顯上漲。
4,通過(guò) Proxy ,多了一個(gè)代理,在消費(fèi)不需要順序的情況下,我們可以支持更高的并發(fā)度, Consumer 的實(shí)例數(shù)可以超過(guò) Consume Queue 的數(shù)量。
5,可以無(wú)縫的繼承其他的 MQ 。中間有一層 Proxy ,后面可以更改存儲(chǔ)引擎,這個(gè)對(duì)客戶端是無(wú)感知的。
6,在 Conusmer 在升級(jí)或 Restart 的時(shí)候, Consumer 如果直接連 broker 的話, rebalance 觸發(fā)比較頻繁, 如果 rebalance 比較頻繁,且 Topic 量比較大的時(shí)候,可能會(huì)造成消息堆積,這個(gè)業(yè)務(wù)不是太接受的;如果加一層 Proxy 的話, rebalance 只在 Proxt 和 Broker 之間進(jìn)行,就不需要 Consumer 再進(jìn)行一次 rebalance , Proxy 只需要維護(hù)著和自己建立連接的 Consumer 就可以了。當(dāng)消費(fèi)者重啟或升級(jí)的時(shí)候,可以最小程度的減少 rebalance 。
以上是我們通過(guò) Proxy 接口給 RocketMQ 帶來(lái)的好處。因?yàn)槎嗔艘粚樱矔?huì)帶來(lái)額外的 Overhead 的,如下:
1,會(huì)消耗 CPU , Proxy 那一層會(huì)做RPC協(xié)議的序列化和反序列化。
如下是 Conusme Proxy 的結(jié)構(gòu)圖,它帶來(lái)了消費(fèi)并發(fā)度的提高。由于我們的 Broker 集群是獨(dú)立部署的,考慮到broker主要是消耗包括網(wǎng)卡、磁盤(pán)和內(nèi)存資源,對(duì)于 CPU 的消耗反而不高,這里的解決方式直接進(jìn)行混合部署,然后直接在新的機(jī)器上進(jìn)行擴(kuò),但是 Broker 這邊的 CPU 也是可以得到利用的。
2,延遲問(wèn)題。經(jīng)過(guò)測(cè)試,在 4Kmsg、20W Tps 下,延遲會(huì)有所增加,大概是 1ms ,從 2ms 到 3ms 左右,這個(gè)時(shí)延對(duì)于業(yè)務(wù)來(lái)說(shuō)是可以接受的。
下面看下 Consumer 這邊的邏輯,如下圖所示,
比如上面部署了兩個(gè) Proxy , Broker,左邊有 6 個(gè) Queue ,對(duì)于順序消息來(lái)說(shuō),左邊這邊 rebalance 是一個(gè)相對(duì)靜態(tài)的結(jié)果, Consumer 的上下線是比較頻繁的。對(duì)于順序消息來(lái)說(shuō),左邊和之前的邏輯是保持一致的, Proxy 會(huì)為每個(gè) Consumer 實(shí)例分配到合適的數(shù)量的 Queue ;對(duì)于不關(guān)心順序性的消息,Proxy 會(huì)把所有的消息都放到一個(gè)隊(duì)列里,然后從這個(gè)隊(duì)列 dispatch 到各個(gè) Consumer ,對(duì)于亂序消息來(lái)說(shuō),理論上來(lái)說(shuō) Consumer 數(shù)量可以無(wú)限擴(kuò)展的;相對(duì)于和普通 Consumer 直連的情況,Consumer 的數(shù)量如果超過(guò)了Consume Queue的數(shù)量,其中多出來(lái)的 Consumer 是沒(méi)有辦法分配到 Queue 的,而且在容器部署環(huán)境下,單 Consumer 不能起太多線程去支撐高并發(fā);在容器這個(gè)環(huán)境下,比較好的方式是多實(shí)例,然后按照 CPU 的核心數(shù),啟動(dòng)多個(gè)線程,比如 8C 的啟動(dòng) 8 個(gè)線程,因?yàn)槿萜魇怯?Quota 的,一般是 1C,2C,4C,8C 這樣,這種情況下,如果線程數(shù)超過(guò)了 CPU 的核心數(shù),其實(shí)對(duì)并發(fā)度并沒(méi)有太大的意義。
接下來(lái),分享一下做這個(gè)接入方式的時(shí)候遇到的一些問(wèn)題,如下圖所示:
1、消息大小的限制。
因?yàn)檫@里有一層 RPC ,在 RPC 請(qǐng)求過(guò)程中會(huì)有單次請(qǐng)求大小的限制;另外一方面是 RocketMQ 的 producer 里會(huì)有一個(gè) MaxMessageSize 方法去控制消息不能超過(guò)這個(gè)大小; Broker 里也有一個(gè)參數(shù),是 Broker 啟動(dòng)的配置,這個(gè)需要Broker重啟,不然修改也不生效, Broker 里面有一個(gè) DefaultAppendMessage 配置,是在啟動(dòng)的時(shí)候傳進(jìn)去對(duì)的參數(shù),如果僅 NameServer 在線變更是不生效的,而且超過(guò)這個(gè)大小會(huì)報(bào)錯(cuò)。因?yàn)楝F(xiàn)在 RocketMQ 默認(rèn)是 4M 的消息,如果將 RocketMQ 作為日志總線,可能消息體大小不是太夠, Procuer 和 Broker 是都需要做變更的。
2、多連接的問(wèn)題。
如果看 RocketMQ 源碼會(huì)發(fā)現(xiàn),多個(gè) Producer 是共享一個(gè)底層的 MQ Client 實(shí)例的,因?yàn)橐粋€(gè) socket 連接吞吐是有限的,所以只會(huì)和Broker建立一個(gè)socket連接。另外,我們也有 socket 與 socket 之間是隔離的,可以通過(guò) Producer 的 setIntanceName() ,當(dāng)與 DefaultI Instance 的 name 不一樣時(shí)會(huì)新啟動(dòng)一個(gè) Client 的,其實(shí)就是一個(gè)新的 socket 連接,對(duì)于有隔離需求的、連接池需求得等,這個(gè)參數(shù)是有用的,在 4.5.0 上新加了一個(gè)接口是指定構(gòu)造的實(shí)例數(shù)量。
3、超時(shí)設(shè)置。
因?yàn)槎嗔艘粚?RPC ,那一層是有一個(gè)超時(shí)設(shè)置的,這個(gè)會(huì)有點(diǎn)不一樣,因?yàn)槲覀兊?RPC 請(qǐng)求里會(huì)帶上超時(shí)設(shè)置的,客戶端到 Proxy 有一個(gè) RTT ,然后 Producer 到 Broker 的發(fā)送消息也是有一個(gè)請(qǐng)求響應(yīng)延時(shí),需要給 SDK 一個(gè)正確的超時(shí)語(yǔ)義。
4、如何選擇一個(gè)合適的 reblance 算法,我們遇到這個(gè)問(wèn)題是在雙機(jī)房同城容災(zāi)的背景下,會(huì)有一邊 Topic 的 MessageQueue 沒(méi)有寫(xiě)入。
這種情況下, RocketMQ 自己默認(rèn)的是按照平均分配算法進(jìn)行分配的,比如有 10 個(gè) Queue , 3 個(gè) Proxy 情況, 1、2、3 是對(duì)應(yīng) Proxy1,4、5、6 是對(duì)應(yīng) Proxy2,7、8、9、10 是對(duì)應(yīng) Proxy3 ,如果在雙機(jī)房同城容災(zāi)部署情況下,一般有一半 Message Queue 是沒(méi)有寫(xiě)入的,會(huì)有一大部分 Consumer 是啟動(dòng)了,但是分配到的 Message Queue 是沒(méi)有消息寫(xiě)入的。然后另外一個(gè)訴求是因?yàn)橛锌鐧C(jī)房的流量,所以他其實(shí)直接復(fù)用開(kāi)源出來(lái)的 Consumer 的實(shí)現(xiàn)里就有根據(jù) MachineRoom 去做 reblance ,會(huì)就近分配你的 MessageQueue 。
5、在 Proxy 這邊需要做一個(gè)緩存,特別是拉消息的緩存。
特別提醒一下, Proxy 拉消息都是通過(guò) Slave 去拉,不需要使用 Master 去拉, Master 的 IO 比較重;還有 Buffer 的管理,我們是遇到過(guò)這種問(wèn)題的,如果只考慮 Message 數(shù)量的話,會(huì)導(dǎo)致 OOM ,所以要注意消息 size 的設(shè)置,
6、端到端壓縮。
因?yàn)?RocketMQ 在消息超過(guò) 4k 的時(shí)候, Producer 會(huì)進(jìn)行壓縮。如果不在客戶端做壓縮,這還是涉及到 RPC 的問(wèn)題, RPC 一般來(lái)說(shuō), Byte 類(lèi)型,就是 Byte 數(shù)組類(lèi)型它是不會(huì)進(jìn)行壓縮的,只是會(huì)進(jìn)行一些常規(guī)的編碼,所以消息體需要在客戶端做壓縮。如果放在 Proxy 這邊做, Proxy 壓力會(huì)比較大,所以不如放在客戶端去承載這個(gè)壓縮。
頭條的容災(zāi)系統(tǒng)建設(shè)
前面大致介紹了我們這邊大致如何接入 RocketMQ ,如何實(shí)現(xiàn)這么一套 Proxy ,以及在實(shí)現(xiàn)這套 Proxy 過(guò)程中遇到的一些問(wèn)題。下面看一下災(zāi)難恢復(fù)的方案,設(shè)計(jì)之初也參考了一些潛在相關(guān)方案。
第一種方案:擴(kuò)展集群,擴(kuò)展集群的方案就像下圖所示。
這是 master 和 slave 跨機(jī)房去部署的方式。因?yàn)槲覀冇幸粚?proxy ,所以可以很方便的去做流量的調(diào)度,讓消息只在一個(gè)主機(jī)房進(jìn)行消息寫(xiě)入,不需要一個(gè)類(lèi)似中控功能的實(shí)體存在。
第二種方案:類(lèi)似 MySQL 和 Redis 的架構(gòu)模式,即單主模式,只有一個(gè)地方式寫(xiě)入的,如下圖所示。數(shù)據(jù)是通過(guò) Mysql Matser/Slave 方式同步到另一個(gè)機(jī)房。這樣 RocketMQ 會(huì)啟動(dòng)一個(gè)類(lèi)似 Kafka 的 Mirror maker 類(lèi)進(jìn)行消息復(fù)制,這樣會(huì)多一倍的冗余,實(shí)際上數(shù)據(jù)還會(huì)存在一些不一致的問(wèn)題。
第三種方案:雙寫(xiě)加雙向復(fù)制的架構(gòu)。這個(gè)結(jié)構(gòu)太復(fù)雜不好控制,尤其是雙向復(fù)制,其中消息區(qū)回環(huán)的問(wèn)題比較好解決,只需針對(duì)在每個(gè)正常的業(yè)務(wù)消息,在 Header 里加一個(gè)標(biāo)志字段就好,另外的 Mirror 發(fā)現(xiàn)有這個(gè)字段就把這條消息直接丟掉即可。這個(gè)鏈路上維護(hù)復(fù)雜而且存在數(shù)據(jù)冗余,其中最大問(wèn)題是兩邊的數(shù)據(jù)不對(duì)等,在一邊掛掉情況下,對(duì)于一些無(wú)法接受數(shù)據(jù)不一致的是有問(wèn)題的。
此外,雙寫(xiě)都是沒(méi)有 Mirror 的方案,如下圖所示。這也是我們最終選擇的方案。我們對(duì)有序消息和無(wú)序消息的處理方式不太一樣,針對(duì)無(wú)序消息只需就近寫(xiě)本機(jī)房就可以了,對(duì)于有序消息我們還是會(huì)有一個(gè)主機(jī)房,Proxy 會(huì)去 NameServer 拉取 Broker 的 Queue 信息, Producer 將有序消息路由到一個(gè)指定主機(jī)房,消費(fèi)端這一側(cè),就是就近拉取消息。對(duì)于順序消息我們會(huì)采取一定的調(diào)度邏輯保證均衡的分擔(dān)壓力獲取消息,這個(gè)架構(gòu)的優(yōu)點(diǎn)是比較簡(jiǎn)單,缺點(diǎn)是當(dāng)集群中一邊掛掉時(shí),會(huì)造成有序消息的無(wú)序,這邊是通過(guò)記錄消息 offset 來(lái)處理的。
此外,還有一種獨(dú)立集群部署的,相當(dāng)于沒(méi)有上圖中間的有序消息那條線,因?yàn)榇蠖鄶?shù)有序消息是整體體系的,服務(wù)要部署單元化,比如某些 uid 、訂單 Id 的消息或請(qǐng)求只會(huì)落到一邊機(jī)房的,完全不用擔(dān)心消息來(lái)得時(shí)候是否需要按照某些 key 去指定 MessageQueue ,因?yàn)檫^(guò)來(lái)的消息必定是隸屬于這個(gè)機(jī)房的,也就是說(shuō)中間有序消息那條線可以不用關(guān)心了,可以直接去掉。但是,這個(gè)是和整個(gè)公司部署方式以及單元化體系有關(guān)系的,對(duì)于部分業(yè)務(wù)我們是直接做到兩個(gè)集群,兩邊的生產(chǎn)者、消費(fèi)者、Broker 、Proxy 全部是隔離的,兩邊都互不發(fā)現(xiàn),就是這么一套運(yùn)行方式,但是這就需要業(yè)務(wù)的上下游要做到單元化的程度才可行。
以上就是 RocketMQ 在頭條的落地實(shí)踐頭條的容災(zāi)系統(tǒng)建設(shè)分享,謝謝。
作者信息:沈輝,畢業(yè)于北京郵電大學(xué),就職于字節(jié)跳動(dòng)基礎(chǔ)架構(gòu),主要參與負(fù)責(zé)消息隊(duì)列服務(wù)的開(kāi)發(fā)與維護(hù)。
本文為云棲社區(qū)原創(chuàng)內(nèi)容,未經(jīng)允許不得轉(zhuǎn)載
評(píng)論
查看更多