分布式系統與消息的投遞¶


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

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

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

網絡請求

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

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

成功與失敗

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

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

超時

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

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

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

消息投遞語義

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

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

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

最多一次

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

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

最少一次

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

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

正好一次

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

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

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

投遞順序

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

序列號

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

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

狀態機

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

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

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

協議

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

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

AMQP 協議

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

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

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

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

MQTT 協議

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

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

總結

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM