知識分享--訊息佇列

Shine-x發表於2020-03-26

什麼場景用到了訊息佇列

說到訊息佇列你腦子就要想到非同步、削峰、解耦,條件反射那種。

非同步:

  • 我們之前的場景裡面有很多步驟都是在一個流程裡面需要做完的,就比如說我的下單系統吧,本來我們業務簡單,下單了付了錢就好了,流程就走完了。
  • 但是後面來了個產品經理,搞了個優惠券系統,OK問題不大,流程裡面多100ms去扣減優惠券。
  • 後來產品經理靈光一閃說我們可以搞個積分系統啊,也行吧,流程裡面多了200ms去增減積分。
  • 再後來後來隔壁的產品老王說:下單成功後我們要給使用者發簡訊,也將就吧,100ms去發個簡訊。
  • 你們可以看到這才加了三個,我可以斬釘截鐵的告訴你真正的下單流程涉及的系統絕對在10個以上(主流電商),越大的越多。
  • 這個鏈路這樣下去,時間長得一批,使用者發現我買個東西你特麼要花幾十秒,垃圾電商我不在你這裡買了,不過要是都像並夕夕這麼便宜,真香
  • 但是我們公司沒有夕夕的那個經濟實力啊,那隻能優化系統了。
  • 那鏈路長了就慢了,但是我們發現上面的流程其實可以同時做的呀,你支付成功後,我去校驗優惠券的同時我可以去增減積分啊,還可以同時發個簡訊啊。
  • 那正常的流程我們是沒辦法實現的呀,怎麼辦,非同步
  • 你對比一下是不是發現,這樣子最多隻用100毫秒使用者知道下單成功了,至於簡訊你遲幾秒發給他他根本不在意是吧。

解耦:

  • 為啥我們不能用執行緒去做,因為用執行緒去做,你是不是要寫程式碼?
  • 你一個訂單流程,你扣積分,扣優惠券,發簡訊,扣庫存。。。等等這麼多業務要呼叫這麼多的介面,每次加一個你要呼叫一個介面然後還要重新發布系統,寫一次兩次還好,寫多了你就說:老子不幹了!
  • 而且真的全部都寫在一起的話,不單單是耦合這一個問題,你出問題排查也麻煩,流程裡面隨便一個地方出問題搞不好會影響到其他的點,小夥伴說我每個流程都try catch不就行了,相信我別這麼做,這樣的程式碼就像個定時炸彈,你不知道什麼時候爆炸,平時不炸偏偏在你做活動的時候炸,你就領個P0故障收拾書包提前回家過年吧。
  • 但是你用了訊息佇列,耦合這個問題就迎刃而解了呀。
  • 你下單了,你就把你 支付成功的訊息告訴別的系統 ,他們收到了去處理就好了,你只用走完自己的流程,把自己的訊息發出去,那後面要接入什麼系統簡單,直接訂閱你傳送的支付成功訊息,你支付成功了我 監聽就好了

削峰:

平時流量很低,但是你要做秒殺活動00 :00的時候流量瘋狂懟進來,你的伺服器,RedisMySQL各自的承受能力都不一樣,你直接全部流量照單全收肯定有問題啊,直接就打掛了。

  • 簡單,把請求放到佇列裡面,然後至於每秒消費多少請求,就看自己的伺服器處理能力,你能處理5000QPS你就消費這麼多,可能會比正常的慢一點,但是不至於打掛伺服器,等流量高峰下去了,你的服務也就沒壓力了。
  • 你看阿里雙十一12:00的時候這麼多流量瞬間湧進去,他有時候是不是會慢一點,但是人家沒掛啊,或者降級給你個友好的提示頁面,等高峰過去了又是一條好漢了。

問題

系統複雜性

本來蠻簡單的一個系統,我程式碼隨便寫都沒事,現在你憑空接入一箇中介軟體在那,我是不是要考慮去維護他,而且使用的過程中是不是要考慮各種問題,比如訊息重複消費訊息丟失訊息的順序消費等等,反正用了之後就是賊煩。

資料一致性

  • 這個其實是分散式服務本身就存在的一個問題,不僅僅是訊息佇列的問題,但是放在這裡說是因為用了訊息佇列這個問題會暴露得比較嚴重一點。
  • 你下單的服務自己保證自己的邏輯成功處理了,你成功發了訊息,但是優惠券系統,積分系統等等這麼多系統,他們成功還是失敗你就不管了?
  • 保證自己的業務資料對的就好了,其實還是比較不負責任的一種說法,這樣就像個渣男,沒有格局這樣呀你的路會越走越窄的

重複消費

一般訊息佇列的使用,我們都是有重試機制的,就是說我下游的業務發生異常了,我會丟擲異常並且要求你重新發一次

我這個活動這裡發生錯誤,你要求重發肯定沒問題。但是大家仔細想一下問題在哪裡?

是的,不止你一個人監聽這個訊息啊,還有別的服務也在監聽,他們也會失敗啊,他一失敗他也要求重發,但是你這裡其實是成功的,重發了,你的錢不就加了兩次了?

真實的情況其實重試是很正常的,服務的網路抖動開發人員程式碼Bug,還有資料問題等都可能處理失敗要求重發的。

一般我們叫這樣的處理叫介面冪等

冪等(idempotent、idempotence)是一個數學與計算機學概念,常見於抽象代數中。

在程式設計中一個冪等操作的特點是其任意多次執行所產生的影響均與一次執行的影響相同。

冪等函式,或冪等方法,是指可以使用相同引數重複執行,並能獲得相同結果的函式。這些函式不會影響系統狀態,也不用擔心重複執行會對系統造成改變。

例如,“setTrue()”函式就是一個冪等函式,無論多次執行,其結果都是一樣的.更復雜的操作冪等保證是利用唯一交易號(流水號)實現.

一般冪等,可以分場景去考慮,看是強校驗還是弱校驗,比如跟金錢相關的場景那就很關鍵呀,就做強校驗,別不是很重要的場景做弱校驗。

所有的服務都成功才能算這一次下單是成功的,那怎麼才能保證資料一致性呢?

分散式事務:把下單,優惠券,積分。。。都放在一個事務裡面一樣,要成功一起成功,要失敗一起失敗。

可用性

你搞個系統本身沒啥問題,你現在突然接入一箇中介軟體在那放著,萬一掛了怎麼辦?我下個單MQ掛了,優惠券不扣了,積分不減了,這不是殺一個程式設計師能搞定的吧,感覺得殺一片。

技術選型

市面上比較主流的訊息佇列中介軟體主要有,Kafka、ActiveMQ、RabbitMQ、RocketMQ 等這幾種

