本套技術專欄是作者(秦凱新)平時工作的總結和昇華,通過從真實商業環境抽取案例進行總結和分享,並給出商業應用的調優建議和叢集環境容量規劃等內容,請持續關注本套部落格。QQ郵箱地址:1120746959@qq.com,如有任何學術交流,可隨時聯絡。
1 訊息中介軟體系統如何進行技術選型
1.1 訊息佇列的常見使用場景
比較核心的有3個:解耦、非同步、削峰。
- 解耦:舉例如下:A系統傳送個資料到BCD三個系統,介面呼叫傳送,那如果E系統也要這個資料呢?那如果C系統現在不需要了呢?現在A系統又要傳送第二種資料了呢?A系統負責人瀕臨崩潰中。。。再來點更加崩潰的事兒,A系統要時時刻刻考慮BCDE四個系統如果掛了咋辦?我要不要重發?我要不要把訊息存起來?頭髮都白了啊。。。
- 非同步:舉例如下:A系統接收一個請求,需要在自己本地寫庫,還需要在BCD三個系統寫庫,自己本地寫庫要3ms,BCD三個系統分別寫庫要300ms、450ms、200ms。最終請求總延時是3 + 300 + 450 + 200 = 953ms,接近1s,時間延遲過大。
- 削峰:每天0點到11點,A系統風平浪靜,每秒併發請求數量就100個。結果每次一到11點~1點,每秒併發請求數量突然會暴增到1萬條。但是系統最大的處理能力就只能是每秒鐘處理1000個請求啊。導致系統崩潰。
1.2 訊息佇列技術選型
2 訊息中介軟體系統高可用實踐
2.1 RabbitMQ的高可用性
rabbitmq有三種模式:單機模式,普通叢集模式,映象叢集模式。
普通叢集模式模式是在多臺機器上啟動多個rabbitmq例項,每個機器啟動一個。但是你建立的queue,只會放在一個rabbtimq例項上,但是每個例項都同步queue的後設資料。完了你消費的時候,實際上如果連線到了另外一個例項,那麼那個例項會從queue所在例項上拉取資料過來。
映象叢集模式是,才是所謂的rabbitmq的高可用模式,跟普通叢集模式不一樣的是,你建立的queue,無論後設資料還是queue裡的訊息都會存在於多個例項上,然後每次你寫訊息到queue的時候,都會自動把訊息到多個例項的queue裡進行訊息同步。
rabbitmq並不是分散式訊息佇列,他就是傳統的訊息佇列,只不過提供了一些叢集、HA的機制而已。rabbitmq一個queue的資料都是放在一個節點裡的,映象叢集下,也是每個節點都放這個queue的完整資料。無法真正實現叢集的擴容。
2.2 kafka的高可用性
kafka由多個broker組成,每個broker是一個節點;你建立一個topic,這個topic可以劃分為多個partition,每個partition可以存在於不同的broker上,每個partition就放一部分資料。一個topic的資料,是分散放在多個機器上的,每個機器就放一部分資料。
kafka 0.8以前,是沒有HA機制的,就是任何一個broker當機了,那個broker上的partition就廢了,沒法寫也沒法讀,沒有什麼高可用性可言。
kafka 0.8以後,提供了HA機制,就是replica副本機制。每個partition的資料都會同步到其他機器上,形成自己的多個replica副本。然後所有replica會選舉一個leader出來,那麼生產和消費都跟這個leader打交道,然後其他replica就是follower。
寫的時候,leader會負責把資料同步到所有follower上去,讀的時候就直接讀leader上資料即可。只能讀寫leader?很簡單,要是你可以隨意讀寫每個follower,那麼就要care資料一致性的問題,系統複雜度太高,很容易出問題。kafka會均勻的將一個partition的所有replica分佈在不同的機器上,這樣才可以提高容錯性。
消費的時候,只會從leader去讀,但是隻有一個訊息已經被所有follower都同步成功返回ack的時候,這個訊息才會被消費者讀到。
3 訊息中介軟體系統消費冪等性實踐
3.1 訊息中介軟體系統重複消費
既然是消費訊息,那肯定要考慮考慮會不會重複消費?能不能避免重複消費?或者重複消費了也別造成系統異常可以嗎?這個是MQ領域的基本問題,其實本質上如何使用訊息佇列保證冪等性。
kafka實際上有個offset的概念,就是每個訊息寫進去,都有一個offset,代表他的序號,然後consumer消費了資料之後,每隔一段時間,會把自己消費過的訊息的offset提交一下,代表我已經消費過了,下次我要是重啟啥的,你就讓我繼續從上次消費到的offset來繼續消費吧。
但是凡事總有意外,比如我們之前生產經常遇到的,就是你有時候重啟系統,看你怎麼重啟了,如果碰到點著急的,直接kill程式了,再重啟。這會導致consumer有些訊息處理了,但是沒來得及提交offset,尷尬了。重啟之後,少數訊息會再次消費一次。
3.1 如何保證冪等性
-
比如你拿個資料要寫庫,你先根據主鍵查一下,如果這資料都有了,你就別插入了,update一下好吧
-
比如你是寫redis,那沒問題了,反正每次都是set,天然冪等性。
-
比如你不是上面兩個場景,那做的稍微複雜一點,你需要讓生產者傳送每條資料的時候,裡面加一個全域性唯一的id,類似訂單id之類的東西,然後你這裡消費到了之後,先根據這個id去比如redis裡查一下,之前消費過嗎?如果沒有消費過,你就處理,然後這個id寫redis。如果消費過了,那你就別處理了,保證別重複處理相同的訊息即可。
-
基於資料庫的唯一鍵來保證重複資料不會重複插入多條,我們之前線上系統就有這個問題,就是拿到資料的時候,每次重啟可能會有重複,因為kafka消費者還沒來得及提交offset,重複資料拿到了以後我們插入的時候,因為有唯一鍵約束了,所以重複資料只會插入報錯,不會導致資料庫中出現髒資料。
4 訊息中介軟體系統訊息零丟失實踐
4.1 rabbitmq 訊息零丟失實踐
-
基於rabbitmq提供的事務功能,
生產者傳送資料之前開啟rabbitmq事務(channel.txSelect),然後傳送訊息,如果訊息沒有成功被rabbitmq接收到,那麼生產者會收到異常報錯,此時就可以回滾事務(channel.txRollback),然後重試傳送訊息;如果收到了訊息,那麼可以提交事務(channel.txCommit)。但是問題是,rabbitmq事務機制一搞,基本上吞吐量會下來,因為太耗效能。
-
基於rabbitmq開啟confirm模式
在生產者那裡設定開啟confirm模式之後,你每次寫的訊息都會分配一個唯一的id,然後如果寫入了rabbitmq中,rabbitmq會給你回傳一個ack訊息,告訴你說這個訊息ok了。如果rabbitmq沒能處理這個訊息,會回撥你一個nack介面,告訴你這個訊息接收失敗,你可以重試。而且你可以結合這個機制自己在記憶體裡維護每個訊息id的狀態,如果超過一定時間還沒接收到這個訊息的回撥,那麼你可以重發。
-
事務機制和cnofirm機制不同之處
事務機制和cnofirm機制最大的不同在於,事務機制是同步的,你提交一個事務之後會阻塞在那兒,但是confirm機制是非同步的,你傳送個訊息之後就可以傳送下一個訊息,然後那個訊息rabbitmq接收了之後會非同步回撥你一個介面通知你這個訊息接收到了。
-
消費端手動ACK機制實現
rabbitmq如果丟失了資料,主要是因為你消費的時候,剛消費到,還沒處理,結果程式掛了,比如重啟了,那麼就尷尬了,rabbitmq認為你都消費了,這資料就丟了。
這個時候得用rabbitmq提供的ack機制,簡單來說,就是你關閉rabbitmq自動ack,可以通過一個api來呼叫就行,然後每次你自己程式碼裡確保處理完的時候,再程式裡ack一把。這樣的話,如果你還沒處理完,不就沒有ack?那rabbitmq就認為你還沒處理完,這個時候rabbitmq會把這個消費分配給別的consumer去處理,訊息是不會丟的。
4.2 kafka訊息零丟失實踐
-
消費端手動ACK機制實現
唯一可能導致消費者弄丟資料的情況,就是說,你那個消費到了這個訊息,然後消費者那邊自動提交了offset,讓kafka以為你已經消費好了這個訊息,其實你剛準備處理這個訊息,你還沒處理,你自己就掛了,此時這條訊息就丟咯。
這不是一樣麼,大家都知道kafka會自動提交offset,那麼只要關閉自動提交offset,在處理完之後自己手動提交offset,就可以保證資料不會丟。但是此時確實還是會重複消費,比如你剛處理完,還沒提交offset,結果自己掛了,此時肯定會重複消費一次,自己保證冪等性就好了。
生產環境碰到的一個問題,就是說我們的kafka消費者消費到了資料之後是寫到一個記憶體的queue裡先緩衝一下,結果有的時候,你剛把訊息寫入記憶體queue,然後消費者會自動提交offset。
然後此時我們重啟了系統,就會導致記憶體queue裡還沒來得及處理的資料就丟失了。
-
kafka broker 資料零丟失保證
比如kafka某個broker當機,然後重新選舉partiton的leader時。大家想想,要是此時其他的follower剛好還有些資料沒有同步,結果此時leader掛了,然後選舉某個follower成leader之後,他不就少了一些資料?這就丟了一些資料啊。
生產環境也遇到過,我們也是,之前kafka的leader機器當機了,將follower切換為leader之後,就會發現說這個資料就丟了
所以此時一般是要求起碼設定如下4個引數:
(1)topic設定replication.factor引數:這個值必須大於1,要求每個partition必須有至少2個副本 (2)kafka服務端設定min.insync.replicas引數:這個值必須大於1,這個是要求一個leader至少感知到有至少一個follower還跟自己保持聯絡,沒掉隊,這樣才能確保leader掛了還有一個follower吧 (3)producer端設定acks=all:這個是要求每條資料,必須是寫入所有replica之後,才能認為是寫成功了 (4)producer端設定retries=MAX(很大很大很大的一個值,無限次重試的意思):這個是要求一旦寫入失敗,就無限重試,卡在這裡了 複製程式碼
我們生產環境就是按照上述要求配置的,這樣配置之後,至少在kafka broker端就可以保證在leader所在broker發生故障,進行leader切換時,資料不會丟失。
-
kafka分割槽partition掛掉之後如何恢復?
在kafka中有一個partition recovery機制用於恢復掛掉的partition。 每個Partition會在磁碟記錄一個RecoveryPoint(恢復點), 記錄已經flush到磁碟的最大offset。當broker fail 重啟時,會進行loadLogs。 首先會讀取該Partition的RecoveryPoint,找到包含RecoveryPoint點上的segment及以後的segment, 這些segment就是可能沒有完全flush到磁碟segments。然後呼叫segment的recover,重新讀取各個segment的msg,並重建索引。
優點:
以segment為單位管理Partition資料,方便資料生命週期的管理,刪除過期資料簡單
在程式崩潰重啟時,加快recovery速度,只需恢復未完全flush到磁碟的segment即可
-
什麼原因導致副本與leader不同步的呢?
慢副本:在一定週期時間內follower不能追趕上leader。最常見的原因之一是IO瓶頸導致follower追加複製訊息速度慢於從leader拉取速度。
卡住副本:在一定週期時間內follower停止從leader拉取請求。follower replica卡住了是由於GC暫停或follower失效或死亡。
新啟動副本:當使用者給主題增加副本因子時,新的follower不在同步副本列表中,直到他們完全趕上了leader日誌。
一個partition的follower落後於leader足夠多時,被認為不在同步副本列表或處於滯後狀態。正如上述所說,現在kafka判定落後有兩種,副本滯後判斷依據是副本落後於leader最大訊息數量(replica.lag.max.messages)或rep licas響應partition leader的最長等待時間(replica.lag.time.max.ms)。前者是用來檢測緩慢的副本,而後者是用來檢測失效或死亡的副本。
注意:新版本中,replica.lag.max.messages已經廢棄。
5 訊息中介軟體系統訊息的順序性實踐
5.1 順序會錯亂的場景
例如:在mysql裡增刪改一條資料,對應出來了增刪改3條binlog,接著這三條binlog傳送到MQ裡面,到消費出來依次執行,起碼得保證人家是按照順序來的吧?不然本來是:增加、修改、刪除;你楞是換了順序給執行成刪除、修改、增加,不全錯了麼。
本來這個資料同步過來,應該最後這個資料被刪除了;結果你搞錯了這個順序,最後這個資料保留下來了,資料同步就出錯了。
- rabbitmq:一個queue,多個consumer,這不明顯亂了
- kafka:一個topic,一個partition,一個consumer,內部多執行緒,這不也明顯亂了
5.2 順序性實踐
(1)rabbitmq:拆分多個queue,每個queue一個consumer,就是多一些queue而已,確實是麻煩點;或者就一個queue但是對應一個consumer,然後這個consumer內部用記憶體佇列做排隊,然後分發給底層不同的worker來處理
(2)kafka:一個topic,一個partition,一個consumer,內部單執行緒消費,寫N個記憶體queue,然後N個執行緒分別消費一個記憶體queue即可
6 訊息佇列的訊息延時以及過期失效實踐
6.1 訊息佇列的延時以及過期場景
可能你的消費端出了問題,不消費了,或者消費的極其極其慢。導致訊息佇列叢集的磁碟都快寫滿了,都沒人消費,這個時候怎麼辦?或者是整個這就積壓了幾個小時,你這個時候怎麼辦?或者是你積壓的時間太長了,導致比如rabbitmq設定了訊息過期時間後就沒了怎麼辦?
6.2 訊息佇列的延時以及過期問題解決思路
- 大量訊息在mq裡積壓並臨時緊急擴容
所以如果你積壓了幾百萬到上千萬的資料,即使消費者恢復了,也需要大概1小時的時間才能恢復過來 一般這個時候,只能操作臨時緊急擴容了,具體操作步驟和思路如下:
1)先修復consumer的問題,確保其恢復消費速度,然後將現有cnosumer都停掉
2)新建一個topic,partition是原來的10倍,臨時建立好原先10倍或者20倍的queue數量
3)然後寫一個臨時的分發資料的consumer程式,這個程式部署上去消費積壓的資料,消費之後不做耗時的處理,直接均勻輪詢寫入臨時建立好的10倍數量的queue
4)接著臨時徵用10倍的機器來部署consumer,每一批consumer消費一個臨時queue的資料
5)這種做法相當於是臨時將queue資源和consumer資源擴大10倍,以正常的10倍速度來消費資料
6)等快速消費完積壓資料之後,得恢復原先部署架構,重新用原先的consumer機器來消費訊息
複製程式碼
-
過期問題解決思路
假設你用的是rabbitmq,rabbitmq是可以設定過期時間的,就是TTL,如果訊息在queue中積壓超過一定的時間就會被rabbitmq給清理掉,這個資料就沒了。那這就是第二個坑了。這就不是說資料會大量積壓在mq裡,而是大量的資料會直接搞丟。
這個情況下,就不是說要增加consumer消費積壓的訊息,因為實際上沒啥積壓,而是丟了大量的訊息。我們可以採取一個方案,就是批量重導,這個我們之前線上也有類似的場景幹過。就是大量積壓的時候,我們當時就直接丟棄資料了,然後等過了高峰期以後,比如大家一起喝咖啡熬夜到晚上12點以後,使用者都睡覺了。
這個時候我們就開始寫程式,將丟失的那批資料,寫個臨時程式,一點一點的查出來,然後重新灌入mq裡面去,把白天丟的資料給他補回來。也只能是這樣了。
假設1萬個訂單積壓在mq裡面,沒有處理,其中1000個訂單都丟了,你只能手動寫程式把那1000個訂單給查出來,手動發到mq裡去再補一次
-
MQ磁碟爆滿解決思路
如果走的方式是訊息積壓在mq裡,那麼如果你很長時間都沒處理掉,此時導致mq都快寫滿了,咋辦?這個還有別的辦法嗎?沒有,誰讓你第一個方案執行的太慢了,你臨時寫程式,接入資料來消費,消費一個丟棄一個,都不要了,快速消費掉所有的訊息。然後走第二個方案,到了晚上再補資料。
7 如何設計完善的訊息中介軟體系統
-
mq支援可伸縮性。就是需要的時候快速擴容,就可以增加吞吐量和容量,那怎麼搞?設計個分散式的系統唄,參照一下kafka的設計理念,broker -> topic -> partition,每個partition放一個機器,就存一部分資料。如果現在資源不夠了,簡單啊,給topic增加partition,然後做資料遷移,增加機器,不就可以存放更多資料,提供更高的吞吐量了?
-
mq支援資料落地磁碟。落磁碟,才能保證別程式掛了資料就丟了。那落磁碟的時候怎麼落啊?順序寫,這樣就沒有磁碟隨機讀寫的定址開銷,磁碟順序讀寫的效能是很高的,這就是kafka的思路。
-
mq支援可用性。採用多副本機制 -> leader & follower -> broker掛了,重新選舉leader即可對外服務。
-
mq支援資料0丟失,參考之前說的那個kafka資料零丟失方案
8 總結
感謝石杉的講義,結合大資料在我們工業大資料平臺的實踐,總結成一篇實踐指南,方便以後查閱反思,後續我會根據本篇部落格進行程式碼技術實踐實現。
秦凱新 於鄭州 201903022307