訊息佇列二三事

tianxiaoxu發表於2018-08-30

最近在看kafka的程式碼,就免不了想看看訊息佇列的一些要點:服務質量(QOS)、效能、擴充套件性等等,下面一一探索這些概念,並談談在特定的訊息佇列如kafka或者mosquito中是如何具體實現這些概念的。

服務質量

服務語義

服務質量一般可以分為三個級別,下面說明它們不同語義。

At most once

至多一次,訊息可能丟失,但絕不會重複傳輸。生產者:完全依賴底層TCP/IP的傳輸可靠性,不做特殊處理,所謂“傳送即忘”。kafka中設定acks=0。消費者:先儲存消費進度,再處理訊息。kafka中設定消費者自動提交偏移量並設定較短的提交時間間隔。

At least once

至少一次,訊息絕不會丟,但是可能會重複。生產者:要做訊息防丟失的保證。kafka中設定acks=1 或 all並設定retries>0。消費者:先處理訊息,再儲存消費進度。kafka中設定消費者自動提交偏移量並設定很長的提交時間間隔,或者直接關閉自動提交偏移量,處理訊息後手動呼叫同步模式的偏移量提交。

Exactly once

精確一次,每條訊息肯定會被傳輸一次且僅一次。這個級別光靠訊息佇列本身並不好保證,有可能要依賴外部元件。生產者:要做訊息防丟失的保證。kafka中設定acks=1 或 all並設定retries>0。mosquito中透過四步握手與DUP、MessageID等標識來實現單次語義。消費者:要做訊息防重複的保證,有多種方案,如:在儲存消費進度和處理訊息這兩個操作中引入兩階段提交協議;讓訊息冪等;讓消費處理與進度儲存處於一個事務中來保證原子性。kafka中關閉自動提交偏移量,並設定自定義的再平衡監聽器,監聽到分割槽發生變化時從外部元件讀取或者儲存偏移量,保證自己或者其他消費者在更換分割槽時能讀到最新的偏移量從而避免重複。總之就是結合ConsumerRebalanceListener、seek和一個外部系統(如支援事務的資料庫)共同來實現單次語義。此外,kafka還提供了GUID以便使用者自行實現去重。kafka 0.11版本透過3個大的改動支援EOS:1.冪等的producer;2. 支援事務;3. 支援EOS的流式處理(保證讀-處理-寫全鏈路的EOS)。這三個級別可靠性依次增加,但是延遲和頻寬佔用也會增加,所以實際情況中,要依據業務型別做出權衡。

可靠性

上面的三個語義不僅需要生產者和消費者的配合實現,還要broker本身的可靠性來進行保證。可靠性就是隻要broker向producer發出確認,就一定要保證這個訊息可以被consumer獲取。

kafka 中一個topic有多個partition,每個partition又有多個replica,所有replica中有一個leader,ISR是一定要同步leader後才能返回提交成功的replica集,OSR內的replica盡力的去同步leader,可能資料版本會落後。在kafka工作的過程中,如果某個replica同步速度慢於replica.lag.time.max.ms指定的閾值,則被踢出ISR存入OSR,如果後續速度恢復可以回到ISR中。可以配置min.insync.replicas指定ISR中的replica最小數量,預設該值為1。LEO是分割槽的最新資料的offset,當資料寫入leader後,LEO就立即執行該最新資料,相當於最新資料標識位。HW是當寫入的資料被同步到所有的ISR中的副本後,資料才認為已提交,HW更新到該位置,HW之前的資料才可以被消費者訪問,保證沒有同步完成的資料不會被消費者訪問到,相當於所有副本同步資料標識位。

每個partition的所有replica需要進行leader選舉(依賴ZooKeeper)。在leader當機後,只能從ISR列表中選取新的leader,無論ISR中哪個副本被選為新的leader,它都知道HW之前的資料,可以保證在切換了leader後,消費者可以繼續看到HW之前已經提交的資料。當ISR中所有replica都當機該partition就不可用了,可以設定unclean.leader.election.enable=true,該選項使得kafka選擇任何一個活的replica成為leader然後繼續工作,此replica可能不在ISR中,就可能導致資料丟失。所以實際使用中需要進行可用性與可靠性的權衡。

kafka建議資料可靠儲存不依賴於資料強制刷盤(會影響整體效能),而是依賴於replica。

順序消費

順序消費是指消費者處理訊息的順序與生產者投放訊息的順序一致。主要可能破壞順序的場景是生產者投放兩條訊息AB,然後A失敗重投遞導致消費者拿到的訊息是BA。

kafka中能保證分割槽內部訊息的有序性,其做法是設定max.in.flight.requests.per.connection=1,也就是說生產者在未得到broker對訊息A的確認情況下是不會傳送訊息B的,這樣就能保證broker儲存的訊息有序,自然消費者請求到的訊息也是有序的。但是我們明顯能感覺到這會降低吞吐量,因為訊息不能並行投遞了,而且會阻塞等待,也沒法發揮 batch的威力。如果想要整個topic有序,那就只能一個topic一個partition了,一個consumer group也就只有一個consumer了。這樣就違背了kafka高吞吐的初衷。

重複消費

重複消費是指一個訊息被消費者重複消費了。 這個問題也是上面第三個語義需要解決的。

