得物技術訊息中介軟體應用的常見問題與方案

得物技術 發表於 2022-05-14

1. 引言

訊息佇列(MQ)中介軟體已經普及很多年了,在網際網路應用中,通常稍大一些的應用,我們都可以見到MQ的身影。當前市面上有很多中訊息中介軟體,包括但不限於RabbitMQ、RocketMQ、ActiveMQ、Kafka(流處理中介軟體) 等。很多開發人員已經熟練的掌握了一個或者多個訊息中介軟體的使用。但是仍然有一些小夥伴們對訊息中介軟體不是特別熟悉,因為各種原因不能深入的去學習瞭解箇中原理和細節,導致使用的時候可能出現這樣那樣的問題。在這裡,我們就針對訊息佇列中介軟體使用中的典型問題作一番分析(包括順序訊息、可靠性保證、訊息冪等、延時訊息等),並提供一些解決方案。

2. 訊息中介軟體應用背景

2.1 訊息中介軟體基本思想

我們在單個系統中,一些業務處理可以順序依次的進行。而涉及到跨系統(有時候系統內部亦然)的時候,會產生比較複雜資料互動(也可以理解為訊息傳遞)的需求,這些資料的互動傳遞方式,可以是同步也可以是非同步的。在非同步傳遞資料的情況下,往往需要一個載體,來臨時儲存與分發訊息。在此基礎上,專門針對訊息接收、儲存、轉發而設計與開發出來的專業應用程式,都可以理解為訊息佇列中介軟體。

引申一下:如果我們自己簡單的使用一張資料庫表,來記錄資料,然後接受資料儲存在資料表,通過定時任務再將資料表的資料分發出去,那麼我們已經實現了一個最簡單的訊息系統(這就是本地訊息表)。

我們可以認為訊息中介軟體的基本思想就是 利用高效可靠的訊息傳遞機制進行非同步的資料傳輸。在這個基本思想的指導下,不同的訊息中間,因為其側重場景目的不同,在功能、效能、整體設計理念上又各有差別。

訊息佇列(MQ)本身是實現了生產者到消費者的單向通訊模型,RabbitMQ、RocketMQ、Kafka這些常用的MQ都是指實現了這個模型的訊息中介軟體。目前最常用的幾個訊息中介軟體主要有,RabbitMQ、RocketMQ、Kafka(分散式流處理平臺)、Pulsar(分散式訊息流平臺)。這裡我將兩個流處理平臺納入其中了, 更早的一些其他訊息中介軟體已經慢慢淡出視野。業務選型的時候我們遵循兩個主要的原則:最大熟悉程度原則(便於運維、使用可靠)、業務契合原則(中介軟體效能可以支撐業務體量、滿足業務功能需求)。

這幾個常用的訊息中介軟體選型對比,很容易找到,這裡就不詳細描述了。大概說一下:Pulsar目前用的不如 RabbitMQ、RocketMQ、Kafka多。RabbitMQ主要偏重是高可靠訊息,RocketMQ效能和功能並重,Kafka主要是在大資料處理中應用比較多(Pulsar比較類似)。

2.2 引入訊息中介軟體的意義

我們先簡單舉例介紹一下非同步、解藕、削峰的意義與價值(參考下面這張流程圖):

得物技術訊息中介軟體應用的常見問題與方案

對於一個使用者註冊介面,假設有2個業務點,分別是註冊、發放新人福利,各需要50ms去處理邏輯。如果我們將這兩個業務流程耦合在一個介面,那麼總計需要100ms處理完成。但是該流程中,使用者註冊時候,可以不用關心自己的福利是否立即發放,只要儘快註冊成功返回資料即可,後續新人福利這一部分業務可以在主流程之外處理。我們如果將其剝離出來,介面主流程中只處理登陸邏輯,並通過MQ推送一條訊息,通過非同步方式處理後續的發放新人福利邏輯,這樣即可保證註冊介面50ms左右即能獲取結果。而發放新人福利的業務,則通過非同步任務慢慢處理。 通過拆分業務點,我們已經做到解耦,註冊的附屬業務中增加或減少功能點都不會影響主流程。另外如果一個業務主流程在某個點請求併發比較高,正好通過非同步方式,可以將壓力分散到更長的時間段中去,達到減輕固定時間段處理壓力的目的,這就是流量削峰。

