metaq原理

概念和術語


  • 消息,全稱爲Message,是指在生產者、服務端和消費者之間傳輸數據。

  • 消息代理:全稱爲Message Broker,通俗來講就是指該MQ的服務端或者說服務器。

  • 消息生產者:全稱爲Message Producer,負責產生消息併發送消息到meta服務器。

  • 消息消費者:全稱爲Message Consumer,負責消息的消費。

  • 消息的主題:全稱爲Message Topic,由用戶定義並在Broker上配置。producer發送消息到某個topic下,consumer從某個topic下消費消息。

  • 主題的分區:也稱爲partition,可以把一個topic分爲多個分區。每個分區是一個有序,不可變的,順序遞增的commit log

  • 消費者分組:全稱爲Consumer Group,由多個消費者組成,共同消費一個topic下的消息,每個消費者消費部分消息。這些消費者就組成一個分組,擁有同一個分組名稱,通常也稱爲消費者集羣

  • 偏移量:全稱爲Offset。分區中的消息都有一個遞增的id,我們稱之爲Offset。它唯一標識了分區中的消息。

基本工作機制
架構示意
095549uk8vvvjvgjaz2oyo.png

DSC0000.png (89.49 KB, 下載次數: 0)

下載附件  保存到相冊

前天 17:55 上傳


從上圖可以看出,有4個集羣。其中,Broker集羣存在MASTER-SLAVE結構。
多臺broker組成一個集羣提供一些topic服務,生產者集羣可以按照一定的路由規則往集羣裏某臺broker的某個topic發送消息,消費者集羣按照一定的路由規則拉取某臺broker上的消息。
生產者,Broker,消費者處理消息過程
每個broker都可以配置一個topic可以有多少個分區,但是在生產者看來,一個topic在所有broker上的的所有分區組成一個分區列表來使用。
在創建producer的時候,生產者會從zookeeper上獲取publish的topic對應的broker和分區列表。生產者在通過zk獲取分區列表之後,會按照brokerId和partition的順序排列組織成一個有序的分區列表,發送的時候按照從頭到尾循環往復的方式選擇一個分區來發送消息。
如果你想實現自己的負載均衡策略,可以實現相應的負載均衡策略接口。
消息生產者發送消息後返回處理結果,結果分爲成功,失敗和超時。
Broker在接收消息後,依次進行校驗和檢查,寫入磁盤,向生產者返回處理結果。
消費者在每次消費消息時,首先把offset加1,然後根據該偏移量找到相應的消息,然後開始消費。只有在成功消費一條消息後纔會接着消費下一條。如果在消費某條消息失敗(如異常),則會嘗試重試消費這條消息,超過最大次數後仍然無法消費,則將消息存儲在消費者的本地磁盤,由後臺線程繼續做重試。而主線程繼續往後走,消費後續的消息。
DFX
順序性
順序性是指如果發送消息的順序是A、B、C,那麼消費者消費的順序也應該是A、B、C。
在單線程內使producer把消息發往同一臺服務器的同一個分區,這樣就可以按照發送的順序達到服務器並存儲,並按照相同順序被消費者消費。
可靠性
Broker存儲消息機制
寫入磁盤,不意味着數據落到磁盤設備上,畢竟中間還隔着一層os,os對寫有緩衝。通常有兩個方法來保證數據落到磁盤上:根據處理頻率(消息條數)或者時間間隔來force 數據寫入到磁盤設備。
Broker災備
類似mysql的同步和異步複製,將一臺master服務器的數據完整複製到另一臺slave服務器,並且slave服務器還提供消費功能。在kafka中,它是這樣描述的"Each server acts as a leader for some of its partitions and a follower for others so load is well balanced within the cluster. “,簡單翻譯爲,每個服務器充當它自身分區的leader並且充當其他服務器的分區的folloer,從而達到負載均衡。
理論上說同步複製能帶來更高的可靠級別,異步複製因爲延遲的存在,可能會丟失極少量的消息數據,相應地,同步複製會帶來性能的損失,因爲要同步寫入兩臺甚至更多的broker機器上纔算寫入成功。在實際實踐中,推薦採用異步複製的架構,因爲異步複製的架構相對簡單,並且易於維護和恢復,對性能也沒有影響。而同步複製對運維要求相對很高,機制複雜容易出錯,故障恢復也比較麻煩。異步複製加上磁盤做磁盤陣列,足以應對非常苛刻的數據可靠性要求。
第一次複製因爲需要跟master完全同步需要耗費一定時間,你可以在數據文件的目錄觀察複製情況。
異步複製的slave可以參與消費者的消費活動,消息消費者可以從slave中獲取消息並消費,消費者會隨機從master和slaves中挑選一臺作爲消費broker。
性能
使用sendfile調用,減少字節複製開銷和系統調用開銷
使用 message set概念,進行批量處理,可以增加一次在網絡中傳輸的內容,減少roundtrip開銷;並可以帶來順序的磁盤操作和連續的內存塊。還可以進行壓縮,壓縮比例比單次處理高。
異常處理
消息重複
消息的重複包含兩個方面,生產者重複發送消息以及消費者重複消費消息。
針對生產者來說,有可能發生這種情況,生產者發送消息,等待服務器應答,這個時候發生網絡故障,服務器實際已經將消息寫入成功,但是由於網絡故障沒有返回應答。那麼生產者會認爲發送失敗,則再次發送同一條消息,如果發送成功,則服務器實際存儲兩條相同的消息。這種由故障引起的重複,MQ是無法避免的,因爲MQ不判斷消息的data是否一致,因爲它並不理解data的語義,而僅僅是作爲載荷來傳輸。
針對消費者來說也有這個問題,消費者成功消費一條消息,但是此時斷電,沒有及時將前進後的offset存儲起來,則下次啓動的時候或者其他同個分組的消費者owner到這個分區的時候,會重複消費該條消息。這種情況MQ也無法完全避免。
生產者的負載均衡和failover
在broker因爲重啓或者故障等因素無法服務的時候,producer通過zookeeper會感知到這個變化,將失效的分區從列表中移除做到fail over。因爲從故障到感知變化有一個延遲,可能在那一瞬間會有部分的消息發送失敗。
運維管理
參數維護

  • Web管理平臺,通過瀏覽器訪問

  • 提供restful api,可以參考這裏

  • 設置jmx端口,通過API或者jconsole等工具查看信息或者修改參數

磁盤空間管理
默認情況下,meta是會保存不斷添加的消息,然後定期對“過期”的數據進行刪除或者歸檔處理。可以選擇在何時開始刪除、備份數據,刪除、備份多長時間之前的數據。
系統設計選型
爲什麼把Topic分成多個分區?
Topic分成多個分區分成多個文件,可以防止單個Topic的文件內容過大。每個分區只能被消費者羣組裏面的一個消費者消費。另外,還可以選擇把Topic的部分分區複製到follower上,從而達到負載均衡和failover的目的。
爲什麼需要消費者羣組
首先,傳統上存在兩種模型:queue和topic。queue保證只有一個消費者能夠消費到內容;topic是廣播給所有消費者,讓它們消費。
在設計時約定,一個消息可以被不同的消費者羣組消費,每個消費者羣組只能消費一次。這樣如果只有一個消費者羣組,那麼達到queue的語義;如果有多個消費者羣組,那麼達到topic的語義
爲什麼選擇以頁面緩存爲中心的設計
節選自分佈式發佈訂閱消息系統 Kafka 架構設計翻譯: 
線性寫入(linear write)的速度大約是300MB/秒,但隨即寫入卻只有50k/秒,其中的差別接近10000倍。線性讀取和寫入是所有使用模式中最具可預計性的一種方式,當代操作系統已經提供了預讀(預先讀取多個塊,加載到內存裏)和後寫(合併一組小數據量寫,然後一次寫入)的技術。
現代操作系變得越來越積極地將主內存用作磁盤緩存。所有現代的操作系統都會樂於將所有空閒內存轉做磁盤緩存,即時在需要回收這些內存的情況下會付出一些性能方面的代價。所有的磁盤讀寫操作都需要經過這個統一的緩存。想要捨棄這個特性都不太容易,除非使用直接I/O。
因此,對於一個進程而言,即使它在進程內的緩存中保存了一份數據,這份數據也可能在OS的頁面緩存(pagecache)中有重複的一份,結構就成了一份數據保存了兩次。同時,注意到,Java對象的內存開銷(overhead)非常大,往往是對象中存儲的數據所佔內存的兩倍(或更糟)。Java中的內存垃圾回收會隨着堆內數據不斷增長而變得越來越不明確,回收所花費的代價也會越來越大。
由於這些因素,使用文件系統並依賴於頁面緩存要優於自己在內存中維護一個緩存或者什麼別的結構 —— 通過對所有空閒內存自動擁有訪問權,我們至少將可用的緩存大小翻了一倍,然後通過保存壓縮後的字節結構而非單個對象,緩存可用大小接着可能又翻了一倍。這麼做下來,在GC性能不受損失的情況下,我們可在一臺擁有32G內存的機器上獲得高達28到30G的緩存。而且,這種緩存即使在服務重啓之後會仍然保持有效,而不象進程內緩存,進程重啓後還需要在內存中進行緩存重建(10G的緩存重建時間可能需要10分鐘),否則就需要以一個全空的緩存開始運行(這麼做它的初始性能會非常糟糕)。這還大大簡化了代碼,因爲對緩存和文件系統之間的一致性進行維護的所有邏輯現在都是在OS中實現的,這事OS做起來要比我們在進程中做那種一次性的緩存更加高效,準確性也更高。如果你使用磁盤的方式更傾向於線性讀取操作,那麼隨着每次磁盤讀取操作,預讀就能非常高效使用隨後準能用得着的數據填充緩存(這也就是offset的遞增順序讀取,能夠大量讀IO的性能)。
Push vs. Pull
消費者主動從Broker上面拉取消息還是Broker主動把消息推送給消費者?其實是各有利弊。
基於push機制的系統很難控制把數據下發給不同消費者的速度。有可能會導致消費者過載。這方面,pull做的比較好。消費者可以自己控制處理數據的速度。
另外,pull-based 消費者可以批量獲取數據。push-base的broker就比較難處理,是每次發送單個消息還是批量發送?如果是批量發送,每次發送多少個?
Pull不好的是,如果broker沒有數據的話,pull-based 消費者可能會忙等。這個問題可以通過"long poll"機制來解決(相當於Java的Future.get)。
消費者位置
大部分消息使用元數據來記錄哪些Broker的消息被消費了。也就是說,當消息傳遞給消費者後,Broker記錄下或者等待消費者的acknowledge後再記錄。但是這裏存在很多問題。如果當消息通過網絡傳遞給消費者,而此時如果消費者沒有來得及處理就宕機了,但是Broker卻記錄了該消息已被消費,那麼該消息將被丟失。爲了避免這種情況,很多消息消息系統會增加一個acknowledge特性,標識該消息被成功消費。然後消費者將acknowledge發送給Broker,而Broker不一定能夠獲得這個acknowledge,進而導致消息被重複消費。其次這種方法還導致網絡開銷以及服務器端必須維護消息的處理狀態。
在類Kafka系統中,主題是由多個有序的分區組成的。每個分區在任意時刻只能被一個消費者消費。這意味着,每個分區裏面的消費者位置僅僅是個整數,標識下一個被消費消息的offset。這樣維護哪些消息被消費就簡單多了,比如通過定期的設置檢查點。
消息分發語義
類Kafka在分發消息時,有3類保障:


  • 至多一次(At most once):消息可能丟失,但是不會被重發

  • 至少一次(At least once):消息不可能丟失,但是可能被重發

  • 幾乎一次(Exactly one):消息被分發一次並且僅僅一次

可以將問題分爲兩類:消息發送的持久化保障和消息消費的持久化保障
這個其實沒有完美的辦法來處理。當生產者發送消息時,可以通過在消息上面設置主鍵,然後萬一失敗時嘗試再次發送,Broker可以回覆相應的確認消息。
當消費者消費消息時,分爲3種情況:


  • 讀取消息,保存offset,處理消息。然後處理消息時崩潰。針對“至多一次”場景。

  • 讀取消息,處理消息,保存offset。然後保存offset崩潰。針對“至少一次”場景。

  • 經典的做法是在保存offset和處理消息這兩個環節採用two-phase commit(2PC)。在Kafka中,一種簡單的方法就是可以把offset和處理後的結果一起存儲。

複製
Kafka可以把每個主題的分區複製到若干個服務器上(參數可配)。很多消息系統如果要提供複製相關的特性,擔心複製會影響到吞吐量,所以一般需要繁瑣的手工配置。而在Kafka中,它默認提供了複製特性–用戶可以把複製銀子設置成1,則相當於是不復制。
每個分區有1個leader和0或者多個followers。
節點處於“alive”由以下兩個條件組成:


  • 必須和zk存在session 2.如果該節點是slave,那麼它必須保證寫複製距離leader不遠。

leader保存了所有正在進行同步的節點列表。如果follower死了,或者離leader太遠,leader將把它從節點中remove掉。“離leader太遠”這個定義可以通過延遲的消息數和延遲的時間參數來定義。
一個消息,只有當所有in-sync複製節點完成了複製後,才能標記爲“commited”。只有處於“commited”的消息才能夠被消費。另一方面,生產者可以權衡延遲和持久化這兩個因素,設置是否等待消息被commit或者等待多少個ack。
採用pull模型,消息的實時性有保證嗎?
消息的實時性受很多因素影響,不能簡單地說實時性一定會降低,主要影響因素如下


  • broker上配置的批量force消息的閾值,force消息的閾值越大,則實時性越低。

  • 消費者每次抓取的數據大小,這個值越大,則實時性越低,但是吞吐量越高。

  • Topic的分區數目對實時性也有較大影響,分區數目越多,則磁盤壓力越大,導致消息投遞的實時性降低。

  • 消費者重試抓取的時間間隔,越長則延遲越嚴重。

  • 消費者抓取數據的線程數

消息的存儲結構
在Kafka中,消息格式是如下

01/**


02 * A message. The format of an N byte message is the following:


03 *


04 * If magic byte is 0


05 *


06 * 1. 1 byte "magic" identifier to allow format changes


07 *


08 * 2. 4 byte CRC32 of the payload


09 *


10 * 3. N - 5 byte payload


11 *


12 * If magic byte is 1


13 *


14 * 1. 1 byte "magic" identifier to allow format changes


15 *


16 * 2. 1 byte "attributes" identifier to allow annotations on the message independent of the version (e.g. compression enabled, type of codec used)


17 *


18 * 3. 4 byte CRC32 of the payload


19 *


20 * 4. N - 6 byte payload


21 *


22 */




磁盤上消息格式如下:

1message length : 4 bytes (value: 1+4+n)


2"magic" value  : 1 byte


3crc            : 4 bytes


4payload        : n bytes




Metaq的消息格式如下

1message length(4 bytes),包括消息屬性和payload data


2checksum(4 bytes)


3message id(8 bytes)


4message flag(4 bytes)


5attribute length(4 bytes) + attribute,可選


6payload




其中checksum採用CRC32算法計算,計算的內容包括消息屬性長度+消息屬性+data,消息屬性如果不存在則不包括在內。消費者在接收到消息後會檢查checksum是否正確。
以下節選自Metaq文檔
同一個topic下有不同分區,每個分區下面會劃分爲多個文件,只有一個當前文件在寫,其他文件只讀。當寫滿一個文件(寫滿的意思是達到設定值)則切換文件,新建一個當前文件用來寫,老的當前文件切換爲只讀。文件的命名以起始偏移量來命名。看一個例子,假設meta-test這個topic下的0-0分區可能有以下這些文件:


  • 00000000000000000000000000000000.meta

  • 00000000000000000000000000001024.meta

  • 00000000000000000000000000002048.meta

  • ……

