Kafka源碼深度解析-系列1 -消息隊列的策略與語義

-Kafka關鍵概念介紹
-消息隊列的各種策略與語義

作為一個消息隊列,Kafka在業界已經相當有名。相對傳統的RabbitMq/ActiveMq,Kafka天生就是分布式的,支持數據的分片、復制以及集群的方便擴展。

與此同時,Kafka是高可靠的、持久化的消息隊列,并且這種可靠性沒有以犧牲性能為前提。

同時,在允許丟消息的業務場景下,Kafka可以以非ACK、異步的方式來運行,從而最大程度的提高性能。

從本篇開始,本序列將會由淺入深、從使用方式到原理再到源碼,全面的剖析Kafka這個消息中間件的方方面面。(所用Kafka源碼為0.9.0)

關鍵概念介紹

topic

以下是kafka的邏輯結構圖: 每個topic也就是自定義的一個隊列,producer往隊列中放消息,consumer從隊列中取消息,topic之間相互獨立。

image

broker

與上圖對應的是kafka的物理結構圖:每個broker通常就是一臺物理機器,在上面運行kafka server的一個實例,所有這些broker實例組成kafka的服務器集群。

每個broker會給自己分配一個唯一的broker id。broker集群是通過zookeeper集群來管理的。每個broker都會注冊到zookeeper上,有某個機器掛了,有新的機器加入,zookeeper都會收到通知。

在0.9.0中,producer/consumer已經不會依賴Zookeeper來獲取集群的配置信息,而是通過任意一個broker來獲取整個集群的配置信息。如下圖所示:只有服務端依賴zk,客戶端不依賴zk。

image

partition

kafka的topic,在每個機器上,是用文件存儲的。而這些文件呢,會分目錄。partition就是文件的目錄。比如一個topic叫abc,分了10個partion,則在機器的目錄上,就是:
abc_0
abc_1
abc_2
abc_3

abc_9

然后每個目錄里面,存放了一堆消息文件,消息是順序append log方式存儲的。關于這個,后面會詳細闡述。

replica/leader/follower

每個topic的partion的所有消息,都不是只存1份,而是在多個broker上冗余存儲,從而提高系統的可靠性。這多臺機器就叫一個replica集合。

在這個replica集合中,需要選出1個leader,剩下的是follower。也就是master/slave。

發送消息的時候,只會發送給leader,然后leader再把消息同步給followers(以pull的方式,followers去leader上pull,而不是leader push給followers)。

那這里面就有一個問題:leader收到消息之后,是直接返回給producer呢,還是等所有followers都寫完消息之后,再返回? 關于這個,后面會相信闡述。

關鍵點:這里replica/leader/follower都是邏輯概念,并且是相對”partion”來講的,而不是”topic”。也就說,同一個topic的不同partion,對于的replica集合可以是不一樣的。

比如
“abc-0” <1,3,5> //abc_0的replica集合是borker 1, 3, 5, leader是1, follower是3, 5
“abc-1” <1,3,7> //abc_1的replica集合是broker 1, 3, 7,leader是1, follower是3, 7
“abc_2” <3,7,9>
“abc_3” <1,7,9>
“abc_4” <1,3,5>

消息隊列的各種策略和語義

對于消息隊列的使用,表面上看起來很簡單,一端往里面放,一端從里面取。但就在這一放一取中,存在著諸多策略。

Producer的策略

是否ACK

所謂ACK,是指服務器收到消息之后,是存下來之后,再給客戶端返回,還是直接返回。很顯然,是否ACK,是影響性能的一個重要指標。在kafka中,request.required.acks有3個取值,分別對應3種策略:

request.required.acks

//0: 不等服務器ack就返回了,性能最高,可能丟數據
//1. leader確認消息存下來了,再返回
//all: leader和當前ISR中所有replica都確認消息存下來了,再返回(這種方式最可靠)

備注:在0.9.0以前的版本,是用-1表示all

同步發送 vs 異步發送

所謂異步發送,就是指客戶端有個本地緩沖區,消息先存放到本地緩沖區,然后有后臺線程來發送。

在0.8.2和0.8.2之前的版本中,同步發送和異步發送是分開實現的,用的Scala語言。從0.8.2開始,引入了1套新的Java版的client api。在這套api中,同步實際上是用異步間接實現的:

在異步發送下,有以下4個參數需要配置:

(1)隊列的最大長度
buffer.memory //缺省為33554432, 即32M

(2)隊列滿了,客戶端是阻塞,還是拋異常出來(缺省是true)
block.on.buffer.full
//true: 阻塞消息
//false:拋異常

(3)發送的時候,可以批量發送的數據量
batch.size //缺省16384字節,即16K

