在kafka中有幾個重要的組成:broker,producer,consumer(consumer group),zookeeper,topic。我們現在針對每個組件去單獨介紹。
一、模塊組成
kafka的架構如下圖所示:
上圖中包含了一個kafka集群的所有組件:
1)三臺broker集群
2)三臺zookeeper集群
3)一個生產者producer
4)一組消費者consumer group
5)一個單獨的消費者consumer3
6)一個topic1,其中partition是2,表示每個topic有兩個分區,replica是3,表示每個分區有三個副本,分布在每臺broker上。
藍色消息發送的流程:
1)producer發送藍色消息到topic1;
2)假設發送到broker1的topic1中的partition1中,此時這個partition1自動成為了當前partition三個副本replicas的leader,則broker2和broker3的兩個partition1自然的成為follower;
3)由當前的replica leader負責當前分區消息的讀和寫,另外兩個分區follower會從leader同步消息。
4)consumer group的consumer1去消費partition1的leader中的消息,則consumer2是不能消費的;同一組內只有一個consumer可以消費消息。
紅色消息發送的流程:
1)producer發送藍色消息到topic1;
2)此時發送到broker2的topic1中的partition2中,此時這個partition2自動成為了當前partition三個副本replicas的leader,則broker1和broker3的兩個partition2自然的成為follower;
3)由當前的replica leader負責當前分區消息的讀和寫,另外兩個分區follower會從leader同步消息。
4)consumer3去消費partition2的leader中的消息。
再簡單模擬了消息的分布和生產消費過程后,我們具體說明每個組成的功能:
1)producer:消息生產者,向kafka的broker發送消息的客戶端。
2)consumer: :消息消費者,向 kafka broker拉取消息的客戶端;
3)consumer group:消費者組,由多個 consumer 組成。 消費者組內每個消費者負責消費不同分區的數據,一個分區只能由一個組內消費者消費;消費者組之間互不影響。所有的消費者都屬于某個消費者組,即消費者組是邏輯上的一個訂閱者。
4)broker:一臺kafka服務器就是一個broker,一個集群在有多個broker。一個broker內可存放逗哥topic。
5 )topic :可以理解為一個隊列, 生產者和消費者面向的都是一個 topic。
6 )partition :一個非常大的topic可以分布到多個broker上,一個 topic分為多個partition,每個partition是一個有序的隊列。
7)Replica:副本,為保證集群中的某個節點發生故障時,該節點上的 partition 數據不丟失,使得kafka仍然能夠繼續工作,kafka提供了副本機制,一個topic的每個分區都有若干個副本,一個leader和若干個follower。
8 )leader:每個分區多個副本的主節點,負責數據的讀寫,即生產者發送數據的對象,以及消費者消費數據的對象都是leader。
9 )follower:每個分區多個副本中的從節點,實時從leader中同步數據,保持和leader數據
同步。leader發生故障時,某個follower會成為新的leader。
二、kafka的存儲機制
Kafka作為一個支持大數據量寫入寫出的消息隊列,由于是基于Scala和Java實現的,而Scala和Java均需要在JVM上運行,所以如果是基于內存的方式,即JVM的堆來進行數據存儲則需要開辟很大的堆來支持數據讀寫,從而會導致GC頻繁影響性能。考慮到這些因素,kafka是使用磁盤存儲數據的。
Kafka 中消息是以 topic 進行分類的,生產者生產消息,消費者消費消息,都是面向topic的。topic存儲結構見下圖:
由于生產者生產的消息會不斷追加到 log 文件末尾,為防止 log 文件過大導致數據定位效率低下,Kafka 采取了分片和索引機制,將每個partition分為多個segment。每個 segment對應兩個文件“.index”文件和“.log”文件。
partition文件夾命名規則:
topic 名稱+分區序號,舉例有一個topic名稱文“kafka”,這個topic有三個分區,則每個文件夾命名如下:
kafka-0
kafka-1
kafka-2
index和log文件的命名規則:
1)partition文件夾中的第一個segment從0開始,以后每個segement文件以上一個segment文件的最后一條消息的offset+1命名(當前日志中的第一條消息的offset值命名)。
2)數值最大為64位long大小。19位數字字符長度,沒有數字用0填充。
舉例,有以下三對文件:
0000000000000000000.log
0000000000000000000.index
0000000000000002584.log
0000000000000002584.index
0000000000000006857.log
0000000000000006857.index
以第二個文件為例看下對應的數據結構:
這里面使用的是稀疏索引,需要注意下:
消息查找過程:
找message-2589,即offset為2589:
1)先定位segment文件,在0000000000000002584中。
2)計算查找的offset在日志文件的相對偏移量
offset - 文件名的數量 = 2589 - 2584 = 5;
在index文件查找第一個參數的值,若找到,則獲取到偏移量,通過偏移量到log文件去找對應偏移量的數據即可;
本例中沒有找到,則找到當前索引中偏移量的上線最接近的值,即3,偏移量文246;然后到log文件中從偏移量為246數據開始向下尋找。
三、生產者Producer
3.1 生產者組成
通過下圖看下生產者發送消息的流程:
1)組裝ProducerRecord,執行發送方法。
2)經過序列化器Seriallizer,將key和value經過序列化成為二進制數組。發送到分區器。
3)在分區器如果制定了partition,則直接返回對應的partition;否則分配器將基于key值來返回一個分區。
4)確定分區后,將這些消息放到指定topic和partition的批量消息中。由另外的線程負責發送批量消息。kafka produce都是批量請求,會積攢一批,然后一起發送,不是調send()就進行立刻進行網絡發包。
5)broker接收到消息后,如果成功會返回一個RecordMetadata,失敗且不重試的話,則會返回一個異常。
3.2 分區策略
3.2.1分區解決的問題
1)方便在集群中擴展,每個 Partition 可以通過調整以適應它所在的機器,而一個 topic又可以有多個 Partition 組成,因此整個集群就可以適應任意大小的數據了;
2)提高了并發,可以以partition為單位進行讀寫。
3.2.2分區的使用及其原則
kafka允許我們在發送消息的時候指定分區,我們需要將發送的消息封裝成ProducerRecord:
然后調用KafkaTemplate中的send方法:
public ListenableFuture<SendResult<K, V>> send(ProducerRecord<K, V> record) {
return this.doSend(record);
}
從上面的多個構造方法中,我們看到可以傳遞不通的參數,其實不通的參數有不同的分區原則:
(1)指明partition的情況下,直接將指明的值直接作為partiton值;
(2)沒有指明partition值但有key的情況下,將key的hash值與topic的partition數進行取余得到partition值;
(3)既沒有partition值又沒有key值的情況下,第一次調用時隨機生成一個整數(后面每次調用在這個整數上自增),將這個值與topic可用的partition總數取余得到partition。
值,也就是常說的 round-robin 算法。
通過如下代碼簡單實踐下:
三種接口:partition1和partition2、partition3。
/**
* 傳partition
*
* @param topic
* @param partition
* @param key
* @param value
* @return void
* @author weirx
* @date: 2021/2/5
*/
@RequestMapping("/send/partition1")
public void sendPartition1(String topic, Integer partition, String key, String value) {
ProducerRecord producerRecord = new ProducerRecord(topic, partition, new Date().getTime(), key, value);
producer.sendPartition(producerRecord);
}
/**
* 無partition,有key
*
* @param topic
* @param key
* @param value
* @return void
* @author weirx
* @date: 2021/2/5
*/
@RequestMapping("/send/partition2")
public void sendPartition2(String topic, String key, String value) {
ProducerRecord producerRecord = new ProducerRecord(topic, key, value);
producer.sendPartition(producerRecord);
}
/**
* 沒有partition,也沒有key
*
* @param topic
* @param value
* @return void
* @author weirx
* @date: 2021/2/5
*/
@RequestMapping("/send/partition3")
public void sendPartition3(String topic, String value) {
ProducerRecord producerRecord = new ProducerRecord(topic, value);
producer.sendPartition(producerRecord);
}
按順序分別測試,參數都如以下方式給:
# 都傳
http://localhost:8085/test/kafka/send/partition1?topic=test-kafka&key=weirx&value=hello%20kafka&partition=9
# 無partition
http://localhost:8085/test/kafka/send/partition2?topic=test-kafka&key=weirx&value=hello%20kafka
# 無partition和key
http://localhost:8085/test/kafka/send/partition3?topic=test-kafka&value=hello%20kafka
分別得到結果是:
傳partition的情況下消息確實存儲在partition9:
2021-02-05 12:18:59.241 INFO 10884 --- [ntainer#0-0-C-1] c.c.b.m.kafka.consumer.KafkaConsumer : ----------------- record =ConsumerRecord(topic = test-kafka, partition = 9, leaderEpoch = 0, offset = 3, CreateTime = 1612498739238, serialized key size = 5, serialized value size = 11, headers = RecordHeaders(headers = [], isReadOnly = false), key = weirx, value = hello kafka)
2021-02-05 12:18:59.241 INFO 10884 --- [ntainer#0-0-C-1] c.c.b.m.kafka.consumer.KafkaConsumer : ------------------ message =hello kafka
不傳partition的情況,連發三次,都是在parttition3
2021-02-05 12:20:53.549 INFO 10884 --- [ntainer#0-0-C-1] c.c.b.m.kafka.consumer.KafkaConsumer : ----------------- record =ConsumerRecord(topic = test-kafka, partition = 3, leaderEpoch = 0, offset = 7, CreateTime = 1612498853547, serialized key size = 5, serialized value size = 11, headers = RecordHeaders(headers = [], isReadOnly = false), key = weirx, value = hello kafka)
2021-02-05 12:20:53.549 INFO 10884 --- [ntainer#0-0-C-1] c.c.b.m.kafka.consumer.KafkaConsumer : ------------------ message =hello kafka
2021-02-05 12:20:59.532 INFO 10884 --- [ntainer#0-0-C-1] c.c.b.m.kafka.consumer.KafkaConsumer : ----------------- record =ConsumerRecord(topic = test-kafka, partition = 3, leaderEpoch = 0, offset = 8, CreateTime = 1612498859530, serialized key size = 5, serialized value size = 11, headers = RecordHeaders(headers = [], isReadOnly = false), key = weirx, value = hello kafka)
2021-02-05 12:20:59.532 INFO 10884 --- [ntainer#0-0-C-1] c.c.b.m.kafka.consumer.KafkaConsumer : ------------------ message =hello kafka
2021-02-05 12:21:02.261 INFO 10884 --- [ntainer#0-0-C-1] c.c.b.m.kafka.consumer.KafkaConsumer : ----------------- record =ConsumerRecord(topic = test-kafka, partition = 3, leaderEpoch = 0, offset = 9, CreateTime = 1612498862258, serialized key size = 5, serialized value size = 11, headers = RecordHeaders(headers = [], isReadOnly = false), key = weirx, value = hello kafka)
2021-02-05 12:21:02.261 INFO 10884 --- [ntainer#0-0-C-1] c.c.b.m.kafka.consumer.KafkaConsumer : ------------------ message =hello kafka
都不傳的情況,連發11條,發現第一條和第十一條都是partition2,中間沒有重復,符合上面的結論。
2021-02-05 12:22:59.484 INFO 10884 --- [ntainer#0-0-C-1] c.c.b.m.kafka.consumer.KafkaConsumer : ----------------- record =ConsumerRecord(topic = test-kafka, partition = 2, leaderEpoch = 2, offset = 2, CreateTime = 1612498979481, serialized key size = -1, serialized value size = 11, headers = RecordHeaders(headers = [], isReadOnly = false), key = null, value = hello kafka)
2021-02-05 12:22:59.485 INFO 10884 --- [ntainer#0-0-C-1] c.c.b.m.kafka.consumer.KafkaConsumer : ------------------ message =hello kafka
2021-02-05 12:23:00.988 INFO 10884 --- [ntainer#0-0-C-1] c.c.b.m.kafka.consumer.KafkaConsumer : ----------------- record =ConsumerRecord(topic = test-kafka, partition = 1, leaderEpoch = 2, offset = 2, CreateTime = 1612498980985, serialized key size = -1, serialized value size = 11, headers = RecordHeaders(headers = [], isReadOnly = false), key = null, value = hello kafka)
2021-02-05 12:23:00.988 INFO 10884 --- [ntainer#0-0-C-1] c.c.b.m.kafka.consumer.KafkaConsumer : ------------------ message =hello kafka
2021-02-05 12:23:01.666 INFO 10884 --- [ntainer#0-0-C-1] c.c.b.m.kafka.consumer.KafkaConsumer : ----------------- record =ConsumerRecord(topic = test-kafka, partition = 8, leaderEpoch = 2, offset = 2, CreateTime = 1612498981663, serialized key size = -1, serialized value size = 11, headers = RecordHeaders(headers = [], isReadOnly = false), key = null, value = hello kafka)
2021-02-05 12:23:01.666 INFO 10884 --- [ntainer#0-0-C-1] c.c.b.m.kafka.consumer.KafkaConsumer : ------------------ message =hello kafka
2021-02-05 12:23:02.307 INFO 10884 --- [ntainer#0-0-C-1] c.c.b.m.kafka.consumer.KafkaConsumer : ----------------- record =ConsumerRecord(topic = test-kafka, partition = 7, leaderEpoch = 2, offset = 2, CreateTime = 1612498982304, serialized key size = -1, serialized value size = 11, headers = RecordHeaders(headers = [], isReadOnly = false), key = null, value = hello kafka)
2021-02-05 12:23:02.307 INFO 10884 --- [ntainer#0-0-C-1] c.c.b.m.kafka.consumer.KafkaConsumer : ------------------ message =hello kafka
2021-02-05 12:23:02.882 INFO 10884 --- [ntainer#0-0-C-1] c.c.b.m.kafka.consumer.KafkaConsumer : ----------------- record =ConsumerRecord(topic = test-kafka, partition = 6, leaderEpoch = 0, offset = 2, CreateTime = 1612498982880, serialized key size = -1, serialized value size = 11, headers = RecordHeaders(headers = [], isReadOnly = false), key = null, value = hello kafka)
2021-02-05 12:23:02.882 INFO 10884 --- [ntainer#0-0-C-1] c.c.b.m.kafka.consumer.KafkaConsumer : ------------------ message =hello kafka
2021-02-05 12:23:03.402 INFO 10884 --- [ntainer#0-0-C-1] c.c.b.m.kafka.consumer.KafkaConsumer : ----------------- record =ConsumerRecord(topic = test-kafka, partition = 5, leaderEpoch = 2, offset = 3, CreateTime = 1612498983400, serialized key size = -1, serialized value size = 11, headers = RecordHeaders(headers = [], isReadOnly = false), key = null, value = hello kafka)
2021-02-05 12:23:03.402 INFO 10884 --- [ntainer#0-0-C-1] c.c.b.m.kafka.consumer.KafkaConsumer : ------------------ message =hello kafka
2021-02-05 12:23:04.026 INFO 10884 --- [ntainer#0-0-C-1] c.c.b.m.kafka.consumer.KafkaConsumer : ----------------- record =ConsumerRecord(topic = test-kafka, partition = 9, leaderEpoch = 0, offset = 4, CreateTime = 1612498984024, serialized key size = -1, serialized value size = 11, headers = RecordHeaders(headers = [], isReadOnly = false), key = null, value = hello kafka)
2021-02-05 12:23:04.026 INFO 10884 --- [ntainer#0-0-C-1] c.c.b.m.kafka.consumer.KafkaConsumer : ------------------ message =hello kafka
2021-02-05 12:23:04.627 INFO 10884 --- [ntainer#0-0-C-1] c.c.b.m.kafka.consumer.KafkaConsumer : ----------------- record =ConsumerRecord(topic = test-kafka, partition = 0, leaderEpoch = 0, offset = 3, CreateTime = 1612498984625, serialized key size = -1, serialized value size = 11, headers = RecordHeaders(headers = [], isReadOnly = false), key = null, value = hello kafka)
2021-02-05 12:23:04.628 INFO 10884 --- [ntainer#0-0-C-1] c.c.b.m.kafka.consumer.KafkaConsumer : ------------------ message =hello kafka
2021-02-05 12:23:05.475 INFO 10884 --- [ntainer#0-0-C-1] c.c.b.m.kafka.consumer.KafkaConsumer : ----------------- record =ConsumerRecord(topic = test-kafka, partition = 4, leaderEpoch = 2, offset = 3, CreateTime = 1612498985473, serialized key size = -1, serialized value size = 11, headers = RecordHeaders(headers = [], isReadOnly = false), key = null, value = hello kafka)
2021-02-05 12:23:05.475 INFO 10884 --- [ntainer#0-0-C-1] c.c.b.m.kafka.consumer.KafkaConsumer : ------------------ message =hello kafka
2021-02-05 12:23:06.154 INFO 10884 --- [ntainer#0-0-C-1] c.c.b.m.kafka.consumer.KafkaConsumer : ----------------- record =ConsumerRecord(topic = test-kafka, partition = 3, leaderEpoch = 0, offset = 10, CreateTime = 1612498986152, serialized key size = -1, serialized value size = 11, headers = RecordHeaders(headers = [], isReadOnly = false), key = null, value = hello kafka)
2021-02-05 12:23:06.154 INFO 10884 --- [ntainer#0-0-C-1] c.c.b.m.kafka.consumer.KafkaConsumer : ------------------ message =hello kafka
2021-02-05 12:23:09.396 INFO 10884 --- [ntainer#0-0-C-1] c.c.b.m.kafka.consumer.KafkaConsumer : ----------------- record =ConsumerRecord(topic = test-kafka, partition = 2, leaderEpoch = 2, offset = 3, CreateTime = 1612498989394, serialized key size = -1, serialized value size = 11, headers = RecordHeaders(headers = [], isReadOnly = false), key = null, value = hello kafka)
2021-02-05 12:23:09.397 INFO 10884 --- [ntainer#0-0-C-1] c.c.b.m.kafka.consumer.KafkaConsumer : ------------------ message =hello kafka
3.3 ack
ack(acknowledgement),是kafka的一種確認機制,保證生產者發送的消息能夠可靠的的發送到broker。每個topic的partition收到消息后都需要向producer返回一個ack。如果收到表示消息發送成功,否則會再次發送。
其具體的ack機制如下所示:
1)左側表示成功發送,send并且接收到ack,繼續執行next send,也收到ack。
2)右側表示send首次失敗了,則去resend到其他的partition上,成功接收到ack。然后再去執行next send。
3.3.1 同步機制
通過上面的分析會產生一個問題:何時發送ack?
通常有兩種方式:
1)超過半數的follower同步成功
2)所有follower同步成功。
kafka采用如上方案的第二種:全部follower通過,發送ack
采取以上的ack方式,又會引發出下一個問題:當一個follower節點由于某種故障,遲遲不能與leader進行同步,此時leader需要一直等待,直到其同步完成。
由于上面的問題,kafka提供了一種機制:ISR(in-sync Replicas)
ISR(in-sync Replicas):Leader會維護一個ISR(in-sync Replicas),內部是所有和leader進行同步的follower,當ISR的所有follower完成同步后,leader會發送ack給producer。如果follower長時間未向leader同步數據,則會將該follower踢出ISR。如果leader宕機了,則會從ISR重新選舉leader。
除此之外還有OSR(Out-Sync Relipcas):不能和leader保持同步的集合。
3.3.2 ack的應答機制
kafka為用戶提供三種可靠性級別。在springboot的yml文件中通過acks的配置:
kafka:
bootstrap-servers: 192.168.184.134:9092,192.168.184.135:9092,192.168.184.136:9092
producer:
# 值的序列化方式
value-serializer: org.apache.kafka.common.serialization.StringSerializer
# acks=0 : 生產者在成功寫入消息之前不會等待任何來自服務器的響應。
# acks=1 : 只要集群的leader節點收到消息,生產者就會收到一個來自服務器成功響應。
# acks=all :只有當所有參與復制的節點全部收到消息時,生產者才會收到一個來自服務器的成功響應。
acks: all
以上機制酌情使用,都會存在問題:
0:broker故障,數據丟失。
1:leader落盤成功并返回ack,follower同步失敗或leader故障。
all:follower同步數據完成,發送ack之前leader故障,則造成了重復數據。
3.3.3 故障處理機制
首先了解兩個名詞:LEO(Log End Offset)和HW(High Watermark)。
LEO:每個副本的最后一個offset。
HW:所有副本中最小的LEO。
當leader發生故障時:
但leader發生故障,會從ISR中選出一個新的leader;為了保證副本的一致性,其余的follower會截掉各自log文件中高于HW的部分,然后重新從leader同步數據。
注意:只保證了副本將數據的一致,不能保證數據不丟失或者不重復。
當follower發生故障時:
當follower發生故障時,會被踢出ISR,待其恢復時,follower會讀取磁盤記錄的上次的HW,并將log文件中高于HW的部分截掉,,從HW開始同步,追趕leader,當該follower的LEO大于等于HW時,即已經追上了leader,就可以重新加入ISR了。
3.4Exactly Once 語義
上面說過生產者可以設定可靠性的級別:
當設置為0時,可以保證每條消息只會發送一次,即At Most Once語義。保證不重復。但不保證數據不丟失。
當設置為all時,可以保證每條消息都會發送成功,即At Least Once語義。保證不丟失,但是不保證不重復。
那么有沒有辦法保證既不丟失也不重復呢?
這里就要提到Exactly Once語義。
在0.11版本的Kafka,引入了一項重大特性:冪等性。
所謂的冪等性就是指Producer不論向Server發送多少次重復數據,Server端都只會持久化一條。
冪等性結合At Least Once語義,就構成了Kafka的Exactly Once語義。
即:At Least Once + 冪等性 = Exactly Once。
要啟用冪等性,只需要將Producer的參數中enable.idompotence設置為true即可。這個配置目前在新版本中無法通過配置文件直接配置,可以通過手動配置kafka的配置文件添加進去。
Kafka的冪等性實現其實就是將原來下游需要做的去重放在了數據上游。開啟冪等性的Producer在初始化的時候會被分配一個 PID,發往同一Partition的消息會附帶Sequence Number。而Broker 端會對<PID, Partition, SeqNumber>做緩存,當具有相同主鍵的消息提交時,Broker只會持久化一條。
作用范圍
1)PID 重啟就會變化。只能實現單回話上的冪等性,這里的會話指的是Producer進程的一次運行。
2)同時不同的Partition也具有不同主鍵,所以冪等性無法保證跨分區跨會話的 Exactly Once。
如果需要跨會話、跨多個topic-partition的情況,需要使用Kafka的事務性來實現。關于事務的原理后面會講解。
本章到此為止,下一章節講解消費者相關的內容。