**另外,單執行緒模型的語言,通常對訊息中介軟體的需求更強烈。多執行緒模型的語言,或者協程型語言,雖然可以通過自身的多執行緒(或協程)機制,來實現業務內部的非同步處理,但是考慮到持久化問題以及管理難度,還是成熟的中介軟體更適合用來做非同步資料通訊,中介軟體還能實現分散式系統之間的資料非同步通訊。

2.3 訊息中介軟體的應用場景

訊息中介軟體的應用場景主要有:

    • 非同步通訊:可以用於業務系統內部的非同步通訊,也可以用於分散式系統資訊互動
    • 系統解耦:將不同性質的業務進行隔離切分,提升效能,主附流程分層,按照重要性進行隔離,減少異常影響
    • 流量削峰:間歇性突刺流量分散處理,減少系統壓力,提升系統可用性
    • 分散式事務一致性:RocketMQ提供的事務訊息功能可以處理分散式事務一致性(如電商訂單場景)。當然,也可以使用分散式事務中介軟體。
    • 訊息順序收發:這是最基礎的功能,先進先出,訊息佇列必備
    • 延時訊息: 延遲觸發的業務場景,如下單後延遲取消未支付訂單等
    • 大資料處理:日誌處理,kafka
    • 分散式快取同步:消費MySQLbinlog日誌進行快取同步,或者業務變動直接推送到MQ消費

所以,如果你的業務中有以上列舉的場景,或者類似的功能、效能需求,那麼快快引入 訊息中介軟體來提升你的業務效能吧。

3. 引入訊息中介軟體帶來的一系列問題

雖然訊息中介軟體引入有以上那麼多好處,但是使用的時候依然會存在很多問題。例如:

  • 引入訊息中介軟體增加了系統複雜度,怎麼使用維護
  • 訊息傳送失敗怎麼辦(訊息丟失)
  • 為了確保能發成功,訊息重複傳送了怎麼辦(訊息重複)
  • 訊息在中介軟體流轉出現異常怎麼處理
  • 訊息消費時候,如果消費流程失敗了怎麼處理,還能不能重新從中介軟體獲取到這條訊息
  • 消費失敗如果還能獲取,那會不會出現失敗情況下,一直重複消費同一條訊息,從而流程卡死
  • 消費失敗如果不能再獲取,那麼我們該怎麼確保這條訊息能再次被處理
  • 重複消費到相同的訊息流程怎麼處理,會不會導致業務異常
  • 那麼我們該怎麼確保消費流程只成功執行一次
  • 對於那些有順序的訊息我們應該怎麼保證傳送和消費的順序一致
  • 訊息太多了,怎麼保證消費指令碼消費速度,以便更得上業務的處理需求,避免訊息無限積壓
  • 我想要傳送的訊息,等上幾秒鐘的時間再消費到,該怎麼做

當然我們對於以上的這些問題,針對業務開發者來說,可以進行提煉,得到以下幾個重點問題:

  • 訊息順序性保證
  • 避免訊息丟失
  • 訊息的重複問題
  • 訊息積壓處理
  • 延遲訊息處理

4. 問題的解決方案

4.1 訊息順序性保證

常規的訊息中介軟體和流處理中介軟體,本身設計一般都能支援順序訊息,但是根據中介軟體本身不同的設計目標,有不同的原理架構,導致我們業務中使用中介軟體的時候,要針對性做不同的處理。

以下幾個常用訊息或流中介軟體的順序訊息設計以及使用中亂序問題分析:

RabbitMQ:

RabbitMQ的單個佇列(queue)自身,可以保證訊息的先進先出,在設計上,RabbitMQ所提供的單個佇列資料是儲存在單個broker節點上的,在開啟映象佇列的情況下,映象的佇列也只是作為訊息副本而存在,服務依然由主佇列提供。這種情況下在單個佇列上進行消費,天然就是順序性的。不過由於單個佇列支援多消費者同時消費,我們在開啟多個消費者消費統一佇列上的資料時候,訊息分散到多個消費者上,在併發高的時候,多個消費者無法保證處理訊息的順序性。

解決方法就是對於需要強制順序的訊息,使用同一個MQ佇列,並且針對單個佇列只開啟一個消費者消費(保證併發處理時候的順序性,多執行緒同理)。由此引發的單個佇列吞吐下降的問題,可以採取kafka的設計思想,針對單一任務開啟一組多個佇列,將需要順序的訊息按照其固定標識(例如:ID)進行路由,分散到這一組佇列中,相同標識的訊息進入到相同的佇列,單個佇列使用單個消費者消費,這樣即可以保證訊息的順序與吞吐。

如圖所示:

得物技術訊息中介軟體應用的常見問題與方案

Kafka:

Kafka是流處理中介軟體,在其設計中,沒有佇列的概念,訊息的收發依賴於Topic,單個topic可以有多個partition(分割槽),這些partition可以分散到多臺broker節點上,並且partition還可以設定副本備份以保證其高可用。

Kafka同一個topic可以有多個消費者,甚至消費組。Kafka中訊息消費一般使用消費組(消費組可以互不干涉的消費同一個topic下的訊息)來進行消費,消費組中可以有多個消費者。同一個消費組消費單個topic下的多個partition時,將由kafka來調節消費組中消費者與partiton的消費進度與均衡。但是有一點是可以保證的:那就是單個partition在同一個消費組中只能被一個消費者消費。

以上的設計理念下,Kafka內部保證在同一個partition中的訊息是順序的,不保證topic下的訊息的順序性。Kafka的訊息生產者傳送訊息的時候,是可以選擇將訊息傳送到哪個partition中的,我們只要將需要順序處理的訊息,傳送到topic下相同的partition,即可保證訊息消費的順序性。(多執行緒語言使用單個消費者,多執行緒處理資料時,需要自己去保證處理的順序,這裡略過)。

得物技術訊息中介軟體應用的常見問題與方案

RocketMQ:

RocketMQ的一些基本概念和原理,可以通過阿里雲的官網做一些瞭解:什麼是訊息佇列RocketMQ版? - 訊息佇列RocketMQ版 - 阿里雲

RocketMQ的訊息收發也是基於Topic的,Topic下有多個 Queue, 分佈在一個或多個 Broker 上,用來保證訊息的高效能收發( 與Kafka的Topic-Partition機制 有些類似,但內部實現原理並不相同 )。

RocketMQ支援區域性順序訊息消費,也就是保證同一個訊息佇列上的訊息順序消費。不支援訊息全域性順序消費,如果要實現某一個主題的全域性順序訊息消費,可以將該主題的佇列數量設定為1,犧牲高可用性。具體圖解可以參考阿里雲文件: 順序訊息2.0 - 訊息佇列RocketMQ版 - 阿里雲

4.2 避免訊息丟失

訊息丟失需要分為三部分來看:訊息生產者傳送訊息到訊息中介軟體的過程不發生訊息丟失,訊息在訊息中介軟體中從接受儲存到被消費的過程中訊息不丟失, 訊息消費的過程中保證能消費到中介軟體傳送的訊息而不會丟失。

生產者傳送訊息不丟失:

