1. 程式人生 > >設計訊息中介軟體時我關心什麼?(解密電商資料一致性與完整性實現,含PPT)

設計訊息中介軟體時我關心什麼?(解密電商資料一致性與完整性實現,含PPT)

導讀:應對高可用及極端峰值,每個技術團隊都有自己的優秀經驗,但是這些方法遠沒有得到體系化的討論。高可用架構在 6 月 25 日舉辦了『高壓下的架構演進』專題活動,進行了閉門私董會研討及對外開放的四個專題的演講,期望能促進業界對應對峰值的方法及工具的討論,本文是去哪兒網餘昭輝介紹設計電商訊息中介軟體的設計經驗。

作者:餘昭輝——去哪兒網 基礎架構部架構師,2011 年加入去哪兒網,經歷過去哪兒網從小到壯大,服務拆分的過程。現負責去哪兒網基礎架構部,參與設計和開發去哪兒大大小小各種基礎元件。本人對網際網路電商中介軟體,併發和非同步程式設計尤為感興趣,是一個自我標榜 Clean Coder。熱衷於與同行技術交流。

我來自去哪兒的基礎架構部,我們部門負責公司的公共元件和基礎服務,包括敏感資訊儲存、發號器、身份證認證、監控中心、任務排程、Redis 等。

電商資料一致性與完整性實現

今天主要給大家分享一下訊息佇列基礎元件的設計。

我們是 2012 年初開始自研訊息佇列和訊息中介軟體的,當時也是契合公司背景,原來公司有一些龐大的單模組系統,如機票交易系統和酒店交易系統等。為了對系統進行拆分,面臨著系統拆分之後事務處理的問題,於是自研了訊息中介軟體。

現在去哪兒網基本所有交易環節都通過訊息的方式流轉,使用了一種訊息驅動的架構,像訂單的流轉、支付等,訊息中介軟體已經成為核心基礎設施,對交易系統非常關鍵。最初設計訊息中介軟體是為了滿足交易場景,後來大家覺得 API 使用非常方便,現在其他業務包括部分搜尋等等功能也切到這個上面來。

截止目前,除了部分搜尋場景是 AMQ,公司其他業務的都使用了自研的訊息中介軟體,一些基本資料如下:

  • 承載公司 1 萬多個 subject

  • 平均接收訊息量 QPS 12 萬+

  • 峰值 QPS 50 萬

  • 最多的一個訊息 subject 有 180 個消費組來消費

訊息中介軟體模型說簡單也很簡單,最小單元就是一條訊息,所以伸縮、擴充套件非常容易,只要根據訊息進行 hash。當中間件承受不住壓力的候,擴充套件是非常簡單的。另外一方面說它複雜也很複雜,訊息中介軟體作為一個公司的基礎元件,如果它出問題就是一個很嚴重的事情。訊息中介軟體出一次故障,就是 6 ~ 7 個部門報 P1 故障。

這便是它的複雜所在,如何保證它的正常執行,那麼在介紹內容之前,先說明一下上下文:今天講的僅僅是適用交易環節的訊息中介軟體,跟通常所說的社交領域的訊息中介軟體有很大的不同。

在交易環節,需要考慮 3 個方面 :

  1. 不能丟訊息。丟訊息意味著掉單,意味著支付成功但是沒給人家出票,這是不能接受的。

  2. 穩定。訊息中介軟體一旦出問題,交易不能進行,也是嚴重的故障。

  3. 效能

在電商的場景前面兩條要高於效能要求,也是今天要重點討論的部分。

典型的訊息中介軟體包含 3 部分 :producer(釋出者)、broker(訊息中介軟體)、consumer(消費者),是一個比較簡單的模型,下圖展示了把訊息傳送給 consumer 的全過程。 

電商資料一致性與完整性實現

Producer 釋出端設計

Producer 訊息釋出端主要關注一致性、容災、效能。

分散式事務一致性的難題

設計訊息中介軟體

上圖是一個訂票的訂單服務,生成的新訂單。如果訂單持久化成功(上面的紅框 ),訊息傳送失敗( 下面的紅框 )。那麼使用者看到下單成功,假設代理商服務訂閱這個訊息是否給使用者出票,那麼現在的情況就是票沒出來,這會引起使用者投訴。但是如果先發訊息,然後再持久化訂單,那可能就是訂單出票了,但其實這個訂單還沒下呢,這就會造成公司的損失。

