歡迎光臨
每天分享高質量文章

分佈式系統與訊息的投遞

  • 網絡請求
    • 成功與失敗
    • 超時
  • 訊息投遞語意
    • 最多一次
    • 最少一次
    • 正好一次
  • 投遞順序
    • 序列號
    • 狀態機
  • 協議
    • AMQP 協議
    • MQTT 協議
  • 總結

訊息是一個非常有趣的概念,它是由來源發出一個離散的通信單元,被髮送給一個或者一群接受者,無論是單體服務還是分佈式系統中都有訊息的概念,只是這兩種系統中傳輸訊息的通道方法或者通道不同;單體服務中的訊息往往可以通過 IO、行程間通信、方法呼叫的方式進行通信,而分佈式系統中的遠程呼叫就需要通過網絡,使用 UDP 或者 TCP 等協議進行傳輸。

communication-reliability

然而網絡在計算機的世界中是最不可控的,如果我們通過網絡請求呼叫其他服務的接口,可能就會由於種種原因沒有將訊息送達至標的的服務,對於當前服務我們並不能控制網絡的傳輸,在很多時候也很難控制網絡通信的質量,這也就是為什麼『網絡是穩定、可信賴的』分佈式系統中常見的謬論之一。

通信渠道的不可靠是造成構建大規模分佈式系統非常複雜並且困難的重要原因。

網絡請求

作為分佈式系統之間各個節點的通信渠道,網絡其實是非常不可靠通信方式,如果我們想要保證節點狀態的一致性,這種通信方式的複雜性使得我們在進行跨服務呼叫時需要處理非常多的邊界條件,在之前的文章 分佈式系統 · 分佈式事務的實現原理 中簡單介紹過,網絡通信可能會包含,成功、失敗以及超時三種情況。

network-communication

每一次網絡請求其實都是一次信息的投遞,由於當前的節點無法得知其他節點信息,只能通過網絡請求的響應來得知這次信息投遞的結果。

成功與失敗

雖然網絡的情況比較不穩定,但是我們在大多數時候通過網絡傳輸一些信息時,無論是傳回的結果是成功還是失敗,其實都能得到確定的結果:

network-success-and-failure

每一次確定的響應都需要這次請求在一個往返以及被呼叫節點中正確處理,流量既不能被中間代理丟包,也不能由於標的節點的錯誤導致無法發出響應,只有在同時滿足了這兩個條件的情況下,我們才能得到確定的響應結果。對於節點來說,這次請求傳回成功還是失敗都比較好處理,因為只要有確定的結果,網絡請求這種通信方式與行程間通信或者方法呼叫這些更可靠的途徑在處理上都沒有太多的區別,但是在通信的過程中出現其他的問題時就比較棘手了。

超時

在分佈式系統中,不是任何的網絡請求都能夠得到確定的響應,如果網絡請求在往返以及被呼叫節點處理的過程中出現了丟包或者節點錯誤,發出請求的節點就可能永遠也無法得到這次請求的響應。

network-timeout

每一個節點在發出請求之後,都對這次請求如何路由以及被處理一無所知,所以節點需要設置一個合適的超時時間,如果請求沒有在規定的時間內傳回,就會認為當前請求已經超時,也就是網絡請求失敗了。

超時的網絡請求是導致分佈式系統難以處理的根本原因之一,在這種問題發生時節點並不知道標的節點是否收到了當前請求,對於冪等的網絡請求還好,一旦請求可能會改變標的節點的狀態就非常棘手了,因為我們並不能確定上一次網絡請求是在哪一步失敗的,如果是響應傳回的過程中發生了故障,那麼如果重試一些請求就會出現問題,可能會觸發銀行的兩次轉賬,這是我們無論如何也無法接受的;總而言之,網絡通信的不穩定迫使我們處理由於超時而出現的複雜問題,這也是在開發分佈式系統時不得不考慮的。

訊息投遞語意

在分佈式系統中使用網絡進行通信確實是一種不可靠的方式,訊息的發送者只能知道掌控當前節點,所以沒有辦法保證傳輸渠道的可靠性,網絡超時這種常見的通信錯誤極大地增加了分佈式系統通信的複雜度,我們可以對網絡提供的基本傳輸能力進行封裝,保證資料通信的可靠性。

