訊息架構的設計難題以及應對之道

JAVA日知錄發表於2021-03-15

概述

在微服務開發中我們經常會引入訊息中介軟體實現業務解耦,執行非同步操作, 現在讓我們來看看使用訊息中介軟體的好處和弊端。

首先需要肯定是使用訊息元件有很多好處,其中最核心的三個是:解耦、非同步、削峰。

  • 解耦:客戶端只要講請求傳送給特定的通道即可,不需要感知接收請求例項的情況。
  • 非同步:將訊息寫入訊息佇列,非必要的業務邏輯以非同步的方式執行,加快響應速度。
  • 削峰:訊息中介軟體在訊息被消費之前一直快取訊息,訊息處理端可以按照自己處理的併發量從訊息佇列中慢慢處理訊息,不會一瞬間壓垮業務。

當然訊息中介軟體並不是銀彈,引入訊息機制後也會有如下一些弊端:

  • 潛在的效能瓶頸:訊息代理可能會存在效能瓶頸。幸運的是目前主流的訊息中介軟體都支援高度的橫向擴充套件。
  • 潛在的單點故障:訊息代理的高可用性至關重要,否則系統整體的可靠性將受到影響,幸運的是大多數訊息中介軟體都是高可用的。
  • 額外的操作複雜性:訊息系統是一個必須獨立安裝、配置和運維的系統元件,增加了運維的複雜度。

這些弊端我們藉助訊息中介軟體本身提供的擴充套件、高可用能力可以解決,但是要真正用好訊息中介軟體我們還需要關注可能會遇到的一些設計難題。

處理併發和順序訊息

在生產環境中為了提高訊息處理的能力以及應用程式的吞吐量,一般會將消費者部署多個例項節點。那麼帶來的挑戰就是如何確保每個訊息只被處理一次,並且是按照他們的傳送順序來處理的。

例如:假設有3個相同的接收方例項從同一個點對點通道讀取訊息,傳送方按順序釋出了 Order CreatedOrder UpdatedOrder Cancelled 這3個事件訊息。簡單的訊息實現可能就會同事講每個訊息給不同的接收方。若由於網路問題導致延遲,訊息可能沒有按照他們發出時的順序被處理,這將導致奇怪的行為,服務例項可能在另一個伺服器處理 Order Created 訊息之前處理 Order Cancelled訊息。

Kafka 使用的解決方案是使用分片(分割槽)通道。整體解決方案分為三個部分:

  1. 一個主題通道由多個分片組成,每個分片的行為類似一個通道。
  2. 傳送方在訊息頭部指定分片鍵如orderId,Kafka使用分片鍵將訊息分配給特定的分片。
  3. 將接收方的多個例項組合在一起,並將他們視為相同的邏輯接收方(消費者組)。kafka將每個分片分配給單個接收器,它在接收方啟動和關閉時重新分配分片。

image.png

如上圖所示,每個Order事件訊息都將orderId作為其分片鍵。特定訂單的每個事件都發布到同一個分片。而且該分片中的訊息始終由同一個接收方例項讀取,因此這樣就能夠保證按順序處理這些訊息。

處理重複訊息

引入訊息架構必須要解決的另一個挑戰是處理重複訊息。在理想情況下,訊息代理應該只傳遞一次訊息,但保證訊息有且僅有一次的訊息傳遞的成本通常很高。相反,很多訊息元件承諾至少保證成功傳遞一次訊息。

在正常情況下,訊息元件只會傳遞一次訊息。但是當客戶端、網路或訊息元件故障可能導致訊息被多次傳遞。假設客戶端在處理訊息後傳送確認訊息前,他的資料庫崩潰了,這時訊息元件將再次傳送未確認的訊息,在資料庫重新啟動時向該客戶端傳送。

處理重複訊息有以下兩種不同的方法:

  • 編寫冪等訊息處理程式
  • 跟蹤訊息並丟棄重複項

編寫冪等訊息處理器

如果應用程式處理訊息的邏輯是滿足冪等的,那麼重複訊息就是無害的。程式的冪等性是指,即使這個應用被相同輸入引數多次重複呼叫時,也不會產生額外的效果。例如:取消一個已經取消的訂單,就是一個冪等性操作。同樣,建立一個已經存在的訂單操作也必是這樣。滿足冪等的訊息處理程式可以被放心的執行多次,只要訊息元件在傳遞訊息時保持相同的訊息順序。

但是不幸的是,應用程式通常不是冪等的。或者你現在正在使用的訊息元件在重新傳遞訊息時不會保留排序。重複或無序訊息可能會導致錯誤。在這種情況下,你需要編寫跟蹤訊息並丟棄重複訊息的訊息處理程式。

跟蹤訊息並丟棄重複訊息

考慮一個授權消費者信用卡的訊息處理程式。它必須為每個訂單僅執行一次信用卡授權操作。這段應用程式每次呼叫時都會產生不同的效果。如果重複訊息導致訊息處理程式多次執行該邏輯,則應用程式的行為將不正確。執行此類應用程式邏輯的訊息處理程式必須通過檢測和丟棄重複訊息而讓它成為冪等的。

一個簡單的解決方案是訊息接收方使用 message id 跟蹤他已處理的訊息並丟棄任何重複項。例如,在資料庫表中儲存它消費的每條訊息的 message id。

image.png

當接收方處理訊息時,它將訊息的 message id 作為建立和變更業務實體的事務的一部分記錄在資料表裡。如上圖所示,接收方將包含message id 的行插入 PROCESSED_MESSAGE表。如果訊息是重複的,則INSERT將失敗,接收方可以選擇丟棄該訊息。

另一個解決方案是訊息處理程式在應用程式表,而不是專門表中記錄 message id。當時用具有受限事務模型的NoSQL資料庫時,此方法特別有用,因為 NoSQL資料庫通常不支援將針對兩個表的更新作為資料庫事務。

處理事務性訊息

服務通常需要在更新資料庫的事務中釋出訊息,資料庫更新和訊息傳送都必須在事務中進行,否則服務可能會更新資料庫然後在傳送訊息之前崩潰。

如果服務不以原子方式執行者兩個操作,則類似的故障可能使系統處於不一致狀態。

接下來我們看一下常用的保證事務訊息的兩種解決方案,最後再看看現代訊息元件RocketMQ的事務性訊息解決方案。

使用資料庫表作為訊息佇列

如果你的應用程式正在使用關係型資料庫,要保證資料的更新和訊息傳送之間的事務可以直接使用事務性發件箱模式,Transactional Outbox

image.png

此模式使用資料庫表作為臨時訊息佇列。如上圖所示,傳送訊息的服務有個OUTBOX資料表,在進行INSERT、UPDATE、DELETE 業務操作時也會給OUTBOX資料表INSERT一條訊息記錄,這樣可以保證原子性,因為這是基於本地的ACID事務。

OUTBOX表充當臨時訊息佇列,然後我們在引入一個訊息中繼(MessageRelay)的服務,由他從OUTBOX表中讀取資料併發布訊息到訊息元件。

訊息中繼的實現可以很簡單,只需要通過定時任務定期從OUTBOX表中拉取最新未釋出的資料,獲取到資料後將資料傳送給訊息元件,最後將完成傳送的訊息從OUTBOX表中刪除即可。

使用事務日誌釋出事件

另外一種保證事務性訊息的方式是基於資料庫的事務日誌,也就是所謂的資料變更捕獲,Change Data Capture,簡稱CDC。

一般資料庫在資料發生變更的時候都會記錄事務日誌(Transaction Log),比如MySQL的binlog。事務日誌可以簡單的理解成資料庫本地的一個檔案佇列,它主要記錄按時間順序發生的資料庫表變更記錄。

這裡我們利用alibaba開源的元件canal結合MySQL來說明下這種模式的工作原理。

更多操作說明可以參考官方文件:https://github.com/alibaba/canal

canal工作原理

  • canal 模擬 MySQL slave 的互動協議,把自己偽裝成一個MySQL的 slave節點 ,向 MySQL master 傳送dump 協議;
  • MySQL master 收到 dump 請求,開始推送 binary log 給 slave (即 canal );
  • canal 解析 binary log 物件(原始為 byte 流),然後可以將解析後的資料直接傳送給訊息元件。

RocketMQ事務訊息解決方案

Apache RocketMQ在4.3.0版中已經支援分散式事務訊息,RocketMQ採用了2PC的思想來實現了提交事務訊息,同時增加一個補償邏輯來處理二階段超時或者失敗的訊息,如下圖所示。

image.png

RocketMQ實現事務訊息主要分為兩個階段:正常事務的傳送及提交、事務資訊的補償流程。

整體流程為:

  • 正常事務傳送與提交階段
    1、生產者傳送一個半訊息給MQServer(半訊息是指消費者暫時不能消費的訊息)
    2、服務端響應訊息寫入結果,半訊息傳送成功
    3、開始執行本地事務
    4、根據本地事務的執行狀態執行Commit或者Rollback操作
  • 事務資訊的補償流程
    1、如果MQServer長時間沒收到本地事務的執行狀態會向生產者發起一個確認回查的操作請求
    2、生產者收到確認回查請求後,檢查本地事務的執行狀態
    3、根據檢查後的結果執行Commit或者Rollback操作
    補償階段主要是用於解決生產者在傳送Commit或者Rollback操作時發生超時或失敗的情況。

在生產者使用RocketMQ傳送事務訊息的時候我們也會借鑑第一種方案即自建一張事務日誌表,然後在執行本地事務的時候同時生成一條事務日誌記錄,讓本地事務與日誌事務在同一個方法中,同時新增 @Transactional 註解,保證兩個操作事務是一個原子操作。這樣如果事務日誌表中有這個本地事務的資訊,那就代表本地事務執行成功,需要Commit,相反如果沒有對應的事務日誌,則表示沒執行成功,需要Rollback。

相關文章