這種一致性問題怎麼解決?

大家可能會想到分散式事務,比如 2PC(Two phase commit)。業內已經有很多文章介紹了分散式事務的利弊,它的成本還是比較高,在此不做討論,下面主要介紹電商系統中應用比較多的另外一種方法。

資料庫的一致性

先來看一下資料庫中的事務

設計訊息中介軟體

在一個 DB 例項中,比如 3306 這個,使用同一個連線,對多個庫的操作是可以放在同一個事務裡的。這樣是可以保證資料的一致性,這是由資料庫決定。比如圖中的業務 DB1,業務 DB2 和一個訊息 DB 都在同一個例項裡,是可以放在同一個事務裡的。

業務資料的一致性保證

有了資料庫這層保證,就可以用這種方式來實現業務操作和訊息傳送了。

先將訂單持久化在同一個事務裡,共享訂單操作的資料庫連線,在這個連線裡將訊息持久化到同一個例項裡的訊息庫裡,然後在事務提交之後將訊息傳送到訊息的 server。如果事務回滾了,訊息就不會發送出去了。

設計訊息中介軟體

詳細看一下流程圖 :

電商資料

  1. 開啟事務。

  2. 業務操作,比如說訂單進行持久化等等動作。

  3. 生成訊息,並存儲,這和業務操作是在同一個事務裡。

  4. 事務提交。

  5. 訊息真正發到出去。訊息如果傳送成功了,會將訊息表裡的訊息刪除,而此時如果訊息傳送失敗了,後臺有一個任務會把訊息表裡面傳送失敗的訊息重新進行傳送,這樣最終達到一致性,保證業務操作成功了,訊息一定能發出去。

(小編:更多瞭解分散式事務的實現,可參看文末推薦閱讀的文章)

現在看看這種模型的優缺點。這種模型 API 非常簡單,業務開發只需要使用 sendMessage 這個簡單的 API,不需要關心事務等。同時運維也非常簡單,我們的做法是公司的 DBA 給所有 DB 例項上預初始化一個訊息庫,業務根本不用關心,對業務完全是透明,API 把這些封裝在底下,使用起來還是非常簡單。 但是這樣有另外一個問題,就是儲存成本。本來只有業務操作訪問 DB,然後還要持久化訊息。原來承受一個 QPS 現在可能只能承受一半了,所以對資料庫操作還是略重一些

另外,有的場景中,可能不僅做資料庫操作,還呼叫了 RPC。這樣的動作是不能放在一個數據庫事務裡的,所以對於這種場景就不能滿足了。現在遇到這種情況只有把 RPC 這種操作拆出去了。所以這種模型的優點就是使用方便,但是有些限制。

還有另外一種實現一致性的方法。

電商資料

  1. 發新的訊息,直接發給 broker,這個訊息發給 broker 並不立即將訊息投遞出去。

  2. 做本地的操作

  3. 再調 broker 的介面,這一步真正把訊息傳送出去。如果這個時候,即使第二步操作成功,第三步傳送失敗了,第一步傳送給 broker 的訊息就是一個未決狀態。

  4. broker 反過來詢問 producer,那條訊息是發還是不發出去呢?這種模型就不需要一個將一個訊息庫放在業務庫同例項了,比較靈活,成本也更低些。但是業務使用的複雜度可能要高一些,需要提供一個介面供 broker 反查。

容錯

講完了一致性,再來看看容錯。broker 會有不同的叢集,producer 發訊息有一個優先順序,預設訊息優先發到本機房叢集,本機房宕掉或者其他什麼原因不可用再向別的機房進行傳送。本機房出故障自動不向本機房傳送,自動熔斷故障機房。後臺系統裡面可以按照 subject 指定路由到特定的 broker 叢集。

Broker 訊息中介軟體設計

訊息中介軟體要支援非常多的 subject,全公司都在使用訊息中介軟體,各業務開發水平也參差不齊,如果有的系統弄了一個死迴圈,瘋狂的發訊息就會給系統帶來不可控的壓力,所以中間層需要做好隔離。其次,業務使用訊息中介軟體可能會遇到各種各樣的問題,需要輔助工具進行診斷。最後還需要全面的監控能力。 

隔離

隔離包括配額和排程。Producer 給 broker 發訊息的時候,每一個 subject 需要給它多少配額,QPS 一旦高於這個配額,做什麼處理?

