訊息佇列<一>
說到訊息佇列很多人無需多考慮,出口就是削峰、非同步、解耦。但是這些名詞是如何在具體的實際場景中使用的,可能就沒有那麼清楚。
下面就給大家介紹一下什麼是訊息佇列,使用的場景是怎樣,還有一些生產中常見的問題:1.如何保證訊息不丟失?2.如何處理重複訊息?3.如何保證訊息的有序性?4.如何處理訊息堆積?
一、什麼是訊息佇列?
簡單的說訊息佇列就是使用佇列來完成通訊的開發元件,百度百科:訊息佇列是在訊息的傳輸過程中儲存訊息的容器。
二、為什麼要使用訊息佇列?
網際網路發展速度肉眼可見,尤其是近幾年網際網路的普及,5g時代的到來,再也不是以前5塊錢30m的時代了,使用者量遞增,服務體系的增大,促使技術架構需要不斷的演進 ,隨之而來的服務拆分,有時整個一套服務有成百上千個微服務。服務與服務之間相互呼叫和依賴,這時我們需要訊息佇列的解耦,由於使用者量大就需要訊息佇列來控制流量的接入,也就是所謂的削峰。一些電商專案中有些服務不需要及時響應的就可以採用非同步的方式來進行處理,例如:積分服務、簡訊通知。下面拿具體場景來具體介紹一下削峰、非同步、解耦。
削峰(流量控制):
可能有的人會把以下的流量控制混為一談,上次和同時探討時就出現過這樣的說法,【限流的控制還有別的技術手段來進行控制,例如:Nginx、閘道器(zuul、gateway)】。 但是訊息佇列的流量控制指的是將訊息儲存在訊息佇列中等待服務慢慢消費。而不是像閘道器到達限制直接返回不做任何業務的處理。
相對來說後端服務是比較脆弱的,因為隨著專案的不斷演進,業務需求越來越多,處理時長也就會隨之增加。像一些大促秒殺活動,爆發式的流量進入,可能就頂不住了。訊息佇列的出現就可以起到緩衝的作用,也就是所謂的削峰填谷。
某些後臺任務,不需要及時地響應,並且業務處理複雜且流程長,那麼過來的請求先放入訊息佇列中,後端服務按照自己的節奏處理。
非同步:
舉個例子:在電商專案中在沒有積分服務和簡訊服務時 ,就是直接扣庫存建立預付訂單。
隨著業務增多,服務拆分越多,請求的鏈路就會越長,響應的速度越來越慢,影響使用者體驗,而且會降低qps,相對於扣庫存訂單生成,簡訊通知和積分的新增沒有必要那麼及時,這裡在下單結束後可以將處理資料放到訊息佇列中,直接返回響應給客戶端,而簡訊服務和積分服務可以慢慢的去消費訊息佇列中的訊息。這樣可以減少請求的等待,還能讓服務非同步併發處理,提升系統總體效能。
解耦:
如果業務不斷增加,出現了資料上報服務,之後說不定還會出現各種服務。也就是說在訂單的下游會不定期的加減服務,如果是硬編碼對服務的改動特別大,這時訊息佇列的出現就可以無狀態的新增下游服務,只需要去訂閱訊息佇列的主題即可,這樣大大降低了服務與服務之間的耦合度。
三、訊息佇列的基本概念
首先在討論 【 1.如何保證訊息不丟失?2.如何處理重複訊息?3.如何保證訊息的有序性?4.如何處理訊息堆積?】之前先說一下訊息佇列的基本概念。
訊息佇列有兩種模型:佇列模型和釋出/訂閱模型。
1、佇列模型
每條訊息只能被一個消費者消費;生產者有多個,消費者有多個,也就是微服務中生產者的節點有多個,消費者的節點也有多個,消費者在消費訊息過程存在競爭關係。
2、釋出/訂閱模型
一條訊息可以被多個消費者同時消費,訊息生產者生成訊息發到同一個topic中,不同的消費者同時訂閱這個主題,也就是說這裡存在資料的冗餘,可以保證一條訊息能被多個消費者消費。即只有一個消費者的情況下和佇列模型基本一致。RabbitMQ
採用佇列模型,RocketMQ
和Kafka
採用釋出/訂閱模型。
四、如何保證訊息不丟失?
一般我們稱傳送訊息方為生產者Producer
,接受消費訊息方為消費者Consumer
,訊息佇列服務端為Broker
。
訊息從Producer
發往Broker
,Broker
將訊息儲存至本地,然後Consumer
從Broker
拉取訊息,或者Broker
推送訊息至Consumer
,最後消費。
上圖可以看到一共有三個階段,分別是生產訊息、儲存訊息和消費訊息。我們從這三個階段分別入手來看看如何確保訊息不會丟失。
1、生產者生產訊息
生產者傳送訊息至Broker
,需要處理Broker
的響應,不論是同步還是非同步傳送訊息,同步和非同步回撥都需要做好try-catch
,妥善的處理響應,如果Broker
返回寫入失敗等錯誤訊息,需要重試傳送。當多次傳送失敗需要作報警,日誌記錄等。
這樣就能保證在生產訊息階段訊息不會丟失。
2、訊息儲存
儲存訊息階段需要在訊息刷盤之後再給生產者響應,假設訊息寫入快取中就返回響應,那麼機器突然斷電這訊息就沒了,而生產者以為已經發送成功了。
如果Broker
是叢集部署,有多副本機制,即訊息不僅僅要寫入當前Broker
,還需要寫入副本機中。那配置成至少寫入兩臺機子後再給生產者響應。這樣基本上就能保證儲存的可靠了。一臺掛了還有一臺還在呢(假如怕兩臺都掛了..那就再多些)。
3、消費者消費訊息
這裡經常會有同學犯錯,有些同學當消費者拿到訊息之後直接存入記憶體佇列中就直接返回給Broker
消費成功,這是不對的。
你需要考慮拿到訊息放在記憶體之後消費者就宕機了怎麼辦。所以我們應該在消費者真正執行完業務邏輯之後,再發送給Broker
消費成功,這才是真正的消費了。
所以只要我們在訊息業務邏輯處理完成之後再給Broker
響應,那麼消費階段訊息就不會丟失。
【總結】要注意訊息可靠性增強了,效能就下降了,等待訊息刷盤、多副本同步後返回都會影響效能。因此還是看業務,例如日誌的傳輸可能丟那麼一兩條關係不大,因此沒必要等訊息刷盤再響應。
五、如何處理重複訊息?
假設我們傳送訊息,就管發,不管Broker
的響應,那麼我們發往Broker
是不會重複的。
但是一般情況我們是不允許這樣的,這樣訊息就完全不可靠了,我們的基本需求是訊息至少得發到Broker
上,那就得等Broker
的響應,那麼就可能存在Broker
已經寫入了,當時響應由於網路原因生產者沒有收到,然後生產者又重發了一次,此時訊息就重複了。
再看消費者消費的時候,假設我們消費者拿到訊息消費了,業務邏輯已經走完了,事務提交了,此時需要更新Consumer offset
了,然後這個消費者掛了,另一個消費者頂上,此時Consumer offset
還沒更新,於是又拿到剛才那條訊息,業務又被執行了一遍。於是訊息又重複了。
可以看到正常業務而言訊息重複是不可避免的,因此我們只能從另一個角度來解決重複訊息的問題。
關鍵點就是冪等。既然我們不能防止重複訊息的產生,那麼我們只能在業務上處理重複訊息所帶來的影響。
六、如何保證訊息的有序性
1、全域性有序性:
如果要保證訊息的全域性有序,首先只能由一個生產者往Topic
傳送訊息,並且一個Topic
內部只能有一個佇列(分割槽)。消費者也必須是單執行緒消費這個佇列。這樣的訊息就是全域性有序的!不過一般情況下我們都不需要全域性有序。
2、部分有序性:
因此絕大部分的有序需求是部分有序,部分有序我們就可以將Topic
內部劃分成我們需要的佇列數,把訊息通過特定的策略發往固定的佇列中,然後每個佇列對應一個單執行緒處理的消費者。這樣即完成了部分有序的需求,又可以通過佇列數量的併發來提高訊息處理效率。
七、如何處理訊息堆積?
訊息的堆積往往是因為生產者的生產速度與消費者的消費速度不匹配。有可能是因為訊息消費失敗反覆重試造成的,也有可能就是消費者消費能力弱,漸漸地訊息就積壓了。
因此我們需要先定位消費慢的原因,如果是bug
則處理bug
,如果是因為本身消費能力較弱,我們可以優化下消費邏輯,比如之前是一條一條訊息消費處理的,這次我們批量處理,比如資料庫的插入,一條一條插和批量插效率是不一樣的。
假如邏輯我們已經都優化了,但還是慢,那就得考慮水平擴容了,增加Topic
的佇列數和消費者數量,注意佇列數一定要增加,不然新增加的消費者是沒東西消費的。一個Topic中,一個佇列只會分配給一個消費者。
當然你消費者內部是單執行緒還是多執行緒消費那看具體場景。不過要注意上面提高的訊息丟失的問題,如果你是將接受到的訊息寫入記憶體佇列之後,然後就返回響應給Broker
,然後多執行緒向記憶體佇列消費訊息,假設此時消費者宕機了,記憶體佇列裡面還未消費的訊息也就丟了。