訊息中介軟體一般都有訊息傳送確認機制(ACK), 對於客戶端來說,只要配置好訊息傳送需要ACK確認,就可以根據返回的結果來判斷訊息是否成功傳送到中介軟體中。這一步通常與中介軟體的訊息接受儲存流程設計有關係。根據中介軟體的設計,我們通常採取的措施如下:

  • 開啟MQ的ACK(或confirm)機制,直接獲知訊息傳送結果
  • 開啟訊息佇列的持久化機制(落盤,如果需要特殊設定的話)
  • 中介軟體本身做好高可用部署
  • 訊息傳送失敗補償設計(重試等)

在具體的業務設計中,如果訊息傳送失敗,我們可以根據業務重要程度,做相應的補償,例如:

  1. 訊息失敗重試機制(傳送失敗,繼續重發,可以設定重試上限)
  2. 如果依然失敗,根據訊息重要性,選擇降級方案:直接丟棄或者降級到其他中介軟體或載體(同時需要相應的降級補償推送或消費設計)

訊息中介軟體訊息不丟失:

數訊息中介軟體的訊息接收儲存機制各不相同,但是會根據其特性設計,最大限度保證訊息不會丟失:

RabbitMQ訊息接收與儲存:

  • RabbitMQ 訊息傳送可以開啟傳送者confirm模式,所有訊息是否傳送成功都會通知傳送者
  • 需要開啟佇列訊息持久化保證訊息落盤
  • RabbitMQ通過映象佇列來保證訊息佇列的高可用,但是映象佇列只有Master提供服務,其他slave只提供備份服務。
  • master當機會從slave中選擇一個成為新的master提供服務
  • master的生產與消費的最新狀態都會廣播到slave

RocketMQ訊息接受與儲存:

  • RocketMQ普通訊息傳送有三種方式:同步(Sync)傳送、非同步(Async)傳送和單向(Oneway)傳送,其區別與準確性保證可以參看 傳送普通訊息(三種方式) - 訊息佇列RocketMQ版 - 阿里雲
  • 具體的RocketMQ內部設計的HA機制是主從同步機制,訊息傳送到Topic下並具體訊息佇列的Master Broker中後,會將訊息同步到Slave。
  • 只有Master Broker才可以接收生產者傳送的訊息。而消費者,可以從Master也可以從Slave拉取並消費訊息。

Kafka在訊息接受到儲存所做的設計有:

  • 分割槽副本方式的設計保證訊息的高可用,在建立topic的時候都可以設定分割槽副本的數量
  • 生產者可以選擇接收不同型別的確認(ACK),比如在訊息被完全提交時候(寫入所有同步副本)的確認,或者在訊息被寫入首領副本時的確認,或者在訊息被髮送到網路時確認
  • Kafka的訊息,寫入分割槽的時候僅僅是儲存在某幾個分割槽副本檔案系統記憶體中,並不是直接刷到磁碟了,因此當機時候,單個副本仍然可能丟失資料。Kafka不能保證單個分割槽副本的資料一定不丟失,而是靠分割槽副本機制來確保訊息的完善性(分佈到不同的broker上)

積壓訊息儲存時效問題

  • Kafka對於topic下的資料,有容量上限、時間上限兩種訊息儲存上限規則,觸發其中任何一個規則,都會刪除淘汰之前的訊息。這個尤其需要注意。
  • RocketMQ,訊息在伺服器儲存時間也有上限,達到上限的訊息將會被刪除。也需要做相應的考量。
  • 受持久化磁碟容量的影響,儲存積壓的資料不能超過磁碟的上限
  • 如果業務消費有異常,需要給足充足的冗餘量,避免因為消費不及時而丟失資料。

消費者消費訊息不丟失:

  • 訊息消費時候,也要開啟相應的ACK機制,訊息消費成功即ACK(對於Kafka就是更新消費的offset)
  • 對於RocketMQ這種有訊息重新消費設計的,需要設定最大消費次數,嘗試失敗的訊息重複消費