ActiveMQRabbitMQ這兩著因為吞吐量還有GitHub的社群活躍度的原因,在各大網際網路公司都已經基本上絕跡了,業務體量一般的公司會是有在用的,但是越來越多的公司更青睞RocketMQ和Kafka這樣的訊息中介軟體了。

KafkaRocketMQ一直在各自擅長的領域發光發亮,不過寫這篇文章的時候我問了螞蟻金服,位元組跳動和美團的朋友,好像大家用的都有點不一樣,應該都是各自的中介軟體,可能做過修改,也可能是自研的,大多沒有開源

就像我們公司就是是基於KafkaRocketMQ兩者的優點自研的訊息佇列中介軟體,吞吐量、可靠性、時效性等都很可觀。

知識分享--訊息佇列

就拿吞吐量來說,早期比較活躍的ActiveMQRabbitMQ基本上不是後兩者的對手了,在現在這樣大資料的年代吞吐量是真的很重要

比如現在突然爆發了一個超級熱點新聞,你的APP註冊使用者高達億數,你要想辦法第一時間把突發全部推送到每個人手上,你沒有大吞吐量的訊息佇列中介軟體用啥去推?

再說這些使用者大量湧進來看了你的新聞產生了一系列的附帶流量,你怎麼應對這些資料,很多場景離開訊息佇列基本上難以為繼

部署方式而言前兩者也是大不如後面兩個天然分散式架構的哥哥,都是高可用的分散式架構,而且資料多個副本的資料也能做到0丟失。

RabbitMQ這個中介軟體其實還行,但是這玩意開發語言居然是erlang,我敢說絕大部分工程師肯定不會為了一箇中介軟體去刻意學習一門語言的,開發維護成本你想都想不到,出個問題查都查半天。

至於RocketMQ(阿里開源的),git活躍度還可以。基本上你push了自己的bug確認了有問題都有阿里大佬跟你試試解答並修復的,我個人推薦的也是這個,他的架構設計部分跟同樣是阿里開源的一個RPC框架是真的很像(Dubbo)可能是因為師出同門的原因吧。

Kafka我放到最後說,你們也應該知道了,壓軸的這是個大哥,大資料領域,公司的日誌採集,實時計算等場景,都離不開他的身影,他基本上算得上是世界範圍級別的訊息佇列標杆了。

分享的問題

  • kafka節點之間如何複製備份的?
  • kafka訊息是否會丟失?為什麼?
  • kafka最合理的配置是什麼?
  • kafka的leader選舉機制是什麼?
  • kafka對硬體的配置有什麼要求?
  • kafka的訊息保證有幾種方式?

Kafka硬體配置推薦

kafka對cpu和HD要求不高,因為他不是密集型計算框架,他只是一個訊息佇列,吞吐量才是真諦。

但他對記憶體要求很高,一般情況下jvm設定幾個G就可以,但是頁快取會用的更多,記憶體大頁快取才大,吞吐量才能上去。

如何確定Kafka的分割槽數、key和consumer執行緒數?

1. 怎麼確定分割槽數?
“我應該選擇幾個分割槽?”——如果你在Kafka中國社群的群裡,這樣的問題你會經常碰到的。不過有些遺憾的是,我們似乎並沒有很權威的答案能夠解答這樣的問題。其實這也不奇怪,畢竟這樣的問題通常都是沒有固定答案的。Kafka官網上標榜自己是”high-throughput distributed messaging system”,即一個高吞吐量的分散式訊息引擎。那麼怎麼達到高吞吐量呢?Kafka在底層摒棄了Java堆快取機制,採用了作業系統級別的頁快取,同時將隨機寫操作改為順序寫,再結合Zero-Copy的特性極大地改善了IO效能。但是,這只是一個方面,畢竟單機優化的能力是有上限的。如何通過水平擴充套件甚至是線性擴充套件來進一步提升吞吐量呢? Kafka就是使用了分割槽(partition),通過將topic的訊息打散到多個分割槽並分佈儲存在不同的broker上實現了訊息處理(不管是producer還是consumer)的高吞吐量。
Kafka的生產者和消費者都可以多執行緒地並行操作,而每個執行緒處理的是一個分割槽的資料。因此分割槽實際上是調優Kafka並行度的最小單元。對於producer而言,它實際上是用多個執行緒併發地向不同分割槽所在的broker發起Socket連線同時給這些分割槽傳送訊息;而consumer呢,同一個消費組內的所有consumer執行緒都被指定topic的某一個分割槽進行消費(具體如何確定consumer執行緒數目我們後面會詳細說明)。所以說,如果一個topic分割槽越多,理論上整個叢集所能達到的吞吐量就越大。

  • 客戶端/伺服器端需要使用的記憶體就越多

    • 先說說客戶端的情況。Kafka 0.8.2之後推出了Java版的全新的producer,這個producer有個引數batch.size,預設是16KB。它會為每個分割槽快取訊息,一旦滿了就打包將訊息批量發出。看上去這是個能夠提升效能的設計。不過很顯然,因為這個引數是分割槽級別的,如果分割槽數越多,這部分快取所需的記憶體佔用也會更多。假設你有10000個分割槽,按照預設設定,這部分快取需要佔用約157MB的記憶體。而consumer端呢?我們拋開獲取資料所需的記憶體不說,只說執行緒的開銷。如果還是假設有10000個分割槽,同時consumer執行緒數要匹配分割槽數(大部分情況下是最佳的消費吞吐量配置)的話,那麼在consumer client就要建立10000個執行緒,也需要建立大約10000個Socket去獲取分割槽資料。這裡面的執行緒切換的開銷本身已經不容小覷了。
      伺服器端的開銷也不小,如果閱讀Kafka原始碼的話可以發現,伺服器端的很多元件都在記憶體中維護了分割槽級別的快取,比如controller,FetcherManager等,因此分割槽數越多,這種快取的成本越久越大。
  • 檔案控制程式碼的開銷

    • 每個分割槽在底層檔案系統都有屬於自己的一個目錄。該目錄下通常會有兩個檔案: base_offset.log和base_offset.index。Kafak的controller和ReplicaManager會為每個broker都儲存這兩個檔案控制程式碼(file handler)。很明顯,如果分割槽數越多,所需要保持開啟狀態的檔案控制程式碼數也就越多,最終可能會突破你的ulimit -n的限制。
  • 降低高可用性

    • Kafka通過副本(replica)機制來保證高可用。具體做法就是為每個分割槽儲存若干個副本(replica_factor指定副本數)。每個副本儲存在不同的broker上。期中的一個副本充當leader 副本,負責處理producer和consumer請求。其他副本充當follower角色,由Kafka controller負責保證與leader的同步。如果leader所在的broker掛掉了,contorller會檢測到然後在zookeeper的幫助下重選出新的leader——這中間會有短暫的不可用時間視窗,雖然大部分情況下可能只是幾毫秒級別。但如果你有10000個分割槽,10個broker,也就是說平均每個broker上有1000個分割槽。此時這個broker掛掉了,那麼zookeeper和controller需要立即對這1000個分割槽進行leader選舉。比起很少的分割槽leader選舉而言,這必然要花更長的時間,並且通常不是線性累加的。如果這個broker還同時是controller情況就更糟了。

但分割槽是否越多越好呢?顯然也不是,因為每個分割槽都有自己的開銷:

怎麼確定分割槽數

  • 視情況而定。基本上你還是需要通過一系列實驗和測試來確定。當然測試的依據應該是吞吐量。雖然LinkedIn這篇文章做了Kafka的基準測試,但它的結果其實對你意義不大,因為不同的硬體、軟體、負載情況測試出來的結果必然不一樣。我經常碰到的問題類似於,官網說每秒能到10MB,為什麼我的producer每秒才1MB? —— 且不說硬體條件,最後發現他使用的訊息體有1KB,而官網的基準測試是用100B測出來的,因此根本沒有可比性。不過你依然可以遵循一定的步驟來嘗試確定分割槽數:建立一個只有1個分割槽的topic,然後測試這個topic的producer吞吐量和consumer吞吐量。假設它們的值分別是Tp和Tc,單位可以是MB/s。然後假設總的目標吞吐量是Tt,那麼分割槽數 = Tt / max(Tp, Tc)
  • Tp表示producer的吞吐量。測試producer通常是很容易的,因為它的邏輯非常簡單,就是直接傳送訊息到Kafka就好了。Tc表示consumer的吞吐量。測試Tc通常與應用的關係更大, 因為Tc的值取決於你拿到訊息之後執行什麼操作,因此Tc的測試通常也要麻煩一些。
  • 另外,Kafka並不能真正地做到線性擴充套件(其實任何系統都不能),所以你在規劃你的分割槽數的時候最好多規劃一下,這樣未來擴充套件時候也更加方便。

訊息-分割槽的分配
預設情況下,Kafka根據傳遞訊息的key來進行分割槽的分配,即hash(key) % numPartitions

def partition(key: Any, numPartitions: Int): Int = {
       Utils.abs(key.hashCode) % numPartitions
}

這就保證了相同key的訊息一定會被路由到相同的分割槽。如果你沒有指定key,那麼Kafka是如何確定這條訊息去往哪個分割槽的呢?

if(key == null) { // 如果沒有指定key
    val id = sendPartitionPerTopicCache.get(topic) // 先看看Kafka有沒有快取的現成的分割槽Id
    id match {
    case Some(partitionId) =>
        partitionId // 如果有的話直接使用這個分割槽Id就好了
    case None => // 如果沒有的話,
        val availablePartitions = topicPartitionList.filter(_.leaderBrokerIdOpt.isDefined) //找出所有可用分割槽的leader所在的broker
    if (availablePartitions.isEmpty)
        throw new LeaderNotAvailableException("No leader for any partition in topic " + topic)
    val index = Utils.abs(Random.nextInt) % availablePartitions.size // 從中隨機挑一個   
    val partitionId = availablePartitions(index).partitionId
    sendPartitionPerTopicCache.put(topic, partitionId) // 更新快取以備下一次直接使用
    partitionId
   }
}

可以看出,Kafka幾乎就是隨機找一個分割槽傳送無key的訊息,然後把這個分割槽號加入到快取中以備後面直接使用——當然了,Kafka本身也會清空該快取(預設每10分鐘或每次請求topic後設資料時)

如何設定consumer執行緒數

我個人的觀點,如果你的分割槽數是N,那麼最好執行緒數也保持為N,這樣通常能夠達到最大的吞吐量。超過N的配置只是浪費系統資源,因為多出的執行緒不會被分配到任何分割槽。讓我們來看看具體Kafka是如何分配的。

  • topic下的一個分割槽只能被同一個consumer group下的一個consumer執行緒來消費,但反之並不成立,即一個consumer執行緒可以消費多個分割槽的資料,比如Kafka提供的ConsoleConsumer,預設就只是一個執行緒來消費所有分割槽的資料。——其實ConsoleConsumer可以使用萬用字元的功能實現同時消費多個topic資料,但這和本文無關。
  • 再討論分配策略之前,先說說KafkaStream——它是consumer的關鍵類,提供了遍歷方法用於consumer程式呼叫實現資料的消費。其底層維護了一個阻塞佇列,所以在沒有新訊息到來時,consumer是處於阻塞狀態的,表現出來的狀態就是consumer程式一直在等待新訊息的到來。——你當然可以配置成帶超時的consumer,具體參看引數consumer.timeout.ms的用法。

下面說說Kafka提供的兩種分配策略: range和roundrobin,由引數partition.assignment.strategy指定,預設是range策略。本文只討論range策略。所謂的range其實就是按照階段平均分配。
舉個例子就明白了,假設你有10個分割槽,P0 ~ P9,consumer執行緒數是3, C0 ~ C2,那麼每個執行緒都分配哪些分割槽呢?

C0 消費分割槽 0, 1, 2, 3
C1 消費分割槽 4, 5, 6
C2 消費分割槽 7, 8, 9

val nPartsPerConsumer = curPartitions.size / curConsumers.size // 每個consumer至少保證消費的分割槽數
val nConsumersWithExtraPart = curPartitions.size % curConsumers.size // 還剩下多少個分割槽需要單獨分配給開頭的執行緒們
...
for (consumerThreadId <- consumerThreadIdSet) { // 對於每一個consumer執行緒
val myConsumerPosition = curConsumers.indexOf(consumerThreadId) //算出該執行緒在所有執行緒中的位置,介於[0, n-1]
assert(myConsumerPosition >= 0)
// startPart 就是這個執行緒要消費的起始分割槽數
val startPart = nPartsPerConsumer * myConsumerPosition + myConsumerPosition.min(nConsumersWithExtraPart)
// nParts 就是這個執行緒總共要消費多少個分割槽
val nParts = nPartsPerConsumer + (if (myConsumerPosition + 1 > nConsumersWithExtraPart) 0 else 1)
...
}

針對於這個例子,nPartsPerConsumer就是10/3=3,nConsumersWithExtraPart為10%3=1,說明每個執行緒至少保證3個分割槽,還剩下1個分割槽需要單獨分配給開頭的若干個執行緒。這就是為什麼C0消費4個分割槽,後面的2個執行緒每個消費3個分割槽

Leader選舉

控制器(Broker)選舉

所謂控制器就是一個Borker,在一個kafka叢集中,有多個broker節點,但是它們之間需要選舉出一個leader,其他的broker充當follower角色。叢集中第一個啟動的broker會通過在zookeeper中建立臨時節點/controller來讓自己成為控制器,其他broker啟動時也會在zookeeper中建立臨時節點,但是發現節點已經存在,所以它們會收到一個異常,意識到控制器已經存在,那麼就會在zookeeper中建立watch物件,便於它們收到控制器變更的通知。

那麼如果控制器由於網路原因與zookeeper斷開連線或者異常退出,那麼其他broker通過watch收到控制器變更的通知,就會去嘗試建立臨時節點/controller,如果有一個broker建立成功,那麼其他broker就會收到建立異常通知,也就意味著叢集中已經有了控制器,其他broker只需建立watch物件即可。

如果叢集中有一個broker發生異常退出了,那麼控制器就會檢查這個broker是否有分割槽的副本leader,如果有那麼這個分割槽就需要一個新的leader,此時控制器就會去遍歷其他副本,決定哪一個成為新的leader,同時更新分割槽的ISR集合。

如果有一個broker加入叢集中,那麼控制器就會通過Broker ID去判斷新加入的broker中是否含有現有分割槽的副本,如果有,就會從分割槽副本中去同步資料。

叢集中每選舉一次控制器,就會通過zookeeper建立一個controller epoch,每一個選舉都會建立一個更大,包含最新資訊的epoch,如果有broker收到比這個epoch舊的資料,就會忽略它們,kafka也通過這個epoch來防止叢集產生“腦裂”。

分割槽副本選舉機制

在kafka的叢集中,會存在著多個主題topic,在每一個topic中,又被劃分為多個partition,為了防止資料不丟失,每一個partition又有多個副本,在整個叢集中,總共有三種副本角色:

首領副本(leader):也就是leader主副本,每個分割槽都有一個首領副本,為了保證資料一致性,所有的生產者與消費者的請求都會經過該副本來處理。
跟隨者副本(follower):除了首領副本外的其他所有副本都是跟隨者副本,跟隨者副本不處理來自客戶端的任何請求,只負責從首領副本同步資料,保證與首領保持一致。如果首領副本發生崩潰,就會從這其中選舉出一個leader。
首選首領副本:建立分割槽時指定的首選首領。如果不指定,則為分割槽的第一個副本。
follower需要從leader中同步資料,但是由於網路或者其他原因,導致資料阻塞,出現不一致的情況,為了避免這種情況,follower會向leader傳送請求資訊,這些請求資訊中包含了follower需要資料的偏移量offset,而且這些offset是有序的。

如果有follower向leader傳送了請求1,接著傳送請求2,請求3,那麼再傳送請求4,這時就意味著follower已經同步了前三條資料,否則不會傳送請求4。leader通過跟蹤 每一個follower的offset來判斷它們的複製進度。

預設的,如果follower與leader之間超過10s內沒有傳送請求,或者說沒有收到請求資料,此時該follower就會被認為“不同步副本”。而持續請求的副本就是“同步副本”,當leader發生故障時,只有“同步副本”才可以被選舉為leader。其中的請求超時時間可以通過引數replica.lag.time.max.ms引數來配置。

我們希望每個分割槽的leader可以分佈到不同的broker中,儘可能的達到負載均衡,所以會有一個首選首領,如果我們設定引數auto.leader.rebalance.enable為true,那麼它會檢查首選首領是否是真正的首領,如果不是,則會觸發選舉,讓首選首領成為首領。

消費組選主

在kafka的消費端,會有一個消費者協調器以及消費組,組協調器GroupCoordinator需要為消費組內的消費者選舉出一個消費組的leader,那麼如何選舉的呢?

如果消費組內還沒有leader,那麼第一個加入消費組的消費者即為消費組的leader,如果某一個時刻leader消費者由於某些原因退出了消費組,那麼就會重新選舉leader,如何選舉?

private val members = new mutable.HashMap[String, MemberMetadata]
leaderId = members.keys.headOption

上面程式碼是kafka原始碼中的部分程式碼,member是一個hashmap的資料結構,key為消費者的member_id,value是後設資料資訊,那麼它會將leaderId選舉為Hashmap中的第一個鍵值對,它和隨機基本沒啥區別。

對於整個選舉演算法的詳情需要先了解Raft選舉演算法,kafka是基於該演算法來實現leader選舉的

訊息傳送方式

先了解Kafka訊息的傳送方式。

  • Kafka訊息傳送分同步(sync)、非同步(async)兩種方式
  • 預設是使用同步方式,可通過producer.type屬性進行配置;
  • Kafka保證訊息被安全生產,有三個選項分別是0,1,-1
  • 通過request.required.acks屬性進行配置:
    • 0代表:不進行訊息接收是否成功的確認(預設值);
    • 1代表:當Leader副本接收成功後,返回接收成功確認資訊;
    • -1代表:當Leader和Follower副本都接收成功後,返回接收成功確認資訊;

六種傳送場景

兩個維度相交,生成六種情況,如下圖:

知識分享--訊息佇列

訊息丟失的場景

  • 網路異常
    • acks設定為0時,不和Kafka叢集進行訊息接受確認,當網路發生異常等情況時,存在訊息丟失的可能;
  • 客戶端異常
    • 非同步傳送時,訊息並沒有直接傳送至Kafka叢集,而是在Client端按一定規則快取並批量傳送。在這期間,如果客戶端發生當機等情況,都會導致訊息的丟失;
  • 緩衝區滿了
    • 非同步傳送時,Client端快取的訊息超出了緩衝池的大小,也存在訊息丟失的可能;
  • Leader副本異常
    • acks設定為1時,Leader副本接收成功,Kafka叢集就返回成功確認資訊,而Follower副本可能還在同步。這時Leader副本突然出現異常,新Leader副本(原Follower副本)未能和其保持一致,就會出現訊息丟失的情況;

以上就是訊息丟失的幾種情況,在日常應用中,我們需要結合自身的應用場景來選擇不同的配置。

訊息消費

  • kafka訊息消費有兩個consumer介面,Low-level API和High-level API:
    • Low-level API:消費者自己維護offset等值,可以實現對Kafka的完全控制;
    • High-level API:封裝了對parition和offset的管理,使用簡單;

如果使用高階介面High-level API,可能存在一個問題就是當訊息消費者從叢集中把訊息取出來、並提交了新的訊息offset值後,還沒來得及消費就掛掉了,那麼下次再消費時之前沒消費成功的訊息就“詭異”的消失了;

解決辦法:

  • 針對訊息丟失:同步模式下,確認機制設定為-1,即讓訊息寫入Leader和Follower之後再確認訊息傳送成功;非同步模式下,為防止緩衝區滿,可以在配置檔案設定不限制阻塞超時時間,當緩衝區滿時讓生產者一直處於阻塞狀態;
  • 針對訊息重複:將訊息的唯一標識儲存到外部介質中,每次消費時判斷是否處理過即可。

複製備份

備份機制是Kafka0.8版本的新特性,備份機制的出現大大提高了Kafka叢集的可靠性、穩定性。有了備份機制後,Kafka允許叢集中的節點掛掉後而不影響整個叢集工作。

一個備份數量為n的叢集允許n-1個節點失敗。在所有備份節點中,有一個節點作為lead節點,這個節點儲存了其它備份節點列表,並維持各個備份間的狀體同步。

Kafka訊息備份分三個角色:分別是Leader副本、Follower副本、ISR集合

  • Leader副本:負責直接響應client端的讀寫請求,即和生產者和消費者直接對接,生產者生產一條訊息,直接進入Leader副本;
  • Follower副本:作為特殊消費者,被動的接收leader副本中的資料。注意:follower副本不能響應client端的讀寫請求;
  • ISR集合:與leader保持同步的follower,屬於ISR副本集合(同步的備份集合),反過來說,在某個時刻,還在被動接收接收,不是和leader完全一致的,不能屬於ISR副本集合,同步完成後才屬於ISR集合;

ISR集合作用

在當前Leader不可用時,Kafka叢集會從ISR集合中選取一個Follower升級為新Leader;通過維護ISR集合,一個擁有(N+1)個備份的Topic可用容忍N個備份不可用

複製提供了高可用性,Producer繼續釋出訊息,Consumer繼續接受訊息。
Leader處理此分割槽的所有的讀寫請求,而follower被動的複製資料。(所有的寫都發給leader, 然後leader將訊息發給follower。)

一個Broker既可能是一個分割槽的leader,也可能是另一個分割槽的slave。kafka實際是保證在足夠多的slave寫入成功的情況下就認為訊息寫入成功,而不是全部寫入成功

Kafka引入了 ISR的概念。ISR是in-sync replicas的簡寫。ISR的副本保持和leader的同步,當然leader本身也在ISR中。初始狀態所有的副本都處於ISR中,當一個訊息傳送給leader的時候,leader會等待ISR中所有的副本告訴它已經接收了這個訊息,如果一個副本失敗了,那麼它會被移除ISR。下一條訊息來的時候,leader就會將訊息傳送給當前的ISR中節點了。

kafka使用Zookeeper實現leader選舉。如果leader失敗,controller會從ISR選出一個新的leader。

Kafka引入了 ISR的概念。ISR是in-sync replicas的簡寫。ISR的副本保持和leader的同步,當然leader本身也在ISR中。初始狀態所有的副本都處於ISR中,當一個訊息傳送給leader的時候,leader會等待ISR中所有的副本告訴它已經接收了這個訊息,如果一個副本失敗了,那麼它會被移除ISR。下一條訊息來的時候,leader就會將訊息傳送給當前的ISR中節點了。

支援豐富的功能

  • 釋出/訂閱訊息傳遞模型
  • 財務級交易訊息
  • 各種跨語言客戶端,例如Java,C / C ++,Python,Go
  • 可插拔的傳輸協議,例如TCP,SSL,AIO
  • 內建的訊息跟蹤功能,還支援開放式跟蹤
  • 多功能的大資料和流生態系統整合
  • 按時間或偏移量追溯訊息
  • 可靠的FIFO和嚴格的有序訊息傳遞在同一佇列中
  • 高效的推拉消費模型
  • 單個佇列中的百萬級訊息累積容量
  • 多種訊息傳遞協議,例如JMS和OpenMessaging
  • 靈活的分散式橫向擴充套件部署架構
  • 快如閃電的批量訊息交換系統
  • 各種訊息過濾器機制,例如SQL和Tag
  • 用於隔離測試和雲隔離群集的Docker映像
  • 功能豐富的管理儀表板,用於配置,指標和監視
  • 認證與授權

他的核心模組:

  • rocketmq-broker:接受生產者發來的訊息並儲存(通過呼叫rocketmq-store),消費者從這裡取得訊息
  • rocketmq-client:提供傳送、接受訊息的客戶端API。
  • rocketmq-namesrv:NameServer,類似於Zookeeper,這裡儲存著訊息的TopicName,佇列等執行時的元資訊。
  • rocketmq-common:通用的一些類,方法,資料結構等。
  • rocketmq-remoting:基於Netty4的client/server + fastjson序列化 + 自定義二進位制協議。
  • rocketmq-store:訊息、索引儲存等。
  • rocketmq-filtersrv:訊息過濾器Server,需要注意的是,要實現這種過濾,需要上傳程式碼到MQ!(一般而言,我們利用Tag足以滿足大部分的過濾需求,如果更靈活更復雜的過濾需求,可以考慮filtersrv元件)。
  • rocketmq-tools:命令列工具。

部署架構

知識分享--訊息佇列
Tip:我們可以看到RocketMQ啥都是叢集部署的,這是他吞吐量大高可用的原因之一,叢集的模式也很花哨,可以支援多master 模式、多master多slave非同步複製模式、多 master多slave同步雙寫模式。

NameServer

主要負責對於源資料的管理,包括了對於Topic和路由資訊的管理。

NameServer是一個功能齊全的伺服器,其角色類似Dubbo中的Zookeeper,但NameServer與Zookeeper相比更輕量。主要是因為每個NameServer節點互相之間是獨立的,沒有任何資訊互動。

NameServer壓力不會太大,平時主要開銷是在維持心跳和提供Topic-Broker的關係資料。

但有一點需要注意,Broker向NameServer發心跳時, 會帶上當前自己所負責的所有Topic資訊,如果Topic個數太多(萬級別),會導致一次心跳中,就Topic的資料就幾十M,網路情況差的話, 網路傳輸失敗,心跳失敗,導致NameServer誤認為Broker心跳失敗。

NameServer 被設計成幾乎無狀態的,可以橫向擴充套件,節點之間相互之間無通訊,通過部署多臺機器來標記自己是一個偽叢集。

每個 Broker 在啟動的時候會到 NameServer 註冊,Producer 在傳送訊息前會根據 Topic 到 NameServer 獲取到 Broker 的路由資訊,Consumer 也會定時獲取 Topic 的路由資訊。

所以從功能上看NameServer應該是和 ZooKeeper 差不多,據說 RocketMQ 的早期版本確實是使用的 ZooKeeper ,後來改為了自己實現的 NameServer 。

Producer

訊息生產者,負責產生訊息,一般由業務系統負責產生訊息。

  • Producer由使用者進行分散式部署,訊息由Producer通過多種負載均衡模式傳送到Broker叢集,傳送低延時,支援快速失敗。
  • RocketMQ 提供了三種方式傳送訊息:同步、非同步和單向
  • 同步傳送:同步傳送指訊息傳送方發出資料後會在收到接收方發回響應之後才發下一個資料包。一般用於重要通知訊息,例如重要通知郵件、營銷簡訊。
  • 非同步傳送:非同步傳送指傳送方發出資料後,不等接收方發回響應,接著傳送下個資料包,一般用於可能鏈路耗時較長而對響應時間敏感的業務場景,例如使用者視訊上傳後通知啟動轉碼服務。
  • 單向傳送:單向傳送是指只負責傳送訊息而不等待伺服器迴應且沒有回撥函式觸發,適用於某些耗時非常短但對可靠性要求並不高的場景,例如日誌收集。

Broker

訊息中轉角色,負責儲存訊息,轉發訊息。

  • Broker是具體提供業務的伺服器,單個Broker節點與所有的NameServer節點保持長連線及心跳,並會定時將Topic資訊註冊到NameServer,順帶一提底層的通訊和連線都是基於Netty實現的。
  • Broker負責訊息儲存,以Topic為緯度支援輕量級的佇列,單機可以支撐上萬佇列規模,支援訊息推拉模型。
  • 官網上有資料顯示:具有上億級訊息堆積能力,同時可嚴格保證訊息的有序性

Consumer

訊息消費者,負責消費訊息,一般是後臺系統負責非同步消費。

  • Consumer也由使用者部署,支援PUSH和PULL兩種消費模式,支援叢集消費廣播訊息,提供實時的訊息訂閱機制
  • Pull:拉取型消費者(Pull Consumer)主動從訊息伺服器拉取資訊,只要批量拉取到訊息,使用者應用就會啟動消費過程,所以 Pull 稱為主動消費型。
  • Push:推送型消費者(Push Consumer)封裝了訊息的拉取、消費進度和其他的內部維護工作,將訊息到達時執行的回撥介面留給使用者應用程式來實現。所以 Push 稱為被動消費型別,但從實現上看還是從訊息伺服器中拉取訊息,不同於 Pull 的是 Push 首先要註冊消費監聽器,當監聽器處觸發後才開始消費訊息。

訊息領域模型

知識分享--訊息佇列

Message

Message(訊息)就是要傳輸的資訊。
一條訊息必須有一個主題(Topic),主題可以看做是你的信件要郵寄的地址。

一條訊息也可以擁有一個可選的標籤(Tag)和額處的鍵值對,它們可以用於設定一個業務 Key 並在 Broker 上查詢此訊息以便在開發期間查詢問題。

Topic

Topic(主題)可以看做訊息的規類,它是訊息的第一級型別。比如一個電商系統可以分為:交易訊息、物流訊息等,一條訊息必須有一個 Topic 。

Topic 與生產者和消費者的關係非常鬆散,一個 Topic 可以有0個、1個、多個生產者向其傳送訊息,一個生產者也可以同時向不同的 Topic 傳送訊息。

一個 Topic 也可以被 0個、1個、多個消費者訂閱。

Tag

Tag(標籤)可以看作子主題,它是訊息的第二級型別,用於為使用者提供額外的靈活性。使用標籤,同一業務模組不同目的的訊息就可以用相同 Topic 而不同的 Tag 來標識。比如交易訊息又可以分為:交易建立訊息、交易完成訊息等,一條訊息可以沒有 Tag

標籤有助於保持您的程式碼乾淨和連貫,並且還可以為 RocketMQ 提供的查詢系統提供幫助。

Group

分組,一個組可以訂閱多個Topic。

分為ProducerGroup,ConsumerGroup,代表某一類的生產者和消費者,一般來說同一個服務可以作為Group,同一個Group一般來說傳送和消費的訊息都是一樣的

Queue

Kafka中叫Partition,每個Queue內部是有序的,在RocketMQ中分為讀和寫兩種佇列,一般來說讀寫佇列數量一致,如果不一致就會出現很多問題。

Message Queue

Message Queue(訊息佇列),主題被劃分為一個或多個子主題,即訊息佇列。

一個 Topic 下可以設定多個訊息佇列,傳送訊息時執行該訊息的 Topic ,RocketMQ 會輪詢該 Topic 下的所有佇列將訊息發出去。

訊息的物理管理單位。一個Topic下可以有多個Queue,Queue的引入使得訊息的儲存可以分散式叢集化,具有了水平擴充套件能力。

Offset

RocketMQ 中,所有訊息佇列都是持久化,長度無限的資料結構,所謂長度無限是指佇列中的每個儲存單元都是定長,訪問其中的儲存單元使用Offset 來訪問,Offset 為 java long 型別,64 位,理論上在 100年內不會溢位,所以認為是長度無限。

也可以認為 Message Queue 是一個長度無限的陣列,Offset 就是下標。

訊息消費模式

訊息消費模式有兩種:Clustering(叢集消費)和Broadcasting(廣播消費)。

預設情況下就是叢集消費,該模式下一個消費者叢集共同消費一個主題的多個佇列,一個佇列只會被一個消費者消費,如果某個消費者掛掉,分組內其它消費者會接替掛掉的消費者繼續消費。

而廣播消費訊息會發給消費者組中的每一個消費者進行消費。

Message Order

Message Order(訊息順序)有兩種:Orderly(順序消費)和Concurrently(並行消費)。

順序消費表示訊息消費的順序同生產者為每個訊息佇列傳送的順序一致,所以如果正在處理全域性順序是強制性的場景,需要確保使用的主題只有一個訊息佇列。

並行消費不再保證訊息順序,消費的最大並行數量受每個消費者客戶端指定的執行緒池限制。

一次完整的通訊流程是怎樣的?

Producer 與 NameServer叢集中的其中一個節點(隨機選擇)建立長連線,定期從 NameServer 獲取 Topic 路由資訊,並向提供 Topic 服務的 Broker Master 建立長連線,且定時向 Broker 傳送心跳。

