1.概述
Apache Kafka最早是由LinkedIn開源出來的分散式訊息系統,現在是Apache旗下的一個子專案,並且已經成為開源領域應用最廣泛的訊息系統之一。Kafka社群非常活躍,從0.9版本開始,Kafka的標語已經從“一個高吞吐量,分散式的訊息系統”改為"一個分散式流平臺"。
Kafka和傳統的訊息系統不同在於:
-
kafka是一個分散式系統,易於向外擴充套件。
-
它同時為釋出和訂閱提供高吞吐量
-
它支援多訂閱者,當失敗時能自動平衡消費者
-
訊息的持久化
kafka和其他訊息佇列的對比:
| kafka | activemq | rabbitmq | rocketmq |
---|---|---|---|---|
背景 | Kafka 是LinkedIn 開發的一個高效能、分散式的訊息系統,廣泛用於日誌收集、流式資料處理、線上和離線訊息分發等場景 | ActiveMQActiveMQ是一種開源的,實現了JMS1.1規範的,面向訊息(MOM)的中介軟體,為應用程式提供高效的、可擴充套件的、穩定的和安全的企業級訊息通訊。 | RabbitMQ是一個由erlang開發的AMQP協議(Advanced Message Queue )的開源實現。 | RocketMQ是阿里巴巴在2012年開源的分散式訊息中介軟體,目前已經捐贈給Apache基金會,已經於2016年11月成為 Apache 孵化專案 |
開發語言 | java,scala | Java | Erlang | Java |
協議支援 | 自己制定的一套協議 | JMS協議 | AMQP | JMS、MQTT |
持久化支援 | 支援 | 支援 | 支援 | 支援 |
事務支援 | 0.11.0之後支援 | 支援 | 支援 | 支援 |
producer容錯 | 在kafka中提供了ack配置選項, request.required.acks=-1,級別最低,生產者不需要關心是否傳送成功 request.required.acks=0,只需要leader分割槽有了即可 request.required.acks=1,isr集合中的所有同步了才返回 可能會有重複資料 | 傳送失敗後即可重試 | 有ack模型 ack模型可能重複訊息 事務模型保證完全一致 | 和kafka類似 |
吞吐量 | kafka具有高的吞吐量,內部採用訊息的批量處理,zero-copy機制,資料的儲存和獲取是本地磁碟順序批量操作,具有O(1)的複雜度,訊息處理的效率很高 | rabbitMQ在吞吐量方面稍遜於kafka,他們的出發點不一樣,rabbitMQ支援對訊息的可靠的傳遞,支援事務,不支援批量的操作;基於儲存的可靠性的要求儲存可以採用記憶體或者硬碟。 | kafka在topic數量不多的情況下吞吐量比rocketMq高,在topic數量多的情況下rocketMq比kafka高 | |
負載均衡 | kafka採用zookeeper對叢集中的broker、consumer進行管理,可以註冊topic到zookeeper上;通過zookeeper的協調機制,producer儲存對應topic的broker資訊,可以隨機或者輪詢傳送到broker上;並且producer可以基於語義指定分片,訊息傳送到broker的某分片上 |
| rabbitMQ的負載均衡需要單獨的loadbalancer進行支援 | NamerServer進行負載均衡 |
2.入門例項
2.1生產者
producer
import java.util.Properties;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerRecord;
public class UserKafkaProducer extends Thread
{
private final KafkaProducer<Integer, String> producer;
private final String topic;
private final Properties props = new Properties();
public UserKafkaProducer(String topic)
{
props.put("metadata.broker.list", "localhost:9092");
props.put("bootstrap.servers", "master2:6667");
props.put("retries", 0);
props.put("batch.size", 16384);
props.put("linger.ms", 1);
props.put("buffer.memory", 33554432);
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
producer = new KafkaProducer<Integer, String>(props);
this.topic = topic;
}
@Override
public void run() {
int messageNo = 1;
while (true)
{
String messageStr = new String("Message_" + messageNo);
System.out.println("Send:" + messageStr);
//返回的是Future<RecordMetadata>,非同步傳送
producer.send(new ProducerRecord<Integer, String>(topic, messageStr));
messageNo++;
try {
sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
複製程式碼
2.2 消費者
Properties props = new Properties();
/* 定義kakfa 服務的地址,不需要將所有broker指定上 */
props.put("bootstrap.servers", "localhost:9092");
/* 制定consumer group */
props.put("group.id", "test");
/* 是否自動確認offset */
props.put("enable.auto.commit", "true");
/* 自動確認offset的時間間隔 */
props.put("auto.commit.interval.ms", "1000");
props.put("session.timeout.ms", "30000");
/* key的序列化類 */
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
/* value的序列化類 */
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
/* 定義consumer */
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
/* 消費者訂閱的topic, 可同時訂閱多個 */
consumer.subscribe(Arrays.asList("foo", "bar"));
/* 讀取資料,讀取超時時間為100ms */
while (true) {
ConsumerRecords<String, String> records = consumer.poll(100);
for (ConsumerRecord<String, String> record : records)
System.out.printf("offset = %d, key = %s, value = %s", record.offset(), record.key(), record.value());
}
複製程式碼
3.Kafka架構原理
對於kafka的架構原理我們先提出幾個問題?
1.Kafka的topic和分割槽內部是如何儲存的,有什麼特點?
2.與傳統的訊息系統相比,Kafka的消費模型有什麼優點?
3.Kafka如何實現分散式的資料儲存與資料讀取?
3.1Kafka架構圖
3.2kafka名詞解釋
在一套kafka架構中有多個Producer,多個Broker,多個Consumer,每個Producer可以對應多個Topic,每個Consumer只能對應一個ConsumerGroup。
整個Kafka架構對應一個ZK叢集,通過ZK管理叢集配置,選舉Leader,以及在consumer group發生變化時進行rebalance。
名稱 | 解釋 |
---|---|
Broker | 訊息中介軟體處理節點,一個Kafka節點就是一個broker,一個或者多個Broker可以組成一個Kafka叢集 |
Topic | 主題,Kafka根據topic對訊息進行歸類,釋出到Kafka叢集的每條訊息都需要指定一個topic |
Producer | 訊息生產者,向Broker傳送訊息的客戶端 |
Consumer | 訊息消費者,從Broker讀取訊息的客戶端 |
ConsumerGroup | 每個Consumer屬於一個特定的Consumer Group,一條訊息可以傳送到多個不同的Consumer Group,但是一個Consumer Group中只能有一個Consumer能夠消費該訊息 |
Partition | 物理上的概念,一個topic可以分為多個partition,每個partition內部是有序的 |
3.3Topic和Partition
在Kafka中的每一條訊息都有一個topic。一般來說在我們應用中產生不同型別的資料,都可以設定不同的主題。一個主題一般會有多個訊息的訂閱者,當生產者釋出訊息到某個主題時,訂閱了這個主題的消費者都可以接收到生產者寫入的新訊息。
kafka為每個主題維護了分散式的分割槽(partition)日誌檔案,每個partition在kafka儲存層面是append log。任何釋出到此partition的訊息都會被追加到log檔案的尾部,在分割槽中的每條訊息都會按照時間順序分配到一個單調遞增的順序編號,也就是我們的offset,offset是一個long型的數字,我們通過這個offset可以確定一條在該partition下的唯一訊息。在partition下面是保證了有序性,但是在topic下面沒有保證有序性。
在上圖中在我們的生產者會決定傳送到哪個Partition。
1.如果沒有Key值則進行輪詢傳送。
2.如果有Key值,對Key值進行Hash,然後對分割槽數量取餘,保證了同一個Key值的會被路由到同一個分割槽,如果想佇列的強順序一致性,可以讓所有的訊息都設定為同一個Key。
3.4消費模型
訊息由生產者傳送到kafka叢集后,會被消費者消費。一般來說我們的消費模型有兩種:推送模型(psuh)和拉取模型(pull)
基於推送模型的訊息系統,由訊息代理記錄消費狀態。訊息代理將訊息推送到消費者後,標記這條訊息為已經被消費,但是這種方式無法很好地保證消費的處理語義。比如當我們把已經把訊息傳送給消費者之後,由於消費程式掛掉或者由於網路原因沒有收到這條訊息,如果我們在消費代理將其標記為已消費,這個訊息就永久丟失了。如果我們利用生產者收到訊息後回覆這種方法,訊息代理需要記錄消費狀態,這種不可取。如果採用push,訊息消費的速率就完全由消費代理控制,一旦消費者發生阻塞,就會出現問題。
Kafka採取拉取模型(poll),由自己控制消費速度,以及消費的進度,消費者可以按照任意的偏移量進行消費。比如消費者可以消費已經消費過的訊息進行重新處理,或者消費最近的訊息等等。
3.5網路模型
3.5.1 KafkaClient --單執行緒Selector
單執行緒模式適用於併發連結數小,邏輯簡單,資料量小。
在kafka中,consumer和producer都是使用的上面的單執行緒模式。這種模式不適合kafka的服務端,在服務端中請求處理過程比較複雜,會造成執行緒阻塞,一旦出現後續請求就會無法處理,會造成大量請求超時,引起雪崩。而在伺服器中應該充分利用多執行緒來處理執行邏輯。
3.5.2 Kafka--server -- 多執行緒Selector
在kafka服務端採用的是多執行緒的Selector模型,Acceptor執行在一個單獨的執行緒中,對於讀取操作的執行緒池中的執行緒都會在selector註冊read事件,負責服務端讀取請求的邏輯。成功讀取後,將請求放入message queue共享佇列中。然後在寫執行緒池中,取出這個請求,對其進行邏輯處理,即使某個請求執行緒阻塞了,還有後續的縣城從訊息佇列中獲取請求並進行處理,在寫執行緒中處理完邏輯處理,由於註冊了OP_WIRTE事件,所以還需要對其傳送響應。
3.6高可靠分散式儲存模型
在Kafka中保證高可靠模型的依靠的是副本機制,有了副本機制之後,就算機器當機也不會發生資料丟失。
3.6.1高效能的日誌儲存
kafka一個topic下面的所有訊息都是以partition的方式分散式的儲存在多個節點上。同時在kafka的機器上,每個Partition其實都會對應一個日誌目錄,在目錄下面會對應多個日誌分段(LogSegment)。LogSegment檔案由兩部分組成,分別為“.index”檔案和“.log”檔案,分別表示為segment索引檔案和資料檔案。這兩個檔案的命令規則為:partition全域性的第一個segment從0開始,後續每個segment檔名為上一個segment檔案最後一條訊息的offset值,數值大小為64位,20位數字字元長度,沒有數字用0填充,如下,假設有1000條訊息,每個LogSegment大小為100,下面展現了900-1000的索引和Log:
由於kafka訊息資料太大,如果全部建立索引,即佔了空間又增加了耗時,所以kafka選擇了稀疏索引的方式,這樣的話索引可以直接進入記憶體,加快偏查詢速度。
簡單介紹一下如何讀取資料,如果我們要讀取第911條資料首先第一步,找到他是屬於哪一段的,根據二分法查詢到他屬於的檔案,找到0000900.index和00000900.log之後,然後去index中去查詢 (911-900) =11這個索引或者小於11最近的索引,在這裡通過二分法我們找到了索引是[10,1367]然後我們通過這條索引的物理位置1367,開始往後找,直到找到911條資料。
上面講的是如果要找某個offset的流程,但是我們大多數時候並不需要查詢某個offset,只需要按照順序讀即可,而在順序讀中,作業系統會對記憶體和磁碟之間新增page cahe,也就是我們平常見到的預讀操作,所以我們的順序讀操作時速度很快。但是kafka有個問題,如果分割槽過多,那麼日誌分段也會很多,寫的時候由於是批量寫,其實就會變成隨機寫了,隨機I/O這個時候對效能影響很大。所以一般來說Kafka不能有太多的partition。針對這一點,RocketMQ把所有的日誌都寫在一個檔案裡面,就能變成順序寫,通過一定優化,讀也能接近於順序讀。
可以思考一下:1.為什麼需要分割槽,也就是說主題只有一個分割槽,難道不行嗎?2.日誌為什麼需要分段
1.分割槽是為了水平擴充套件 2.日誌如果在同一個檔案太大會影響效能。如果日誌無限增長,查詢速度會減慢
3.6.2副本機制
Kafka的副本機制是多個服務端節點對其他節點的主題分割槽的日誌進行復制。當叢集中的某個節點出現故障,訪問故障節點的請求會被轉移到其他正常節點(這一過程通常叫Reblance),kafka每個主題的每個分割槽都有一個主副本以及0個或者多個副本,副本保持和主副本的資料同步,當主副本出故障時就會被替代。
在Kafka中並不是所有的副本都能被拿來替代主副本,所以在kafka的leader節點中維護著一個ISR(In sync Replicas)集合,翻譯過來也叫正在同步中集合,在這個集合中的需要滿足兩個條件:
-
節點必須和ZK保持連線
-
在同步的過程中這個副本不能落後主副本太多
另外還有個AR(Assigned Replicas)用來標識副本的全集,OSR用來表示由於落後被剔除的副本集合,所以公式如下:ISR = leader + 沒有落後太多的副本; AR = OSR+ ISR;
這裡先要說下兩個名詞:HW(高水位)是consumer能夠看到的此partition的位置,LEO是每個partition的log最後一條Message的位置。HW能保證leader所在的broker失效,該訊息仍然可以從新選舉的leader中獲取,不會造成訊息丟失。
當producer向leader傳送資料時,可以通過request.required.acks引數來設定資料可靠性的級別:
-
1(預設):這意味著producer在ISR中的leader已成功收到的資料並得到確認後傳送下一條message。如果leader當機了,則會丟失資料。
-
0:這意味著producer無需等待來自broker的確認而繼續傳送下一批訊息。這種情況下資料傳輸效率最高,但是資料可靠性確是最低的。
-
-1:producer需要等待ISR中的所有follower都確認接收到資料後才算一次傳送完成,可靠性最高。但是這樣也不能保證資料不丟失,比如當ISR中只有leader時(其他節點都和zk斷開連線,或者都沒追上),這樣就變成了acks=1的情況。
4.高可用模型及冪等
在分散式系統中一般有三種處理語義:
-
at-least-once:
至少一次,有可能會有多次。如果producer收到來自ack的確認,則表示該訊息已經寫入到Kafka了,此時剛好是一次,也就是我們後面的exactly-once。但是如果producer超時或收到錯誤,並且request.required.acks配置的不是-1,則會重試傳送訊息,客戶端會認為該訊息未寫入Kafka。如果broker在傳送Ack之前失敗,但在訊息成功寫入Kafka之後,這一次重試將會導致我們的訊息會被寫入兩次,所以訊息就不止一次地傳遞給最終consumer,如果consumer處理邏輯沒有保證冪等的話就會得到不正確的結果。
在這種語義中會出現亂序,也就是當第一次ack失敗準備重試的時候,但是第二訊息已經傳送過去了,這個時候會出現單分割槽中亂序的現象,我們需要設定Prouducer的引數max.in.flight.requests.per.connection,flight.requests是Producer端用來儲存傳送請求且沒有響應的佇列,保證Producer端未響應的請求個數為1。 -
at-most-once:
如果在ack超時或返回錯誤時producer不重試,也就是我們講request.required.acks=-1,則該訊息可能最終沒有寫入kafka,所以consumer不會接收訊息。
-
exactly-once:
剛好一次,即使producer重試傳送訊息,訊息也會保證最多一次地傳遞給consumer。該語義是最理想的,也是最難實現的。在0.10之前並不能保證exactly-once,需要使用consumer自帶的冪等性保證。0.11.0使用事務保證了
4.1 如何實現exactly-once
要實現exactly-once在Kafka 0.11.0中有兩個官方策略:
4.1.1單Producer單Topic
每個producer在初始化的時候都會被分配一個唯一的PID,對於每個唯一的PID,Producer向指定的Topic中某個特定的Partition傳送的訊息都會攜帶一個從0單調遞增的sequence number。
在我們的Broker端也會維護一個維度為<PID,Topic,Partition>,每次提交一次訊息的時候都會對齊進行校驗:
-
如果訊息序號比Broker維護的序號大一以上,說明中間有資料尚未寫入,也即亂序,此時Broker拒絕該訊息,Producer丟擲InvalidSequenceNumber
-
如果訊息序號小於等於Broker維護的序號,說明該訊息已被儲存,即為重複訊息,Broker直接丟棄該訊息,Producer丟擲DuplicateSequenceNumber
-
如果訊息序號剛好大一,就證明是合法的
上面所說的解決了兩個問題:
1.當Prouducer傳送了一條訊息之後失敗,broker並沒有儲存,但是第二條訊息卻傳送成功,造成了資料的亂序。
2.當Producer傳送了一條訊息之後,broker儲存成功,ack回傳失敗,producer再次投遞重複的訊息。
上面所說的都是在同一個PID下面,意味著必須保證在單個Producer中的同一個seesion內,如果Producer掛了,被分配了新的PID,這樣就無法保證了,所以Kafka中又有事務機制去保證。
4.1.2事務
在kafka中事務的作用是
-
實現exactly-once語義
-
保證操作的原子性,要麼全部成功,要麼全部失敗。
-
有狀態的操作的恢復
事務可以保證就算跨多個<Topic, Partition>,在本次事務中的對消費佇列的操作都當成原子性,要麼全部成功,要麼全部失敗。並且,有狀態的應用也可以保證重啟後從斷點處繼續處理,也即事務恢復。在kafka的事務中,應用程式必須提供一個唯一的事務ID,即Transaction ID,並且當機重啟之後,也不會發生改變,Transactin ID與PID可能一一對應。區別在於Transaction ID由使用者提供,而PID是內部的實現對使用者透明。為了Producer重啟之後,舊的Producer具有相同的Transaction ID失效,每次Producer通過Transaction ID拿到PID的同時,還會獲取一個單調遞增的epoch。由於舊的Producer的epoch比新Producer的epoch小,Kafka可以很容易識別出該Producer是老的Producer並拒絕其請求。為了實現這一點,Kafka 0.11.0.0引入了一個伺服器端的模組,名為Transaction Coordinator,用於管理Producer傳送的訊息的事務性。該Transaction Coordinator維護Transaction Log,該log存於一個內部的Topic內。由於Topic資料具有永續性,因此事務的狀態也具有永續性。Producer並不直接讀寫Transaction Log,它與Transaction Coordinator通訊,然後由Transaction Coordinator將該事務的狀態插入相應的Transaction Log。Transaction Log的設計與Offset Log用於儲存Consumer的Offset類似。
最後
關於訊息佇列或者Kafka的一些常見的面試題,通過上面的文章可以提煉出以下幾個比較經典的問題:
- 為什麼使用訊息佇列?訊息佇列的作用是什麼?
- Kafka的topic和分割槽內部是如何儲存的,有什麼特點?
- 與傳統的訊息系統相比,Kafka的消費模型有什麼優點?
- Kafka如何實現分散式的資料儲存與資料讀取?
- kafka為什麼比rocketmq支援的單機partion要少?
- 為什麼需要分割槽,也就是說主題只有一個分割槽,難道不行嗎?
- 日誌為什麼需要分段?
- kafka是依靠什麼機制保持高可靠,高可用?
- 訊息佇列如何保證訊息冪等?
- 讓你自己設計個訊息佇列,你會怎麼設計,會考慮哪些方面?
大部分問題都可以從上面總結後找到答案,如果還不會的話就關注我的公眾號,讓我為你解答吧。
最後這篇文章被我收錄於JGrowing,一個全面,優秀,由社群一起共建的Java學習路線,如果您想參與開源專案的維護,可以一起共建,github地址為:https://github.com/javagrowing/JGrowing 麻煩給個小星星喲。
打個廣告,如果你覺得這篇文章對你有文章,可以關注我的技術公眾號,你的關注和轉發是對我最大的支援,O(∩_∩)O