(4)最長等多長時間,批量發送
linger.ms //缺省是0
//類似TCP/IP協議中的linger algorithm,> 0 表示發送的請求,會在隊列中積攥,然后批量發送。

很顯然,異步發送可以提高發送的性能,但一旦客戶端掛了,就可能丟數據。

對于RabbitMQ, ActiveMQ,他們都強調可靠性,因此不允許非ACK的發送,也沒有異步發送模式。Kafka提供了這個靈活性,允許使用者在性能與可靠性之間做權衡。

(5)消息的最大長度
max.request.size //缺省是1048576,即1M

這個參數會影響batch的大小,如果單個消息的大小 > batch的最大值(16k),那么batch會相應的增大

Consumer的策略

Push vs Pull

所有的消息隊列都要面對一個問題,是broker把消息Push給消費者呢,還是消費者主動去broker Pull消息?

kafka選擇了pull的方式,為什么呢? 因為pull的方式更靈活:消息發送頻率應該如何,消息是否可以延遲然后batch發送,這些信息只有消費者自己最清楚!

因此把控制權交給消費者,消費者自己控制消費的速率,當消費者處理消息很慢時,它可以選擇減緩消費速率;當處理消息很快時,它可以選擇加快消費速率。而在push的方式下,要實現這種靈活的控制策略,就需要額外的協議,讓消費者告訴broker,要減緩還是加快消費速率,這增加了實現的復雜性。

另外pull的方式下,消費者可以很容易的自適應控制消息是batch的發送,還是最低限度的減少延遲,每來1個就發送1個。

消費的confirm

在消費端,所有消息隊列都要解決的一個問題就是“消費確認問題”:消費者拿到一個消息,然后處理這個消息的時候掛了,如果這個時候broker認為這個消息已經消費了,那這條消息就丟失了。

一個解決辦法就是,消費者在消費完之后,再往broker發個confirm消息。broker收到confirm消息之后,再把消息刪除。

要實現這個,broker就要維護每個消息的狀態,已發送/已消費,很顯然,這會增大broker的實現難度。同時,這還有另外一個問題,就是消費者消費完消息,發送confirm的時候,掛了。這個時候會出現重復消費的問題。

kafka沒有直接解決這個問題,而是引入offset回退機制,變相解決了這個問題。在kafka里面,消息會存放一個星期,才會被刪除。并且在一個partion里面,消息是按序號遞增的順序存放的,因此消費者可以回退到某一個歷史的offset,進行重新消費。

當然,對于重復消費的問題,需要消費者去解決。

broker的策略

消息的順序問題

在某些業務場景下,需要消息的順序不能亂:發送順序和消費順序要嚴格一致。而在kafka中,同一個topic,被分成了多個partition,這多個partition之間是互相獨立的。

之所以要分成多個partition,是為了提高并發度,多個partition并行的進行發送/消費,但這卻沒有辦法保證消息的順序問題。

一個解決辦法是,一個topic只用一個partition,但這樣很顯然限制了靈活性。

還有一個辦法就是,所有發送的消息,用同一個key,這樣同樣的key會落在一個partition里面。

消息的刷盤機制

我們都知道,操作系統本身是有page cache的。即使我們用無緩沖的io,消息也不會立即落到磁盤上,而是在操作系統的page cache里面。操作系統會控制page cache里面的內容,什么時候寫回到磁盤。在應用層,對應的就是fsync函數。

我們可以指定每條消息都調用一次fsync存盤,但這會較低性能,也增大了磁盤IO。也可以讓操作系統去控制存盤。

消息的不重不漏 – Exactly Once

一個完美的消息隊列,應該做到消息的“不重不漏”,這里面包含了4重語義:
消息不會重復存儲;
消息不會重復消費;
消息不會丟失存儲;
消息不會丟失消費。

先說第1個:重復存儲。發送者發送一個消息之后,服務器返回超時了。那請問,這條消息是存儲成功了,還是沒有呢?
要解決這個問題:發送者需要給每條消息增加一個primary key,同時服務器要記錄所有發送過的消息,用于判重。很顯然,要實現這個,代價很大

重復消費:上面說過了,要避免這個,消費者需要消息confirm。但同樣,會引入其他一些問題,比如消費完了,發送confirm的時候,掛了怎么辦? 一個消息一直處于已發送,但沒有confirm狀態怎么辦?

丟失存儲:這個已經解決

丟失消費:同丟失存儲一樣,需要confirm。

總結一下:真正做到不重不漏,exactly once,是很難的。這個需要broker、producer、consumer和業務方的協調配合。

在kafka里面,是保證消息不漏,也就是at least once。至于重復消費問題,需要業務自己去保證,比如業務加判重表等。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容