訊息ACK帶來兩個問題

  • 訊息消費失敗如果不能ACK可能會導致訊息消費無限阻塞在某條訊息處
  • 訊息失敗重新消費導致訊息消費重複

無限阻塞的問題,可以參考RocketMQ消費失敗的重試機制,對訊息重試做一定的設計:

  1. 在訊息體上設計重試次數的屬性,消費失敗的訊息增加重試次數後重新傳送到中介軟體,等待下一次消費,本次消費成功發回訊息直接ACK
  2. 訊息重試次數達到上限之後,如果仍不能成功,則啟用降級方案,將訊息儲存到異常資訊持久化載體如DB中
  3. 手動或者定時任務補償處理失敗的訊息

訊息重複消費問題參考下一個小節。

4.3 訊息的重複問題(消費冪等)

在分析常用中介軟體的時候,我們往往會發現,中介軟體設計者將這個問題的處理,下放給中介軟體使用者,也就是業務開發者了。誠然,業務消費處理的邏輯比訊息生產者複雜的多。生產者只需要保證將訊息成功傳送到中介軟體即可,而消費者需要在消費指令碼中處理各種複雜的業務邏輯。

解決訊息重複消費的問題,核心是使用唯一標識,來標記某條訊息是否已經處理過。 具體方案可選的則有很多,比如:

  • 使用資料庫自增主鍵,或者唯一鍵來保證資料不會重複變動
  • 使用中間狀態,以及狀態變動有序性來判斷業務是否以已經被處理
  • 利用一張日誌表來記錄已經處理成功的訊息的 ID,如果新到的訊息 ID 已經在日誌表中,那麼就不再處理這條訊息
  • 或者訊息唯一標識,在Redis等NoSQL中維護一個處理快取,判斷是否已經處理過
  • 如果消費者業務流程比較長,則需要開發者自己保證整個業務消費邏輯中資料處理的事務性

4.4 訊息積壓處理

通常我們在引入訊息中介軟體的時候,已經會評估與測試訊息消費的生產與消費速率,儘量使其達到平衡。但業務也有一些不可預知的突發情況,可能會造成訊息的大量積壓。在這個時候,我們可以採取如下的方式,來做處理:

臨時緊急擴容

  1. 通過增加消費指令碼的方式,提升消費速率,如果下游沒有限制的話,可以很快的減少訊息積壓
  2. 如果消費者下游資料處理能力有限,我們可以考慮建立臨時佇列,通過臨時指令碼,將訊息快速轉移到臨時佇列,優先保證線上業務能順利貫通,而後開啟更多的消費指令碼處理積壓的資料。(順序訊息需要額外處理,並保證最終處理的順序)
  3. 優化消費指令碼的處理速度,突破下游限制,如果有可能,可以考慮批量處理,下游擴容等方式。

訊息積壓預防

  • 做好業務設計與降級,避免產生無效訊息佔用資源
  • 根據訊息積壓程度,動態增減消費者數量,減少訊息積壓
  • 做好訊息積壓處理緊急預案,異常情況根據預案設計,迅速針對處理

4.5 延遲訊息處理

延遲訊息這一項功能,在部分MQ中介軟體中有實現。延時訊息和定時訊息其實可以互相轉換。

RocketMQ:\
RocketMQ定時訊息不支援任意的時間精度(出於效能考量)。只支援特定級別的延遲訊息。訊息延遲級別在broker端通過messageDelayLevel配置。其內部對每一個延遲級別建立對應的訊息消費佇列,然後建立對應延遲級別的定時任務,從訊息消費佇列中將訊息拉取並恢復訊息的原主題和原訊息消費佇列。

RabbitMQ:

RabbitMQ實現延遲訊息通常有兩個方案:一是建立一個訊息延遲死信佇列,搭配一個死信轉發佇列來實現消費延時。但是該方式如果前一個訊息沒達到TTL時間,後一個訊息即便達到了,也不會被轉發到轉發佇列中;另一個是使用延時Exchange外掛(rabbitmq_delayed_message_exchange),訊息在達到TTL之後才會轉發到對應的佇列中並被消費。

