高可用保證消息絕對順序消費的BROKER設計方案
在要求嚴格順序消息的場景下,消息的發送者,BROKER端(BROKER端和消息存儲放在一起),消息的消費者都要求按照順序進行,三者任何一個環節的亂序都會導致消息最終的消費順序被打亂。
如果為每一個消息維護一個有序的ID,發送和存儲消息無序,消費邏輯會變得非常復雜,消費端要對消息進行重新編排,會影響消費的性能。
為了保證消息發送、保存、消費三個環節都有順序,就要求在同一個時刻只能有一個同步發送消息的線程,消息必須按照接收到的順序進行保存,消息的消費也只能由一個線程處理。
發送端,消費端為了高可用需要部署多個實例,然后再通過一個協調者,比如ZOOKEEPER等,控制單個實例工作,其他實例處于待命狀態。當工作 實例發生了故障,協調者就會喚醒待命的實例進行工作。由于發送端、消費端實例是無狀態的,切換工作實例不會產生亂序的問題。消息保存的BROKER端是一 個有狀態的應用,如果部署多個實例,當發生故障時,由于故障實例上可能還有未消費的消息就不能進行切換。
在一些要求數據不丟失、必須有序、BROKER高可用的場景下(比如跨數據中心數據庫表的同步,需要按照數據庫LOG順序回放到另一個數據中心, 數據亂序或者丟失信息都可能導致兩個數據中心的數據不一致),BROKER往往采用MASTER-SLAVE同步雙寫,或者同一個消息被同步寫到多臺機器 上,為了保證服務宕機等情況下消息不丟失,有的業務要求每條消息都落到磁盤上。如果采用同步寫多份會嚴重影響性能,如果采用單組MASTER-SLAVE 的結構,當MASTER宕機后,SLAVE成為新的MASTER可以接受發送者的消息,但是無法滿足數據任一時刻都有兩份的要求。
我們現在需要一種設計方案,在保證數據可靠性的條件下性能盡可能的高,同時滿足任一時刻數據至少寫入2份。
下面提供一種BROKER高可用,又能滿足數據任一時刻都有兩份的方案 :
- 采用MASTER-SLAVE結構方式,同步寫入消息(消息允許重復),MASTER-SLAVE上的消息在邏輯上保持一致;
- SLAVE在MASTER宕機后不接受發送請求,但可以進行消費;
- 一個消息隊列分配兩組以上的BROKER組(一個BROKER組由MASTER-SLAVE組成),BROKER組的集群信息在協調者上保存 為一個單向的鏈表,消費者和發送者各有一份獨立的鏈表數據。有消息的BROKER組一定會按受理發送請求的先后順序保存在消費者對應的鏈表上,消費者只能 從鏈表表頭的BROKER組上消費,當BROKER組上的消息消費完且不為當前受理發送請求的BROKER組則從消息鏈表中移除;
- 沒有積壓消息的BROKER組才能被添加到發送鏈表的表尾,當有BROKER組發生故障時會從BROKER組中移除,移除的BROKER組必須保證沒有積壓消息后才能被添加回鏈表;
- 只有發送鏈表表頭的BROKER組才能接受發送請求,同時新切換為受理發送請求的BROKER組會添加到消費鏈表的表尾。
異常處理流程:
- BROKER組有機器宕機則從發送鏈表中移除;
- 當新BROKER組被挑選為當前發送者,則把該組BROKER添加到消費鏈表的表尾;
- 當異常BROKER組的消息消費完成則從消費鏈表表頭移除;
- 當BROKER組機器都恢復正常,且沒有可以消費的消息則添加到發送鏈表的表尾。
(點擊放大圖像)
具體的處理流程描述如下所述。
發送者處理流程
正常情況下,我們可以采用單組MASTER-SLAVE結構的集群方案,MASTER接收到發送者的消息后同步轉發給SLAVE。發送者只有接收 到MASTER,SLAVE都寫入成功的信息才算成功,否則這條消息需要發送者再次進行發送。但是當有一臺機器發生故障時這個集群無法滿足 MASTER,SLAVE都寫入成功的條件。這個時候我們需要把發送者的發送請求FAILOVER到其他的集群上。如果只是簡單地進行發送請求的切換,如 果切換到的BROKER集群上有未消費的消息就可能破壞數據的順序要求。同時消費者還必須知道發送者切換的過程,否則消費者無法知道自己應該先從哪個 BROKER集群上消費,一旦獲取消費的BROKER集群順序與發送時的順不一致,順序性就會被破壞。我們需要記錄好發送到不同BROKER集群的先后順 序,消費者按照記錄的順序進行消費。
如果BROKER集群發生過切換,當前接受請求的BROKER集群可能和消費者當前應該消費的集群不同,需要對發送者和消費者單獨維護當前應該使用的集群信息。
BROKER集群發生故障后怎么通知發送者,可以有多種方式,比如由ZOOKEEPER協調,或者由客戶端處理。我們可以采用發送者來處理BROKER集群故障的問題,當發送者感知到發送失敗或者連接失敗時向協調者發起請求,由協調者返回當前可用的BROKER集群。
協調者判斷BROKER集群是否可以接收新的消息,除了要判斷BROKER是否存活外,還需要查詢其是否有未消費的消息,只有集群上沒有可消費的 消息時才能接收新的發送請求。因此協調者需要知道每個BROKER集群上存放的消息情況。我們可以在BROKER集群被選中為可以接收發送請求時,標識其 為有未消費消息的狀態,當消費者把上面的消息都消費完成后,由該BROKER集群向協調者匯報自己已經消費完成。如果該集群服務都不可用時,無法匯報自己 的消息積壓情況,協調者會一直標記其為有未消費的消息,直到該集群服務恢復后,匯報完是否存在有未消費的消息。
(點擊放大圖像)
消費者處理流程
消費者需要消費消息時,先從協調者上獲取當前應該獲取消息的BROKER集群,當消費完成時,BROKER集群會向協調者匯報自己已經沒有積壓消 息了。協調者接收到匯報后就把當前BROKER集群從需要消費的列表中移除。消費者從一個集群上獲取不到消息后會再次請求協調者,獲取下一個可以消費的集 群信息,從新的集群上繼續消費消息。
協調者處理流程
當協調者接收到發送者的請求時,先查看發送列表中是否存在可用的集群,如果沒有就會檢查消息分配的所有集群,把滿足條件(消息無積 壓,MASTER-SLAVE都工作正常)的集群加入到可發送集群列表中。如果也沒有找到可用集群,那么發送者會被阻塞,直到找到可以使用的集群。
當集群被選為當前可用集群時,需要在未返回給發送者之前把該集群信息同步添加到消費集群列表中,防止協調者出現故障時,消費者獲取不到這個集群的信息,被跳過導致消費亂序。
當協調者接收到消費者的請求時,協調者只需要把消費集群列表表頭第一個集群返回給消費者就可以了。消費者消費完消息會通知相應的BROKER集群,該集群感知到消息都已經被消費后馬上匯報給協調者,協調者收到匯報信息就會把該集群從消費集群列表的表頭移除。
(點擊放大圖像)
如何控制單個實例發送
上面主要描述了對BROKER集群的控制,防止消息由于BROKER集群調度順序不對導致消息亂序。
順序消息還需要滿足發送者順序發送,消費者順序消費,通常為了保證應用的高可用。我們會對發送者和消費者部署多個實例,當一個實例發生異常宕機 時,其他的實例可以繼續工作,防止單點故障。對于順序消息同一個時間點只能有一個線程在工作,單個實例只啟動一個線程進行發送和消費,只需要編寫代碼的時 候控制就可以做到,但是當我們把應用部署為多個實例時,實例之間就需要一個協調者,保證每次都只有一個工作實例。
發送者啟動時先注冊一個ZOOKEEPER的監聽事件,通過ZOOKEEPER選舉出來一個LEADER,只有拿到LEADER權限的發送者實例才能夠發送消息,沒有取到LEADER權限的發送者需要馬上中斷發送消息的線程。消費者應用可以按照上述方案進行相同的處理。
注意事項
MASTER-SLAVE集群中單臺機器接收到消息,發送者視為發送失敗,可能存在消息重復發送,SLAVE成為MASTER后繼續接受消費請求,消費者可能取到已經消費過的消息,因此需要業務邏輯做可以重復消費的處理。
如果有積壓的消息,MASTER和SLAVE同時宕機,由于順序的要求,消費者會被阻塞,不能繼續進行消費,雖然這種情況極少發生,還是需要注意。消費者被阻塞,但是不會影響發送者,只要有可以接收消息的BROKER集群,發送者可以繼續進行工作。
主從之間同步復制消息也需要保證順序處理,避免SLAVE上消息的順序與MASTER上的順序不一致。
單個線程發送和消費,在一些業務場景下可能不能滿足性能需求,用戶可以根據自己的業務邏輯,把沒有順序要求的業務進行拆分,分成不同的消息類型進行發送,單個消息類型保證順序。
作者簡介
丁俊,有9年工作經驗,目前就職于京東商城云平臺,為消息中間件研發小組leader,主要負責公司內部高性能、高可用消息中間件的架構。