這個地方我們也踩了一個小坑。假設給每個 subject 3000 QPS 限制,最初 producer 端的設計沒有考慮配額這種情況,配額生效之後,達到 3000 QPS broker 開始拒絕訊息,也就是返回異常。 Producer 一般的設計遇到這種異常時候就不斷地重試,這種一拒絕 producer 就不斷地重試,雪上加霜,頻寬都要打滿了。

公司業務部門比較多,因為不能要求所有 producer 都立即配合升級,於是我們做了改進,producer 達到 QPS 不是立即拒絕傳送過來的訊息,而是拖一會兒,這樣來避免將中間層拖垮。當然最好的方式是 producer 進行配合,當 broker 超配額,producer 降低傳送速率。

還有一種情況,我們有很多個 subject,有 180 個 consumer group 進行訂閱,如果 QPS 達到 100,就是 1.8 萬,如果 QPS 達到 1000,就是 18 萬,180 倍的增長,所以怎麼與其它 subject 進行隔離很重要。 

我們第一版做的很簡單,用執行緒池隔離,每個 subject 分配一個執行緒池,這種做法隔離效果是很好,但是 subject 不斷地增長,資源就不夠用了。這個問題抽象來看就像作業系統的 Scheduler 執行緒排程器,每一個佇列可以想象成 OS 裡的執行緒,然後系統用一些來發送這些佇列,這些執行緒就對應 CPU 的 core。我們就模仿 Linux 的 Scheduler 實現了個排程,可能實現水平關係,但是最後測試發現效果很差,量一大起來,佇列就堵住了,完全發不出去訊息。

最後我們看 actor 這種模型,系統裡可以跑成千上萬的 actor,它肯定也有一個排程器,最後就模仿 akka 的排程器,它叫 dispatcher,實現了排程的策略。

可治理

像剛才配額都是可以動態調整,不能訊息量突然上來了,重啟訊息系統來調整。還有訊息可靠級別,當訊息上線時,我們要關心訊息 QPS 能達到,能容忍多少丟失?如果這個時候訊息中介軟體出問題了,我們就可以根據上線時可靠級別給有的訊息降級。

降級也有很多種策略,比如僅僅給投遞一次,如果中介軟體出問題,這種訊息就投遞一次算了,不管你是否消費成功,都不給你重發。還有重發次數,比如有的消費者那邊有問題,消費不成功,比如消費格式變了,重發一天也消費不了,就可以實時的去調整訊息的重發次數。還有可以按多少比例給它發訊息,比如說 50%,那麼 50% 的訊息就給拋棄。

還有日誌,為了好查問題,每條訊息都有軌跡日誌,出問題的時候就可以有選擇的是否儲存這些日誌了。一個公司不可能所有訊息都要求 100% 可靠。比如訂單支付的訊息級別是最高的,但是搜尋,比如說現在有報價,代理商幫旗下所有的酒店價格變了一下,關心它的系統就要受到價格的更新,這是通過訊息廣播出去的。這個訊息,它的變動是比較頻繁,QPS 也很高,丟了一兩條訊息,可能又被後面的訊息覆蓋了,它的可靠級別就比訂單的級別要低,這個時候遇到問題,為了保護訂單訊息,肯定首先對它進行降級。

輔助工具

訊息的傳送投遞軌跡視覺化,訊息回溯、訊息補發等等。發一條訊息過來,這條訊息什麼時候接收到?什麼時候進行投遞,投遞到哪些消費者是要有視覺化,這樣便於使用者查詢問題。

訊息回溯。比如消費者把訊息的內容理解錯了,幾號到幾號之間所有的訊息都要進行重新發送,這樣就要回溯這段時間的訊息。

訊息補發。漏發的訊息,把訊息重新補發一下,使用者可以上傳一個檔案,將這些檔案裡的內容解析成訊息然後傳送。

顯示訊息的傳送和消費的關係。系統發出了哪些 topic 的訊息,系統訂閱了哪些 topic 的訊息,有哪些消費有哪些訂閱了,這些都非常重要。

監控

監控分為兩塊。一個是指標監控,比如像 QPS 監控,耗時等。可以細化到 subject、consumer 等粒度。第二個是鏈路監控、全鏈路跟蹤,這是另外一個產品 QTracer 做的,可以根據訊息 id 來查這個訊息所關聯的鏈路,來看看這個訊息的情況。

Consumer 消費端設計

上下線控制

上下線的策略,如果消費端應用還沒有啟動成功的時候,訊息就已經過來,這是不能接受的。我們使用了一個和 nginx 差不多的方法,利用一個 healthcheck.html,如果有這個檔案,就把消費者上線。釋出系統將應用釋出之後,會檢查應用是否 ready,ready 之後就會 touch 一下這個檔案,然後 consumer 就上線了。另外就是可以手動遮蔽消費端,比如一個消費組有多臺機器,可以遮蔽其中幾臺。

冪等

冪等分兩種實現。有的業務是可以處理冪等的,藉助訊息裡的業務欄位然後根據業務場景。但有的業務可能不太好實現冪等,我們的客戶單預設提供了冪等的措施,比如基於 Redis、MySQL 等。

關於順序

訊息中介軟體是不嚴格保證順序的,只是儘量保證有序,一般情況下先發送的訊息先到,但並不做出這種承諾。要保證順序對實現方式和成本都是不小的挑戰。

使用方一般怎麼來保證順序呢?

一種是狀態機。涉及交易的系統一般都有狀態機。比如訂單流轉,假設現在訂單狀態是待支付,業務收到支付成功的訊息,訂單就流轉成支付成功,這個時候收到了訂單完成或者出票成功這樣的訊息,這個訊息不是對應的當前狀態,都會進行拒絕,拒絕後訊息的 server 稍後會重發。 

另外一種方法是 producer 在訊息裡面攜帶一個版本號。Consumer 收到以後會和自己當前的版本號進行比較,接到訊息的版本號如果小於資料庫的版本號,這個訊息就不消費了,直接吞掉,這裡要注意,這樣的訊息就不是拒絕了。

Q & A

Q:訊息入庫本地庫,後臺應用掃描資料庫重發訊息,會不會導致訊息重發?

餘昭輝:有幾層保證。首先,訊息一旦傳送成功,就把訊息給刪除了。而後臺應用掃描也是掃描指定時間之前的訊息,但這可能還是不能完全杜絕重發,比如在刪除之前,被掃描到了,就會導致這個問題。我們在 server 端會根據訊息的 id 進行一個去重,不過去重也是有個限制的,也就是隻保證多長時間內的訊息不重複,而不是永久。如果有的業務覺得這還不夠,就要自己去實現冪等了。

Q:看你們實現了跨機房,如果中介軟體在兩個機房,其中一個機房出問題,會不會有影響,機房做容災?

餘昭輝:這個是沒做的,如果一個機房出現不可恢復的故障,需要人工進行恢復。訊息收到之後,首先落本地庫,還會儲存一份到 HBase。你剛才說機房宕掉的,那些儲存把訊息恢復回來,沒有做自動容災。

Q:冪等需要業務上做本地支援?

餘昭輝:對。如果業務完全不能接受重複訊息,就必須實現冪等。

Q:訊息佇列是基於 Kafka 嗎?如果自研底層是怎麼樣實現的

餘昭輝:這塊是自己研發實現的,訊息儲存直接用 MySQL,開發語言用 Java,去哪兒網主流的語言就是 Java,公司裡其他語言很少。關於儲存的分割槽,因為模型非常簡單,分割槽就是用訊息的 id,hash 進行分割槽。

Q:broker 做服務中心,如果 broker 重啟,訊息持久化的情況怎麼處理? 

餘昭輝:brocker 進行重啟,先參看最開始的那個模型。訊息發到 broker,broker 如果回成功了,說明訊息一定落地了,只有落地成功了,broker 才會回成功。返回成功,producer 就可以把本地訊息刪除掉。如果你發一條訊息正好碰到服務重啟,儲存沒落地,broker 肯定不會回訊息,訊息就在本地庫裡面,稍後,後臺應用又會把訊息掃出來重發。

Q:從第一版到現在有沒有對架構方面的考量或者重新調整設計? 

餘昭輝:架構上面變動不多,主要是裡面細節在不斷調整。比如說最初的時候,網路這一塊直接用的是一個 RPC 框架,沒有自己實現網路傳輸。在 2013 年,碰到一些問題,因此又把網路這塊完全重寫了。比如上文提到佇列隔離排程的地方,也是重新設計了。

本文相關 PPT 連結如下

文章出處:高可用架構

高可用架構