今日頭條在消息服務平臺和容災體系建設方面的實踐與思考

本篇文章整理自今日頭條的沈輝在 RocketMQ 開發者沙龍中的演講,主要和你們分享一下,RocketMQ 在微服務架構下的實踐和容災體系建設。沈輝是今日頭條的架構師,主要負責 RocketMQ 在頭條的落地以及架構設計,參與消息系統的時間大概一年左右。算法

如下是本次分享的議題:sql

  • 頭條的業務背景
  • 爲何選擇 RocketMQ
  • RocketMQ 在頭條的落地實踐
  • 頭條的容災系統建設

業務背景

今日頭條的服務大量使用微服務,容器數目巨大,業務線繁多, Topic 的數量也很是多。另外,使用的語言比較繁雜,包括 Python,Go, C++, Java, JS 等,對於基礎組件的接入,維護 SDK 的成本很高。後端

引入 RocketMQ 以前採用的消息隊列是 NSQ 和 kafka , NSQ 是純內存的消息隊列,缺乏消息的持久性,不落盤直接寫到 Golang 的 channel 裏,在併發量高的時候 CPU 利用率很是高,其優勢是能夠無限水平擴展,另外,因爲不須要保證消息的有序性,集羣單點故障對可用性基本沒有影響,因此具備很是高的可用性。咱們也用到了 Kafka ,它的主要問題是在業務線和 Topic 繁多,其寫入性能會出現明顯的降低,拆分集羣又會增長額外的運維負擔。而且在高負載下,其故障恢復時間比較長。因此,針對當時的情況和業務場景的需求,咱們進行了一些調研,指望選擇一款新的 MQ 來比較好的解決目前的困境,最終選擇了 RocketMQ 。數組

爲何選擇 RocketMQ

這是一個通過阿里巴巴多年雙11驗證過的、能夠支持億級併發的開源消息隊列,是值得信任的。其次關注一下他的特性。 RocketMQ 具備高可靠性、數據持久性,和 Kafka 同樣是先寫 PageCache ,再落盤,而且數據有多副本;而且它的存儲模型是全部的 Topic 都寫到同一個 Commitlog 裏,是一個append only 操做,在海量 Topic 下也能將磁盤的性能發揮到極致,而且保持穩定的寫入時延。而後就是他的性能,通過咱們的 benchmark ,採用一主兩從的結構,單機 qps 能夠達到 14w , latency 保持在 2ms 之內。對比以前的 NSQ 和 Kafka , Kafka 的吞吐很是高,可是在多 Topic 下, Kafka 的 PCT99 毛刺會很是多,並且平均值很是長,不適合在線業務場景。另外 NSQ 的消息首先通過 Golang 的 channel ,這是很是消耗 CPU 的,在單機 5~6w 的時候 CPU 利用率達到 50~60% ,高負載下的寫延遲不穩定。另外 RocketMQ 對在線業務特性支持是很是豐富的,支持 retry , 支持併發消費,死信隊列,延時消息,基於時間戳的消息回溯,另外消息體支持消息頭,這個是很是有用的,能夠直接支持實現消息鏈路追蹤,否則就須要把追蹤信息寫到 message 的 body 裏;還支持事務的消息。綜合以上特性最終選擇了 RocketMQ 。緩存

RocketMQ 在頭條的落地實踐

下面簡單介紹下,今日頭條的部署結構,如圖所示:多線程

lALPDgQ9rLm1fPXNAnnNBQA_1280_633_png_620x10000q90g

因爲生產者種類繁多,咱們傾向於保持客戶端簡單,由於推進 SDK 升級是一個很沉重的負擔,因此咱們經過提供一個 Proxy 層,來保持生產端的輕量。 Proxy 層是由一個標準的 gRpc 框架實現,也能夠用 thrift ,固然任何 RPC 都框架均可以實現。架構

Producer 的 Proxy 相對比較簡單,雖然在 Producer 這邊也集成了不少好比路由管理、監控等其餘功能, SDK 只需實現發消息的請求,因此 SDK 的很是輕量、改動很是少,在迭代過程當中也不須要一個個推業務去升級 SDK 。 SDK 經過服務發現去找到一個 Proxy 實例,而後創建鏈接發送消息, Proxy 的工做是根據 RPC 請求的消息轉發到對應的 Broker 集羣上。 Consumer Proxy 實現的是 pull 和二次 reblance 的邏輯,這個後面會講到,至關於把 Consumer 的 pull 透傳給 Brokerset , Proxy 這邊會有一個消息的 cache ,必定程度上下降對 broker page cache 的污染。這個架構和滴滴的 MQ 架構有點類似,他們也是以前作了一個 Proxy ,用 thrift 作 RPC ,這對後端的擴容、運維、減小 SDK 的邏輯上來講都是頗有必要的。併發

在容器以及微服務場景下爲何要作這個 Porxy ?

lALPDgQ9rLm1fPfNAp_NBOQ_1252_671_png_620x10000q90g

有如下幾點緣由:
一、 SDK 會很是簡單輕量。app

二、很容易對流量進行控制; Proxy 能夠對生產端的流量進行控制,好比咱們指望某些Broker壓力比較大的時候,可以切一些流量或者說切流量到另外的機房,這種流量的調度,多環境的支持,再好比有些預發佈環境、預上線環境的支持,咱們 Topic 這邊寫入的流量能夠在 Proxy 這邊能夠很方便的完成控制,不用修改 SDK 。框架

3,解決鏈接的問題;特別是解決 Python 的問題, Python 實現的服務若是要得到高併發度,通常是採起多進程模型,這意味着一個進程一個鏈接,特別是對於部署到 Docker 裏的 Python 服務,它可能一個容器裏啓動幾百個進程,若是直接連到 Broker ,這個 Broker 上的鏈接數可能到幾十上百萬,此時 CPU 軟中斷會很是高,致使讀寫的延時的明顯上漲。

4,經過 Proxy ,多了一個代理,在消費不須要順序的狀況下,咱們能夠支持更高的併發度, Consumer 的實例數能夠超過 Consume Queue 的數量。

5,能夠無縫的繼承其餘的 MQ 。中間有一層 Proxy ,後面能夠更改存儲引擎,這個對客戶端是無感知的。

6,在 Conusmer 在升級或 Restart 的時候, Consumer 若是直接連 broker 的話, rebalance 觸發比較頻繁, 若是 rebalance 比較頻繁,且 Topic 量比較大的時候,可能會形成消息堆積,這個業務不是太接受的;若是加一層 Proxy 的話, rebalance 只在 Proxt 和 Broker 之間進行,就不須要 Consumer 再進行一次 rebalance , Proxy 只須要維護着和本身創建鏈接的 Consumer 就能夠了。當消費者重啓或升級的時候,能夠最小程度的減小 rebalance 。

以上是咱們經過 Proxy 接口給 RocketMQ 帶來的好處。由於多了一層,也會帶來額外的 Overhead 的,以下:

1,會消耗 CPU , Proxy 那一層會作RPC協議的序列化和反序列化。

以下是 Conusme Proxy 的結構圖,它帶來了消費併發度的提升。因爲咱們的 Broker 集羣是獨立部署的,考慮到broker主要是消耗包括網卡、磁盤和內存資源,對於 CPU 的消耗反而不高,這裏的解決方式直接進行混合部署,而後直接在新的機器上進行擴,可是 Broker 這邊的 CPU 也是能夠獲得利用的。

2,延遲問題。通過測試,在 4Kmsg、20W Tps 下,延遲會有所增長,大概是 1ms ,從 2ms 到 3ms 左右,這個時延對於業務來講是能夠接受的。

下面看下 Consumer 這邊的邏輯,以下圖所示,

lALPDgQ9rLm1fPrNAljNBK4_1198_600_png_620x10000q90g