Producer 只能將訊息傳送到 Broker master,但是 Consumer 則不一樣,它同時和提供 Topic 服務的 Master 和 Slave建立長連線,既可以從 Broker Master 訂閱訊息,也可以從 Broker Slave 訂閱訊息。

具體如下圖:

知識分享--訊息佇列

NameService啟動流程

在org.apache.rocketmq.namesrv目錄下的NamesrvStartup這個啟動類基本上描述了他的啟動過程我們可以看一下程式碼:

  • 第一步是初始化配置
  • 建立NamesrvController例項,並開啟兩個定時任務:
  • 每隔10s掃描一次Broker,移除處於不啟用的Broker
  • 每隔10s列印一次KV配置。
  • 第三步註冊鉤子函式,啟動伺服器並監聽Broker。

NameService還有很多東西的哈我這裡就介紹他的啟動流程,大家還可以去看看程式碼,還是很有意思的,比如路由註冊會傳送心跳包,還有心跳包的處理流程路由刪除路由發現等等。

Tip:本來我想貼很多原始碼的,後面跟歪歪(Java3y)討論了很久做出了不貼的決定,大家理解過程為主!我主要是做只是掃盲還有一些痛點分析嘛,深究還是得大家花時間,我要啥都介紹篇幅就不夠了。

Producer

鏈路很長涉及的細節也多,看一下鏈路圖。

知識分享--訊息佇列

Producer是訊息傳送方,那他怎麼傳送的呢?

通過輪訓,Producer輪訓某個Topic下面的所有佇列實現傳送方的負載均衡

知識分享--訊息佇列

Broker

Broker在RocketMQ中是進行處理Producer傳送訊息請求,Consumer消費訊息的請求,並且進行訊息的持久化,以及HA策略和服務端過濾,就是叢集中很重的工作都是交給了Broker進行處理。

Broker模組是通過BrokerStartup進行啟動的,會例項化BrokerController,並且呼叫其初始化方法

知識分享--訊息佇列

大家去看Broker的原始碼的話會發現,他的初始化流程很冗長,會根據配置建立很多執行緒池主要用來傳送訊息拉取訊息查詢訊息客戶端管理消費者管理,也有很多定時任務,同時也註冊了很多請求處理器,用來傳送拉取訊息查詢訊息的。

Consumer

知識分享--訊息佇列

Consumer是訊息接受,那他怎麼接收訊息的呢?

知識分享--訊息佇列

消費端會通過RebalanceService執行緒,10秒鐘做一次基於Topic下的所有佇列負載。

總結一下

RocketMQ優點:

  • 單機吞吐量:十萬級
  • 可用性:非常高,分散式架構
  • 訊息可靠性:經過引數優化配置,訊息可以做到0丟失
  • 功能支援:MQ功能較為完善,還是分散式的,擴充套件性好
  • 支援10億級別的訊息堆積,不會因為堆積導致效能下降
  • 原始碼是java,我們可以自己閱讀原始碼,定製自己公司的MQ,可以掌控
  • 天生為金融網際網路領域而生,對於可靠性要求很高的場景,尤其是電商裡面的訂單扣款,以及業務削峰,在大量交易湧入時,後端可能無法及時處理的情況
  • RoketMQ在穩定性上可能更值得信賴,這些業務場景在阿里雙11已經經歷了多次考驗,如果你的業務有上述併發場景,建議可以選擇RocketMQ

RocketMQ缺點:

  • 支援的客戶端語言不多,目前是java及c++,其中c++不成熟
  • 社群活躍度不是特別活躍那種
  • 沒有在 mq 核心中去實現JMS等介面,有些系統要遷移需要修改大量程式碼

訊息去重

去重原則:使用業務端邏輯保持冪等性

冪等性:就是使用者對於同一操作發起的一次請求或者多次請求的結果是一致的,不會因為多次點選而產生了副作用,資料庫的結果都是唯一的,不可變的。

只要保持冪等性,不管來多少條重複訊息,最後處理的結果都一樣,需要業務端來實現。

去重策略:保證每條訊息都有唯一編號(比如唯一流水號),且保證訊息處理成功與去重表的日誌同時出現。

建立一個訊息表,拿到這個訊息做資料庫的insert操作。給這個訊息做一個唯一主鍵(primary key)或者唯一約束,那麼就算出現重複消費的情況,就會導致主鍵衝突,那麼就不再處理這條訊息。

訊息重複

訊息領域有一個對訊息投遞的QoS定義,分為:

  • 最多一次(At most once)
  • 至少一次(At least once)
  • 僅一次( Exactly once)

QoS:Quality of Service,服務質量

幾乎所有的MQ產品都聲稱自己做到了At least once

既然是至少一次,那避免不了訊息重複,尤其是在分散式網路環境下。

比如:網路原因閃斷,ACK返回失敗等等故障,確認資訊沒有傳送到訊息佇列,導致訊息佇列不知道自己已經消費過該訊息了,再次將該訊息分發給其他的消費者。

不同的訊息佇列傳送的確認資訊形式不同,例如RabbitMQ是傳送一個ACK確認訊息,RocketMQ是返回一個CONSUME_SUCCESS成功標誌,Kafka實際上有個offset的概念。

RocketMQ沒有內建訊息去重的解決方案,最新版本是否支援還需確認。

訊息的可用性

當我們選擇好了叢集模式之後,那麼我們需要關心的就是怎麼去儲存和複製這個資料,RocketMQ對訊息的刷盤提供了同步和非同步的策略來滿足我們的,當我們選擇同步刷盤之後,如果刷盤超時會給返回FLUSH_DISK_TIMEOUT,如果是非同步刷盤不會返回刷盤相關資訊,選擇同步刷盤可以盡最大程度滿足我們的訊息不會丟失。

除了儲存有選擇之後,我們的主從同步提供了同步和非同步兩種模式來進行復制,當然選擇同步可以提升可用性,但是訊息的傳送RT時間會下降10%左右。

RocketMQ採用的是混合型的儲存結構,即為Broker單個例項下所有的佇列共用一個日誌資料檔案(即為CommitLog)來儲存。

Kafka採用的是獨立型的儲存結構,每個佇列一個檔案。

這裡帥丙認為,RocketMQ採用混合型儲存結構的缺點在於,會存在較多的隨機讀操作,因此讀的效率偏低。同時消費訊息需要依賴ConsumeQueue,構建該邏輯消費佇列需要一定開銷。

RocketMQ 刷盤實現

Broker 在訊息的存取時直接操作的是記憶體(記憶體對映檔案),這可以提供系統的吞吐量,但是無法避免機器掉電時資料丟失,所以需要持久化到磁碟中。

刷盤的最終實現都是使用NIO中的 MappedByteBuffer.force() 將對映區的資料寫入到磁碟,如果是同步刷盤的話,在Broker把訊息寫到CommitLog對映區後,就會等待寫入完成。

非同步而言,只是喚醒對應的執行緒,不保證執行的時機,流程如圖所示。

知識分享--訊息佇列

順序訊息

我簡單的說一下我們使用的RocketMQ裡面的一個簡單實現吧。

Tip:為啥用RocketMQ舉例呢,這玩意是阿里開源的,我問了下身邊的朋友很多公司都有使用,所以讀者大概率是這個的話我就用這個舉例吧,具體的細節我後面會在RocketMQKafka各自章節說到。

生產者消費者一般需要保證順序訊息的話,可能就是一個業務場景下的,比如訂單的建立、支付、發貨、收貨。

那這些東西是不是一個訂單號呢?一個訂單的肯定是一個訂單號的說,那簡單了呀。

一個topic下有多個佇列,為了保證傳送有序,RocketMQ提供了MessageQueueSelector佇列選擇機制,他有三種實現:

知識分享--訊息佇列

我們可使用Hash取模法,讓同一個訂單傳送到同一個佇列中,再使用同步傳送,只有同個訂單的建立訊息傳送成功,再傳送支付訊息。這樣,我們保證了傳送有序。

RocketMQ的topic內的佇列機制,可以保證儲存滿足FIFO(First Input First Output 簡單說就是指先進先出),剩下的只需要消費者順序消費即可。

RocketMQ僅保證順序傳送,順序消費由消費者業務保證!!!

這裡很好理解,一個訂單你傳送的時候放到一個佇列裡面去,你同一個的訂單號Hash一下是不是還是一樣的結果,那肯定是一個消費者消費,那順序是不是就保證了?

真正的順序消費不同的中介軟體都有自己的不同實現我這裡就舉個例子

分散式事務

Half Message(半訊息)

是指暫不能被Consumer消費的訊息。Producer 已經把訊息成功傳送到了 Broker 端,但此訊息被標記為暫不能投遞狀態,處於該種狀態下的訊息稱為半訊息。需要 Producer

對訊息的二次確認後,Consumer才能去消費它。

訊息回查

由於網路閃段,生產者應用重啟等原因。導致 Producer 端一直沒有對 Half Message(半訊息) 進行 二次確認。這是Brock伺服器會定時掃描長期處於半訊息的訊息,會

主動詢問 Producer端 該訊息的最終狀態(Commit或者Rollback),該訊息即為 訊息回查

知識分享--訊息佇列

  1. A服務先傳送個Half Message給Brock端,訊息中攜帶 B服務 即將要+100元的資訊。
  2. 當A服務知道Half Message傳送成功後,那麼開始第3步執行本地事務。
  3. 執行本地事務(會有三種情況1、執行成功。2、執行失敗。3、網路等原因導致沒有響應)
  4. 如果本地事務成功,那麼Product像Brock伺服器傳送Commit,這樣B服務就可以消費該message。
  5. 如果本地事務失敗,那麼Product像Brock伺服器傳送Rollback,那麼就會直接刪除上面這條半訊息。
  6. 如果因為網路等原因遲遲沒有返回失敗還是成功,那麼會執行RocketMQ的回撥介面,來進行事務的回查。

訊息過濾

  • Broker端訊息過濾  
    Broker中,按照Consumer的要求做過濾,優點是減少了對於Consumer無用訊息的網路傳輸。缺點是增加了Broker的負擔,實現相對複雜。
  • Consumer端訊息過濾
    這種過濾方式可由應用完全自定義實現,但是缺點是很多無用的訊息要傳輸到Consumer端。

Broker的Buffer問題

Broker的Buffer通常指的是Broker中一個佇列的記憶體Buffer大小,這類Buffer通常大小有限。

另外,RocketMQ沒有記憶體Buffer概念,RocketMQ的佇列都是持久化磁碟,資料定期清除。

RocketMQ同其他MQ有非常顯著的區別,RocketMQ的記憶體Buffer抽象成一個無限長度的佇列,不管有多少資料進來都能裝得下,這個無限是有前提的,Broker會定期刪除過期的資料。

例如Broker只儲存3天的訊息,那麼這個Buffer雖然長度無限,但是3天前的資料會被從隊尾刪除。

回溯消費

回溯消費是指Consumer已經消費成功的訊息,由於業務上的需求需要重新消費,要支援此功能,Broker在向Consumer投遞成功訊息後,訊息仍然需要保留。並且重新消費一般是按照時間維度。

例如由於Consumer系統故障,恢復後需要重新消費1小時前的資料,那麼Broker要提供一種機制,可以按照時間維度來回退消費進度。

RocketMQ支援按照時間回溯消費,時間維度精確到毫秒,可以向前回溯,也可以向後回溯。

訊息堆積

訊息中介軟體的主要功能是非同步解耦,還有個重要功能是擋住前端的資料洪峰,保證後端系統的穩定性,這就要求訊息中介軟體具有一定的訊息堆積能力,訊息堆積分以下兩種情況:

  • 訊息堆積在記憶體Buffer,一旦超過記憶體Buffer,可以根據一定的丟棄策略來丟棄訊息,如CORBA Notification規範中描述。適合能容忍丟棄訊息的業務,這種情況訊息的堆積能力主要在於記憶體Buffer大小,而且訊息堆積後,效能下降不會太大,因為記憶體中資料多少對於對外提供的訪問能力影響有限。
  • 訊息堆積到持久化儲存系統中,例如DB,KV儲存,檔案記錄形式。當訊息不能在記憶體Cache命中時,要不可避免的訪問磁碟,會產生大量讀IO,讀IO的吞吐量直接決定了訊息堆積後的訪問能力。
  • 評估訊息堆積能力主要有以下四點:
  • 訊息能堆積多少條,多少位元組?即訊息的堆積容量。
  • 訊息堆積後,發訊息的吞吐量大小,是否會受堆積影響?
  • 訊息堆積後,正常消費的Consumer是否會受影響?
  • 訊息堆積後,訪問堆積在磁碟的訊息時,吞吐量有多大?

定時訊息

定時訊息是指訊息發到Broker後,不能立刻被Consumer消費,要到特定的時間點或者等待特定的時間後才能被消費。

如果要支援任意的時間精度,在Broker層面,必須要做訊息排序,如果再涉及到持久化,那麼訊息排序要不可避免的產生巨大效能開銷。

RocketMQ支援定時訊息,但是不支援任意時間精度,支援特定的level,例如定時5s,10s,1m等。

出處:北京英浦教育

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章