Kafka本身不支援延時訊息或定時訊息, 想要實現訊息的延時,需要使用其他的方案。

藉助資料庫與定時任務實現延時訊息:

常用資料庫的索引結構都支援資料的順序索引。藉助資料庫可以很方便的實現任意時間訊息的延時消費。使用一張表儲存資料的消費時間,開啟定時任務,在滿足條件之後將該訊息提取出來,後續轉發到順序佇列去處理或者直接處理都可以(已處理需要做標記,後續不再出現),但是直接處理需要考慮吞吐量和併發重複性等問題。不如單個指令碼轉發到普通佇列去處理方便。資料庫支援的定時任務訊息積壓是可控的,但是吞吐量會有侷限。

藉助Reids的有序列表實現延時訊息:

Reids的有序列表zset結構,可以實現延時訊息。將訊息的消費時間作為分值,把訊息新增到zset中。使用 zrangebyscore 命令消費訊息

#命令格式 zrangebysocre key min max withscores limit 0 1 消費最早的一條訊息 # min max分別表示開始的分值與結束的分值區間,分別使用 0和當前時間戳,可以查出達到消費時間的訊息 # withscores 表示查詢的資料要帶分值。 limit 後面 就是查詢的起始 offset 和數量 zrangebyscore key 0 {當前時間戳} withscores limit 0 1

當然,這個方案也有侷限性,首先,redis必須配置持久化防止訊息丟失(如果配置不合理不能100%保證,但是每個命令都持久化會造成效能下降,需要權衡);其次,如果延時訊息過多會造成訊息的積壓形成大key;再次,需要自己做重複消費和消費失敗的平衡處理(當然有可能,還是建議開啟單個消費程式將延時訊息轉移到普通佇列去消費)。

基於時間輪的任務排程:

在很多軟體中,都有基於時間輪實現定時任務的實現,使用時間輪以及多級時間輪可以實現延時任務排程。如果我們希望自己實現延時任務佇列,可以考慮使用此演算法來實現任務的排程,但是需要自己根據具體的需求去設計支援任務的延時上限以及排程的時間粒度(多層級)。時間輪演算法我這裡就先不講解了,感興趣的可以自己去搜尋瞭解。

5. 總結

通過以上幾個小節的介紹,相信各位已經能很自然的理解 訊息佇列 非同步解耦 的功能與核心思想,並且對如何使用MQ來架構自己的業務有了一定的認知。大多數MQ使用中的問題,只是要求我們多思考,將細節思慮周到,以保證業務的高可用。甚至,我們還可以在這幾個解決方案中提煉一些核心出來,以便在業務中參照類似的思想,優化我們的業務。 比如 訊息順序性保證 其核心是順序訊息生產者傳送到唯一分割槽,再維持固定分割槽的單消費者順序消費;避免訊息丟失的核心是每個步驟的確認與降級機制;消費冪等的核心是唯一性標識與步進狀態;訊息積壓處理的核心是快速響應應急預案;延遲訊息的核心是訊息排序,優化點是效能提升。

科學的方法有歸納和演繹,學習問題處理方案的過程中,提煉出相應的核心思想,並在使用中演繹,將這些歸納總結的知識點,再應用到業務中去,更加得心應手的處理相應的事務,構建出高可用的業務架構,這才是我們最需要做到的。\

參考:

  • 訊息佇列RocketMQ版 - 幫助中心 - 阿里雲
  • 丁威,周繼鋒 .《RocketMQ技術內幕——RocketMQ架構設計與實現原理》. 機械工業出版社
  • Neha Narkhede,Gwen Shapira,Todd Palino .《Kafka權威指南》. 人民郵電出版社

文/LISUXING

關注得物技術,做最潮技術人!