一般的訊息系統如kafka或者類似的rocketmq都不能也不提倡在系統內部解決,而是配合第三方元件,讓使用者自己去解決。究其原因還是解決問題的成本與解決問題後獲得的價值不匹配,所以乾脆不解決,就像作業系統對待死鎖一樣,採取“鴕鳥政策”。但是kafka 0.11還是處理了這個問題,見發行說明,維護者是想讓使用者無可挑剔嘛 [笑cry]。

效能

衡量一個訊息系統的效能有許多方面,最常見的就是下面幾個指標。

連線數

是指系統在同一時刻能支援多少個生產者或者消費者的連線總數。連線數和broker採用的網路IO模型直接相關,常見模型有:單執行緒、連線每執行緒、Reactor、Proactor等。單執行緒一時刻只能處理一個連線,連線每執行緒受制於server的執行緒數量,Reactor是目前主流的高效能網路IO模型,Proactor由於作業系統對真非同步的支援不太行所以尚未流行。

kafka的broker採用了類似於Netty的Reactor模型:1(1個Acceptor執行緒)+N(N個Processor執行緒)+M(M個Work執行緒)。其中Acceptor負責監聽新的連線請求,同時註冊OPACCEPT事件,將新的連線按照RoundRobin的方式交給某個Processor執行緒處理。每個Processor都有一個NIO selector,向 Acceptor分配的 SocketChannel 註冊 OPREAD、OPWRITE事件,對socket進行讀寫。N由num.networker.threads決定。Worker負責具體的業務邏輯如:從requestQueue中讀取請求、資料儲存到磁碟、把響應放進responseQueue中等等。M的大小由num.io.threads決定。

Reactor模型一般基於IO多路複用(如select,epoll),是非阻塞的,所以少量的執行緒能處理大量的連線。如果大量的連線都是idle的,那麼Reactor使用epoll的效率是槓槓的,如果大量的連線都是活躍的,此時如果沒有Proactor的支援就最好把epoll換成select或者poll。具體做法是-Djava.nio.channels.spi.SelectorProvider把sun.nio.ch包下面的EPollSelectorProvider換成PollSelectorProvider。

QPS

是指系統每秒能處理的請求數量。QPS通常可以體現吞吐量(該術語很廣,可以用TPS/QPS、PV、UV、業務數/小時等單位體現)的大小。

kafka中由於可以採用 batch 的方式(還可以壓縮),所以每秒鐘可以處理的請求很多(因為減少了解析量、網路往復次數、磁碟IO次數等)。另一方面,kafka每一個topic都有多個partition,所以同一個topic下可以並行(注意不是併發喲)服務多個生產者和消費者,這也提高了吞吐量。

平均響應時間

平均響應時間是指每個請求獲得響應需要的等待時間。

kafka中處理請求的瓶頸(也就是最影響響應時間的因素)最有可能出現在哪些地方呢?網路? 有可能,但是這個因素總體而言不是kafka能控制的,kafka可以對訊息進行編碼壓縮並批次提交,減少頻寬佔用;磁碟? 很有可能,所以kafka從分利用OS的pagecache,並且對磁碟採用順序寫,這樣能大大提升磁碟的寫入速度。同時kafka還使用了零複製技術,把普通的複製過程:disk->read buffer->app buffer->socket buffer->NIC buffer 中,核心buffer到使用者buffer的複製過程省略了,加快了處理速度。此外還有檔案分段技術,每個partition都分為多個segment,避免了大檔案操作的同時提高了並行度。CPU? 不大可能,因為訊息佇列的使用並不涉及大量的計算,常見消耗有執行緒切換、編解碼、壓縮解壓、記憶體複製等,這些在大資料處理中一般不是瓶頸。

併發數

是指系統同時能處理的請求數量數。一般而言,QPS = 併發數/平均響應時間 或者說 併發數 = QPS*平均響應時間。

這個引數一般只能估計或者計算,沒法直接測。顧名思義,機器效能越好當然併發數越高咯。此外注意用上多執行緒技術並且提高程式碼的並行度、最佳化IO模型、減少減少記憶體分配和釋放等手段都是可以提高併發數的。

擴充套件性

訊息系統的可擴充套件性是指要為系統元件新增的新的成員的時候比較容易。

kafka中擴充套件性的基石就是topic採用的partition機制。第一,Kafka允許Partition在cluster中的Broker之間移動,以此來解決資料傾斜問題。第二,支援自定義的Partition演算法,比如你可以將同一個Key的所有訊息都路由到同一個Partition上去(來獲得順序)。第三,partition的所有replica透過ZooKeeper來進行叢集管理,可以動態增減副本。第四,partition也支援動態增減。

對於producer,不存在擴充套件問題,只要broker還夠你連線就行。對於consumer,一個consumer group中的consumer可以增減,但是最好不要超過一個topic的partition數量,因為多餘的consumer並不能提升處理速度,一個partition在同一時刻只能被一個consumer group中的一個consumer消費

程式碼上的可擴充套件性就屬於設計模式的領域了,這裡不談。

參考

《kafka技術內幕》

Kafka的儲存機制以及可靠性

Kafka 0.11.0.0 是如何實現Exactly-once 語義的

本文轉自:mageekchiu  作者:mageek

原文連結:

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31137683/viewspace-2213164/,如需轉載,請註明出處,否則將追究法律責任。

相關文章