其中00000000000000000000000000000000.meta表示最開始的文件,起始偏移量爲0。第二個文件00000000000000000000000000001024.meta的起始偏移量爲1024,同時表示它的前一個文件的大小爲1024-0=1024。同樣,第三個文件00000000000000000000000000002048.meta的起始偏移量爲2048,表明00000000000000000000000000001024.meta的大小爲2048-1024=1024。
以起始偏移量命名並排序這些文件,那麼當消費者要抓取某個起始偏移量開始位置的數據變的相當簡單,只要根據傳上來的offset二分查找文件列表,定位到具體文件,然後將絕對offset減去文件的起始節點轉化爲相對offset,即可開始傳輸數據。例如,同樣以上面的例子爲例,假設消費者想抓取從1536開始的數據1M,則根據1536二分查找,定位到00000000000000000000000000001024.meta這個文件(1536在1024和2048之間),1536-1024=512,也就是實際傳輸的起始偏移量是在00000000000000000000000000001024.meta文件的512位置。
對zookeeper的使用
Broker Node Registry
/brokers/ids/[0…N] –> host:port (ephemeral node) 
[0…N]表示是broker id,每個broker id 必須唯一。在broker啓動時就完成註冊。 
含義是每個broker對應的host:port
Broker Topic Registry
/brokers/topics/[topic]/[0…N] –> nPartions (ephemeral node) 
含義是每個broker id 對應主題的分區數
Consumer Id Registry
消費者羣組含有多個消費者,不同消費者名稱不同。每個消費者含有一個group id屬性。
/consumers/[group_id]/ids/[consumer_id] –> {“topic1”: #streams, …, “topicN”: #streams} (ephemeral node)
含義是每個消費者羣組下面的消費者所消費的topic列表。
Consumer Offset Tracking
/consumers/[group_id]/offsets/[topic]/[broker_id-partition_id] –> offset_counter_value ((persistent node)
每個消費者羣組對某個主題的服務器id-分區id消費的offset_counter_value
Partition Owner registry
/consumers/[group_id]/owners/[topic]/[broker_id-partition_id] –> consumer_node_id (ephemeral node)
含義是某消費者羣組的某個consumer_node_id對某個主題的服務器id-分區id消費
Broker node registration
當新borker加入是,它註冊在broker節點下,value是hostname和port。它同時也註冊它含有的topic列表和topic的分區情況。新主題被創建時會自動註冊到zk上。
Consumer registration algorithm
當消費者啓動時:


  • 把自己註冊到某個消費者羣組

  • 在consumer id下,註冊監聽change事件(新消費者離開或者加入),每次變化會重新計算該羣組下的消費者負載。

  • 在broker id下,註冊監聽change事件(新borker離開或者加入),每次變化會重新計算所有消費者羣組的消費者負載。

  • 如果某個消費者使用了topic filter機制,那麼它會在broker topic下注冊change事件(新主題加入),每次變化會重新計算相關聯的topic的消費者的負載。

  • 當自己加入後,重新計算消費者羣組的消費者負載。

Consumer rebalancing algorithm
一個分區只能被一個消費者消費,這樣可以避免不必要的同步機制。具體算法如下:


  • For each topic T that Ci subscribes to

  • let PT be all partitions producing topic T

  • let CG be all consumers in the same group as Ci that consume topic T

  • sort PT (so partitions on the same broker are clustered together)

  • sort CG

  • let i be the index position of Ci in CG and let N = size(PT)/size(CG)

  • assign partitions from iN to (i+1)N - 1 to consumer Ci

  • remove current entries owned by Ci from the partition owner registry

  • add newly assigned partitions to the partition owner registry (we may need to re-try this until the original partition owner releases its ownership)

中文僞碼如下:

1set $topicList = $consumer.subscrbe    


2        for each $topic in $topicList //針對某個消費者訂閱的所有主題


3    set $partitionList = $topic.partitions //獲得主題的所有分區


4    set $comsumerList = ($topic.comsumers and $consumser.group.consumsers )//獲得消費該主題的所有消費者並且這些消費者均是與當前消費者是同一個羣組的


5    $partitionList.sort() //like broker0-p0,broker0-p1 ,broker1-p0,broker1-p1 


6    $comsumerList.sort()


7    set $consumerIndex =  $comsuserList.getIndex($consumser) //獲得當前消費者在羣組裏面的索引     


8    set $N = $partitionList.size()/$comsuserList.size()//獲得分區數除以消費者數的商


9    //好吧,後面幾句話實在沒看懂,估計要看源碼,鬱悶。//TODO




RocketMQ的簡單介紹
由於目前RocketMQ的系統性介紹文檔不是很全,且由於筆者時間有限,僅僅是粗略翻了下。發現有幾個值的一說的地方。
消息過濾
支持Broker端消息過濾,在Broker中,按照Consumer的要求做過濾,優點是減少了對於Consumer無用消息的網絡傳輸。缺點是增加了Broker的負擔,實現相對複雜。
支持Consumer端消息過濾。這種過濾方式可由應用完全自定義實現,但是缺點是很多無用的消息要傳輸到Consumer端。
零拷貝選型
Consumer消費消息過程,使用了零拷貝,零拷貝包含以下兩種方式


  • 使用mmap + write方式 優點:即使頻繁調用,使用小塊文件傳輸,效率也很高 缺點:不能很好的利用DMA方式,會比sendfile多消耗CPU,內存安全性控制複雜,需要避免JVM Crash問題。

  • 使用sendfile方式 優點:可以利用DMA方式,消耗CPU較少,大塊文件傳輸效率高,無內存安全新問題。 缺點:小塊文件效率低於mmap方式,只能是BIO方式傳輸,不能使用NIO。 
    RocketMQ選擇了第一種方式,mmap+write方式,因爲有小塊數據傳輸的需求,效果會比sendfile更好。

服務發現
Name Server是專爲RocketMQ設計的輕量級名稱服務,代碼小於1000行,具有簡單、可集羣橫向擴展、無狀態等特點。將要支持的主備自動切換功能會強依賴Name Server。
後記
如果不閱讀源碼,總感覺少了些什麼的。
對英文的翻譯還是比較生硬
核心是對獨到的模型設計,對zookeeper的運用非常巧妙,以及對衆多細節的考慮。的確是個非常優秀的MQ。
下一個坑,完成對zk源碼的閱讀。
參考


  • Kafka 0.8 Documentation

  • Metamorphosis WIKI

  • ROCKETMQ WIKI

  • 分佈式發佈訂閱消息系統 Kafka 架構設計翻譯

  http://my.oschina.net/geecoodeer/blog/194829


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章