好比上面部署了兩個 Proxy , Broker,左邊有 6 個 Queue ,對於順序消息來講,左邊這邊 rebalance 是一個相對靜態的結果, Consumer 的上下線是比較頻繁的。對於順序消息來講,左邊和以前的邏輯是保持一致的, Proxy 會爲每一個 Consumer 實例分配到合適的數量的 Queue ;對於不關心順序性的消息,Proxy 會把全部的消息都放到一個隊列裏,而後從這個隊列 dispatch 到各個 Consumer ,對於亂序消息來講,理論上來講 Consumer 數量能夠無限擴展的;相對於和普通 Consumer 直連的狀況,Consumer 的數量若是超過了Consume Queue的數量,其中多出來的 Consumer 是沒有辦法分配到 Queue 的,並且在容器部署環境下,單 Consumer 不能起太多線程去支撐高併發;在容器這個環境下,比較好的方式是多實例,而後按照 CPU 的核心數,啓動多個線程,好比 8C 的啓動 8 個線程,由於容器是有 Quota 的,通常是 1C,2C,4C,8C 這樣,這種狀況下,若是線程數超過了 CPU 的核心數,其實對併發度並無太大的意義。

接下來,分享一下作這個接入方式的時候遇到的一些問題,以下圖所示:

lADPDgQ9rLm1fPzNAdLNBFw_1116_466_jpg_620x10000q90g

一、消息大小的限制。

由於這裏有一層 RPC ,在 RPC 請求過程當中會有單次請求大小的限制;另一方面是 RocketMQ 的 producer 裏會有一個 MaxMessageSize 方法去控制消息不能超過這個大小; Broker 裏也有一個參數,是 Broker 啓動的配置,這個須要Broker重啓,否則修改也不生效, Broker 裏面有一個 DefaultAppendMessage 配置,是在啓動的時候傳進去對的參數,若是僅 NameServer 在線變動是不生效的,並且超過這個大小會報錯。由於如今 RocketMQ 默認是 4M 的消息,若是將 RocketMQ 做爲日誌總線,可能消息體大小不是太夠, Procuer 和 Broker 是都須要作變動的。

二、多鏈接的問題。

若是看 RocketMQ 源碼會發現,多個 Producer 是共享一個底層的 MQ Client 實例的,由於一個 socket 鏈接吞吐是有限的,因此只會和Broker創建一個socket鏈接。另外,咱們也有 socket 與 socket 之間是隔離的,能夠經過 Producer 的 setIntanceName() ,當與 DefaultI Instance 的 name 不同時會新啓動一個 Client 的,其實就是一個新的 socket 鏈接,對於有隔離需求的、鏈接池需求得等,這個參數是有用的,在 4.5.0 上新加了一個接口是指定構造的實例數量。

三、超時設置。

由於多了一層 RPC ,那一層是有一個超時設置的,這個會有點不同,由於咱們的 RPC 請求裏會帶上超時設置的,客戶端到 Proxy 有一個 RTT ,而後 Producer 到 Broker 的發送消息也是有一個請求響應延時,須要給 SDK 一個正確的超時語義。

四、如何選擇一個合適的 reblance 算法,咱們遇到這個問題是在雙機房同城容災的背景下,會有一邊 Topic 的 MessageQueue 沒有寫入。

這種狀況下, RocketMQ 本身默認的是按照平均分配算法進行分配的,好比有 10 個 Queue , 3 個 Proxy 狀況, 一、二、3 是對應 Proxy1,四、五、6 是對應 Proxy2,七、八、九、10 是對應 Proxy3 ,若是在雙機房同城容災部署狀況下,通常有一半 Message Queue 是沒有寫入的,會有一大部分 Consumer 是啓動了,可是分配到的 Message Queue 是沒有消息寫入的。而後另一個訴求是由於有跨機房的流量,因此他其實直接複用開源出來的 Consumer 的實現裏就有根據 MachineRoom 去作 reblance ,會就近分配你的 MessageQueue 。

五、在 Proxy 這邊須要作一個緩存,特別是拉消息的緩存。

特別提醒一下, Proxy 拉消息都是經過 Slave 去拉,不須要使用 Master 去拉, Master 的 IO 比較重;還有 Buffer 的管理,咱們是遇到過這種問題的,若是隻考慮 Message 數量的話,會致使 OOM ,因此要注意消息 size 的設置,

六、端到端壓縮。

