天天看點

分布式系統與消息投遞

消息是一個非常有趣的概念,它是由來源發出一個離散的通信單元,被發送給一個或者一群接受者,無論是單體服務還是分布式系統中都有消息的概念,隻是這兩種系統中傳輸消息的通道方法或者通道不同;單體服務中的消息往往可以通過 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),也就是投遞語義,最多一次、最少一次和正好一次。

總結

繼續閱讀