message-delivery-problems

網絡請求由於超時的問題,訊息的發送者只能通過重試的方式對訊息進行重發,但是這就可能會導致訊息的重覆發送與處理,然而如果超時後不重新發送訊息也可能導致訊息的丟失,所以如何在不可靠的通信方式中,保證訊息不重不漏是非常關鍵的。

message-delivery-qos

我們一般都會認為,訊息的投遞語意有三種,分別是最多一次(At-Most Once)、最少一次(At-Least Once)以及正好一次(Exactly Once),我們分別會介紹這三種訊息投遞語意究竟是如何工作的。

最多一次

最多一次其實非常容易保證的,UDP 這種傳輸層的協議其實保證的就是最多一次訊息投遞,訊息的發送者只會嘗試發送該訊息一次,並不會關心該訊息是否得到了遠程節點的響應。

at-most-once

無論該請求是否發送給了接受者,發送者都不會重新發送這條訊息;這其實就是最最基本的訊息投遞語意,然而訊息可能由於網絡或者節點的故障出現丟失。

最少一次

為瞭解決最多一次時的訊息丟失問題,訊息的發送者需要在網絡出現超時重新發送相同的訊息,也就是引入超時重試的機制,在發送者發出訊息會監聽訊息的響應,如果超過了一定時間也沒有得到響應就會重新發送該訊息,直到得到確定的響應結果。

at-least-once

對於最少一次的投遞語意,我們不僅需要引入超時重試機制,還需要關心每一次請求的響應,只有這樣才能確保訊息不會丟失,但是卻可能會造成訊息的重覆,這就是最少一次在解決訊息丟失後引入的新問題。

正好一次

雖然最少一次解決了最多一次的訊息丟失問題,但是由於重試卻帶來了另一個問題 – 訊息重覆,也就是接受者可能會多次收到同一條訊息;從理論上來說,在分佈式系統中想要解決訊息重覆的問題是不可能的,很多訊息服務提供了正好一次的 QoS 其實是在接收端進行了去重。

exactly-once

訊息去重需要生產者生產訊息時加入去重的 key,消費者可以通過唯一的 key 來判斷當前訊息是否是重覆訊息,從訊息發送者的角度來看,實現正好一次的投遞是不可能的,但是從整體來看,我們可以通過唯一 key或者重入冪等的方式對訊息進行『去重』。

訊息的重覆是不可能避免的,除非我們允許訊息的丟失,然而相比於丟失訊息,重覆發送訊息其實是一種更能讓人接受的處理方式,因為一旦訊息丟失就無法找回,但是訊息重覆卻可以通過其他方法來避免副作用。

投遞順序

由於一些網絡的問題,訊息在投遞時可能會出現順序不一致性的情況,在網絡條件非常不穩定時,我們就可能會遇到接收方處理訊息的順序和生產者投遞的不一致;想要滿足絕對的順序投遞,其實在生產者和消費者的單執行緒運行時是相對比較好解決的,但是在市面上比較主流的訊息佇列中,都不會對訊息的順序進行保證,在這種大前提下,消費者就需要對順序不一致的訊息進行處理,常見的兩種方式就是使用序列號或者狀態機

序列號

使用序列號保證投遞順序的方式其實與 TCP 協議中使用的 SEQ 非常相似,因為網絡並不能保證所有資料包傳輸的順序並且每個棧幀的傳輸大小有限,所以 TCP 協議在發送資料包時加入 SEQ,接受方可以通過 SEQ 將多個資料包拼接起來並交由上層協議進行處理。

message-delivery-sequence

在投遞訊息時加入序列號其實與 TCP 中的序列號非常類似,我們需要在資料之外增加訊息的序列號,對於消費者就可以根據每一條訊息附帶的序列號選擇如何處理順序不一致的訊息,對於不同的業務來說,常見的處理方式就是用阻塞的方式保證序列號的遞增或者忽略部分『過期』的訊息。

狀態機

