消息隊列學習(一)
前言:
本文是學習和參考李玥老師的消息隊列高手課,一方面幫助自己學習記錄,另一方面作為分享
1,為什么使用消息隊列?
1.1,進程間通信
消息隊列設計之初就是為了解決進程間的通訊問題,只不過現在它更多的是用來做服務解耦和異步處理等場景。由於不同的進程處於不用的內存空間上,所以無法直接進行通訊,必須通過其他方式完成,比如:管道、共享內存、消息隊列等。
1.2,異步處理
一個簡單場景,在電商系統中,當用戶成功下單並且付款完成后,需要減去庫存,發送短信給用戶,業務流程如圖:
其中,每一個環節都是同步執行,如果每個環節操作都需要50ms,那么一整套流程下來需要4 * 50 = 200 ms,但是上述環節中,減庫存和發短信之間互不影響,互不依賴,所以異步進行,這個時候我們就需要使用消息隊列完成,那么業務流程就變成如下圖:
因為減庫存和發短信是異步執行,所以總的執行時間就變成了3 * 50ms = 150 ms,如此一來減少了50ms時間,更何況真實的項目中肯定不止減庫存和發短信這兩種業務可以異步。所以說,利用消息隊列可以大大減少業務的執行時間從而提高系統的響應效率。
1.3,流量控制
聯想一下秒殺的場景,短時間內,海量的請求服務端,后端服務器很可能因為過載而奔潰掉,雖然說我們可以水平擴容添加機器,但是一味加機器不是解決辦法,你需要考慮到機器成本、服務器成本、運維成本,再者說老板還有個願意不願意,所以提高我們服務自身的健壯性才是我們需要關注的。而自身的健壯性就需要保證我們的服務在自身可以承受的范圍內盡可能的去處理更多的請求,處理不了的可以拒絕,但是往往現實中的程序會因為各種原因,並沒有好的這么健壯性,雖然說我們可以通直接返回錯誤信息給客戶,但是這種方式對客戶來說體驗非常差,所以我們需要設計出一套足夠健壯的架構來保證我們服務端程序。
使用消息隊列,秒殺場景的請求流程如下:
海量的請求從app發出到達網關,這么多的請求並不會直接沖擊后台服務,而是先堆積的消息隊列里,多個后台服務根據自己的處理能力去消費隊列里面的請求,對於超時的消息直接丟棄,app段對於超時的請求直接響應秒殺失敗。因為網關沒有復雜的業務,所以處理能力是遠遠大於后端服務的,所以不需要擔心網關扛不住,如果真的網關扛不住,那么還有別的措施去處理,但是這個處理措施的代價是遠遠小於服務端扛不住的處理措施的。但是上述的架構設計也是有利有弊,優點是可以根據后台的處理能力自動調節流量,但是缺點就是:
- 增加了業務的調用鏈,響應時間變長
- 增加了系統復雜度,上下游系統都需要將同步調用改成異步調用
所以可以如果預先能夠估計出后台服務處理請求能力,就可以做如下改造:
網關在放行請求先,先去消息隊列獲取一個token,如果獲取到則放行,獲取不到則拒絕請求,這樣一來能保證放行的請求和可控的范圍內,發牌器原理也比較簡單,就是單位時間內生成指定個數的令牌放入到消息隊列中,從而達到控制流量的目的,而優點是不破壞原有的同步調用方式。
1.4,服務解耦
假設A服務被B和C服務以及D服務所依賴,如果A服務發生變動,則需要去和B,C,D三個服務同時做調試,以保證服務正常,這樣一來代價是非常大的,如果采用消息隊列,就很簡單了,B,C,D服務不直接和A服務產生關系,而是依賴於消息隊列,A服務在做該改動后,只需要給消息隊列發送一條消息即可,這種設計就很類似於觀察者模式,盡可能的降低系統與系統之間的耦合性。
2,如何選擇消息隊列?
2.1,出發點
選擇開源框架我們應該從以下幾點考慮出發:
- 開源性,如果選擇的框架沒有開源,或者開源了很少的一部分,那如此一來,如果日后我們在使用中出現重大bug,因為不知道源碼,那我們將束手無策,只能被動的等待帶作者的修改,而這個時間是不確定的。
- 活躍性,如果該框架比較冷門,社區活躍度底,那么在使用中你很可能會遇到各種BUG,而且網上基本有很少的資料供你參考。
- 兼容性,如果你選擇的框架兼容性很差,那么導致的問題就是日后你再想在使用這個框架的基礎上添加一些新的東西將會變得很難
而對於消息隊列來說,除了以上幾點,還有其他方面我們需要考慮:
- 消息傳遞的可靠性
- 支持集群,因為現在的項目基本上並發量很大,所以需要支持集群來負載均衡
- 性能好,感覺我好像說了一句廢話 -v-。
2.2,常見的消息隊列
正確的選擇使用那種消息隊列是該根據自己的業務場景和市面上流行的不同的消息隊列的特性而定,下面就在上述原則上簡單總結一下市面上流行的消息隊列的優缺點和使用場景(從流行程度划分梯隊)
第一梯隊:
RabbitMQ
老牌消息隊列,由Erlang語言開發,社區活躍度高,支持AQMP協議。
優點:
- 社區活躍度高,基本上使用中所碰到的問題,網上都可以找到答案
- 語言兼容性好,有大多數語言的客戶端
- 開箱即用,輕量級,容易部署何使用
- 支持靈活的路由配置
缺點:
- 對消息堆積支持不好,消息隊列會影響性能
- 性能不是很高,每秒處理幾萬到十幾萬,是常見消息隊列中性能最差的
- 開發語言冷門,熟悉容易精通難,而且Erlang學習路線陡峭
RocketMQ
國產優秀的開源消息隊列框架,由阿里巴巴設計研發,經歷過雙11的洗禮,性能和穩定性以及可靠性值得信賴,慢慢被國內大廠所引用
優點:
國產開源,所以社區有很多中文的資料
性能好,高出RabbitMQ一個量級,大概每秒是幾十萬
Java語言開發,國人寫的,所以源碼學習起來相對容易,所以進行二次開發比較容易
響應時延低,可以做到毫秒級
經歷過雙11的洗禮,性能和穩定性以及可靠性有保證,且具備現代消息隊列所有的功能和特性
缺點:
- 因為是國產,誕生時間也不是很長,所以在國際上不是那么流行,所以兼容性不是很好
Kafka
最初的設計目的是用於處理海量的日志,但是Kafka給人的第一印象不好,比如不保證消息的可靠性,可能會丟失消息,也不支持集群,功能上也比較簡陋,但這些在Kafka的設計之初是被容許的,因為追求極致的性能,因為最開始它就只是為了處理海量日志而生,而這些問題在處理日志時,是可以被接受的,但是慢慢的Kafka這些問題都被慢慢完善。
優點:
- 性能高,Kafka采用Java和Scala開發,采用大量批量和異步的思想,所以性能是最好的
- 兼容性好,特別是對接大數據和流計算
缺點:
- 因為是異步和批量,當有消息來后,Kafka並不是立即發送出去,而是攢一批一發,所以同步收發消息的時延高,不適合做有在線業務的場景
第二梯隊:
ActiveMQ
最老牌的開源消息隊列,是十年前唯一可供選擇的開源消息隊列,基本上不建議使用,因為已經進入老年區,社區活躍度很低
ZeroMQ
嚴格來說 ZeroMQ 並不能稱之為一個消息隊列,而是一個基於消息隊列的多線程網絡庫,如果你的需求是將消息隊列的功能集成到你的系統進程中,可以考慮使用 ZeroMQ。
最后說一下 Pulsar,很多人可能都沒聽說過這個產品,Pulsar 是一個新興的開源消息隊列產品,最早是由 Yahoo 開發,目前處於成長期,流行度和成熟度相對沒有那么高。與其他消息隊列最大的不同是,Pulsar 采用存儲和計算分離的設計,我個人非常喜歡這種設計,它有可能會引領未來消息隊列的一個發展方向,建議你持續關注這個項目。
3,消息隊列中的隊列和主題是什么?
隊列,就是我們理解的那個隊列,沒什么不一樣的,FIFO模式,缺點就是隊列里的同一個消息只能被一個消費者消費一次
主題,為了解決消息多次消費的問題,引入主題,消息隊列架構便成了發布-訂閱的模式,如圖:
不同的消費者訂閱不同的主題,每個消費者能獲得每個主題的所有消息,從而實現一個消息給多個消費者消費。
RabbitMQ的架構設計模型:
RabbitMQ並沒有使用發布-訂閱模式,而依然采用隊列設計,但是使用交換機Exchange來實現消息被多次消費,消息的生產者不管消息會被發送到哪里,只需要將消息丟給交換機,通過交換機消息便會被路由到響應的隊列中。
RocketMQ 的架構設計模型:
RocketMQ 采用的是標准的發布-訂閱模式,而RocketMQ 做了改良,在主題層面引入了隊列這個概念,這么做的原因是解決消息阻塞的問題。為了保證消息的可靠性,大多數消息隊列都采用了消費-確認機制,這就表明,在前面一個消費沒有被正常消費前,消息隊列的Broker是不會再推送消息的,換句話說就是同一時刻是由一條消息被消費,這顯然會很影響性能,所以RocketMQ引入了隊列的概念,相同性質的消費者被划分成一個消費組,每個消費組消費Bocker里面的N個隊列,每個消費組獲取的隊列里的數據都是相同的,都是同一個主題下的所有消息,換句話說,多個消費組之間,消息是共享的,而組內,每個消費者都是競爭關系,組內隊列的消息只能被消費一次。因為同一個消息被多個消費組的隊列共享,所以需要在每個消費組內維護每個隊列的當前消費位置,當消息被消費,位置加1,該位置之前的消息就是已經被消費的消息,之后的就是為消費的,如果同一個消息在每個隊列的位置都處於已消費位置,則Broker就可以刪除這條消息。Kafka於RocketMQ架構相同,只不過隊列在Kafka的模型中叫做分區。
總結一下,為什么RocketMQ和Kafka快?回想RBMQ,每個消費者對應一個隊列,為了保證順序性和消息不丟失,所以每個消費者都是串行消費隊列上的消息,所以說兩個消息不能被一個消費者同時消費,而RCMQ這種模式,一個消費者可以消費消費組所能消費的所有隊列里面的消息,而且是可以並行消費不同的的隊列的消息的,所以性能是比較高的,不同隊列中的消息可以同時被消費,並且消費組的線程也可以並發的消費不同的消息。再一個,因為消費位置游標的存在,每個消費者線程之間不需要等待彼此的通知,等到消息在所有消費組都消費過后被刪除。還有一個可優化點,同一個隊列上面的消息無法被並發消費,如何優化,策略如下:
- 在同一個消費組內,維護一個全局的下標,采用CAS機制改變
- 建立一個重試隊列,等到消息被消費后,消費了幾次移動幾次下標,如果消息失敗就丟進重試隊列
4,消息隊列常見問題?
4.1,消息堆積怎么辦?
先分析一下,消息堆積的原因是什么,首先,如果單位時間內,生產者生產10個消息,消費者消費10個消息,那這樣是不會產生堆積問題的,那么產生堆積問題很可能就是生產力過大,消費能力過小引起的。而避免這種問題就需要注意在程序設計之初消費端如何設計,我們需要盡可能的保證消費端的處理能力大於生產者。當然,這個往往在我們發現已經出現消息堆積都已經很難改善,所以此時我們能做的就是檢查服務端的代碼,能優化盡量優化,可以使用異步就是用異步,最后可以水品擴容服務端,提高消費能力。而在服務生產段,在發生消息堆積時,我們可以適當的調整一下系統,做做服務降低,關閉掉一部分可以關閉的服務,減少生產者的數量進而減少消息數量。
當然,對於生產者生產力慢的問題,我們可以采用批量和增加並發的方式處理。
4.2,消息怎么保證不丟失?
消息從生產到消費一共會經歷如下圖幾個階段:
- 生產者在生產階段采用ack機制,確保消息正確存儲到Broker,
- 存儲階段,需要防止宕機或者進程被終止,所以可以調整Broker參數,將消息存儲到磁盤上,或者搭建集群
- 消費階段,當消費者業務正常處理完成后,再應答,而不是一接到消息就應答
總的來說,可以通過給每個消息添加序號來檢測和處理消息丟失的問題
4.3,怎么保證消息重復消費?
先考慮一下,這種情況能不能避免,但是很可惜,在現有的消息隊列中,基本上是避免不了了的,在消息隊列遙測傳輸協議MQTT種固定了3種消息發送的服務質量標准:
- At most once: 至多一次。消息在傳遞時,最多會被送達一次。換一個說法就是,沒什么消息可靠性保證,允許丟消息。一般都是一些對消息可靠性要求不太高的監控場景使用,比如每分鍾上報一次機房溫度數據,可以接受數據少量丟失。
- At least once: 至少一次。消息在傳遞時,至少會被送達一次。也就是說,不允許丟消息,但是允許有少量重復消息出現。
- Exactly once:恰好一次。消息在傳遞時,只會被送達一次,不允許丟失也不允許重復,這個是最高的等級。
而市面上大多的消息隊列采用的都是至少一次這種標准(Kafka另說),也就是說消息隊列肯定會重復,這樣想想其實也正常,寧願重復也不要少,那么我們就應該想想怎么避免這種消息重復消費的問題,常見的如下:
- 保證消費端服務的冪等性,而保證冪等性手段有很多,比如利用數據庫主鍵唯一性機制、樂觀鎖等。
- 利用redis判斷,消費者生產的每一條消息都生成一個全局ID(雪花算法),最好可以自增,並插入到redis,消費者消費后,吧redis里面的記錄刪除掉,每次消費者消費消息前,先去redis里面查詢一下這條消息的有效性
4.4,如何嚴格保證消息順序性
- 單一隊列,單一消費者,單一生產者
- 采用一致性hash,用消息的某個可以做唯一校驗的屬性的hash值來保證每次都進入相同的隊列中或者不考慮擴容,采用隊列數量取模