由於 RocketMQ 在消息超過 4k 的時候, Producer 會進行壓縮。若是不在客戶端作壓縮,這仍是涉及到 RPC 的問題, RPC 通常來講, Byte 類型,就是 Byte 數組類型它是不會進行壓縮的,只是會進行一些常規的編碼,因此消息體須要在客戶端作壓縮。若是放在 Proxy 這邊作, Proxy 壓力會比較大,因此不如放在客戶端去承載這個壓縮。

頭條的容災系統建設

前面大體介紹了咱們這邊大體如何接入 RocketMQ ,如何實現這麼一套 Proxy ,以及在實現這套 Proxy 過程當中遇到的一些問題。下面看一下災難恢復的方案,設計之初也參考了一些潛在相關方案。

第一種方案:擴展集羣,擴展集羣的方案就像下圖所示。

lALPDgQ9rLm1fP7NAnPNBEM_1091_627_png_620x10000q90g

這是 master 和 slave 跨機房去部署的方式。由於咱們有一層 proxy ,因此能夠很方便的去作流量的調度,讓消息只在一個主機房進行消息寫入,不須要一個相似中控功能的實體存在。

第二種方案:相似 MySQL 和 Redis 的架構模式,即單主模式,只有一個地方式寫入的,以下圖所示。數據是經過 Mysql Matser/Slave 方式同步到另外一個機房。這樣 RocketMQ 會啓動一個相似 Kafka 的 Mirror maker 類進行消息複製,這樣會多一倍的冗餘,實際上數據還會存在一些不一致的問題。

lADPDgQ9rLm1fQDNAk7NBG0_1133_590_jpg_620x10000q90g

第三種方案:雙寫加雙向複製的架構。這個結構太複雜很差控制,尤爲是雙向複製,其中消息區迴環的問題比較好解決,只需針對在每一個正常的業務消息,在 Header 里加一個標誌字段就好,另外的 Mirror 發現有這個字段就把這條消息直接丟掉便可。這個鏈路上維護複雜並且存在數據冗餘,其中最大問題是兩邊的數據不對等,在一邊掛掉狀況下,對於一些沒法接受數據不一致的是有問題的。

lALPDgQ9rLm1fQHNAmXNBCg_1064_613_png_620x10000q90g

此外,雙寫都是沒有 Mirror 的方案,以下圖所示。這也是咱們最終選擇的方案。咱們對有序消息和無序消息的處理方式不太同樣,針對無序消息只需就近寫本機房就能夠了,對於有序消息咱們仍是會有一個主機房,Proxy 會去 NameServer 拉取 Broker 的 Queue 信息, Producer 將有序消息路由到一個指定主機房,消費端這一側,就是就近拉取消息。對於順序消息咱們會採起必定的調度邏輯保證均衡的分擔壓力獲取消息,這個架構的優勢是比較簡單,缺點是當集羣中一邊掛掉時,會形成有序消息的無序,這邊是經過記錄消息 offset 來處理的。

lADPDgQ9rLm1fQPNAlnNA9s_987_601_jpg_620x10000q90g

此外,還有一種獨立集羣部署的,至關於沒有上圖中間的有序消息那條線,由於大多數有序消息是總體體系的,服務要部署單元化,好比某些 uid 、訂單 Id 的消息或請求只會落到一邊機房的,徹底不用擔憂消息來得時候是否須要按照某些 key 去指定 MessageQueue ,由於過來的消息一定是隸屬於這個機房的,也就是說中間有序消息那條線能夠不用關心了,能夠直接去掉。可是,這個是和整個公司部署方式以及單元化體系有關係的,對於部分業務咱們是直接作到兩個集羣,兩邊的生產者、消費者、Broker 、Proxy 所有是隔離的,兩邊都互不發現,就是這麼一套運行方式,可是這就須要業務的上下游要作到單元化的程度纔可行。

以上就是 RocketMQ 在頭條的落地實踐頭條的容災系統建設分享,謝謝。

做者信息:沈輝,畢業於北京郵電大學,就任於字節跳動基礎架構,主要參與負責消息隊列服務的開發與維護。

 

 

 

 

 

 

原文連接

本文爲雲棲社區原創內容,未經容許不得轉載

相關文章
相關標籤/搜索