使用序列號確實能夠保證訊息狀態的一致,但是卻需要在訊息投遞時額外增加欄位,這樣消費者才能在投遞出現問題時進行處理,除了這種方式之外,我們也可以通過狀態機的方式保證資料的一致性,每一個資源都有相應的狀態遷移事件,這些事件其實就是一個個訊息(或操作),它們能夠修改資源的狀態:

message-delivery-statemachine

在狀態機中我們可以規定,狀態的遷移方向,所有資源的狀態只能按照我們規定好的線路進行改變,在這時只要對生產者投遞的訊息狀態做一定的約束,例如:資源一旦 completed 就不會變成 failed,因為這兩個狀態都是業務邏輯中定義的最終狀態,所以處於最終狀態的資源都不會繼續接受其他的訊息。

假設我們有如下的兩條訊息 active 和 complete,它們分別會改變當前資源的狀態,如果一個處於 pending狀態的資源先收到了 active 再收到 complete,那麼狀態就會從 pending 遷移到 active 再到 completed;但是如果資源先收到 complete 後收到 active,那麼當前資源的狀態會直接從 pending 跳躍到 completed,對於另一條訊息就會直接忽略;從總體來看,雖然訊息投遞的順序是亂序的,但是資源最終還是通過狀態機達到了我們想要的正確狀態,不會出現不一致的問題。

協議

訊息投遞其實有非常多相關的應用,最常見的組件就是訊息佇列了,作為一種在各個 Web 專案中常用的組件,它提供了很多能力,包括訊息的持久儲存、不同的投遞語意以及複雜的路由規則等等,能夠顯著地增加系統的可用性、起到比較比明顯的削峰效果。

在這裡將介紹幾種比較常見的訊息佇列協議,我們將簡單說明各個協議的作用以及它們的實現原理和關鍵特性,也會簡單提及一些遵循這些協議實現的訊息佇列中間件。

AMQP 協議

AMQP 協議的全稱是 Advanced Message Queuing Protocol,它是一個用於面向訊息中間件的開放標準,協議中定義了佇列、路由、可用性以及安全性等方面的內容。

amqp-protoco

該協議目前能夠為通用的訊息佇列架構提供一系列的標準,將發佈訂閱、佇列、事務以及流資料等功能抽象成了用於解決訊息投遞以及相關問題的標準,StormMQ、RabbitMQ 都是 AMQP 協議的一個實現。

在所有實現 AMQP 協議的訊息中間中,RabbitMQ 其實是最出名的一個實現,在分佈式系統中,它經常用於儲存和轉發訊息,當生產者短時間內創建了大量的訊息,就會通過訊息中間件對訊息轉儲,消費者會按照當前的資源對訊息進行消費。

producer-and-consume

RabbitMQ 在訊息投遞的過程中保證儲存在 RabbitMQ 中的全部訊息不會丟失、推送者和訂閱者需要通過信號的方式確認訊息的投遞,它支持最多一次和最少一次的投遞語意,當我們選擇最少一次時,需要冪等或者重入機制保證訊息重覆不會出現問題。

MQTT 協議

另一個用於處理髮布訂閱功能的常見協議就是 MQTT 了,它建立在 TCP/IP 協議之上,能夠在硬體性能底下或者網絡狀態糟糕的情況下完成發佈與訂閱的功能;與 AMQP 不同,MQTT 協議支持三種不同的服務質量級別(QoS),也就是投遞語意,最多一次、最少一次和正好一次。

從理論上來看,在分佈式系統中實現正好一次的投遞語意是不可能的,這裡實現的正好一次其實是協議層做了重試和去重機制,消費者在處理 MQTT 訊息時就不需要關係訊息是否重覆這種問題了。

總結

在分佈式系統中想要保證訊息的送達確實是一件比較複雜的事情,通信方式的不確定使得我們需要處理很多問題,我們既需要在網絡錯誤或者超時時進行重試,還需要對一些請求支持重入和冪等,保證不會出現一致性的錯誤;這其實都是因為在分佈式系統中,正好一次的訊息投遞語意是不存在的,訊息要麼可能會丟失,要麼就可能會重覆。

赞(0)

分享創造快樂