kafka寫入的過程

前幾天和大佬交流,說一些大牛可以分分鐘複製一箇中間件,拿這個標準衡量自己還是差的有點遠的,在工作中經常用到的是kafka,現在有點時間再深入瞭解一下kafka的寫入過程。

幾個基本的概念:

  • broker: 消息處理結點,多個broker組成kafka集羣。
  • topic: 一類消息,如page view,click行爲等。
  • partition: topic的物理分組,每個partition都是一個有序隊列。
  • replica:partition 的副本,保障 partition 的高可用。
  • producer: 產生信息的主體,可以是服務器日誌信息等。
  • consumer: 消費producer產生話題消息的主體。
  • Consumer group:high-level consumer API 中,每個 consumer 都屬於一個 consumer group,每條消息只能被 consumer group 的一個 Consumer 消費,但可以被多個 consumer group 消費。
  • segment: 多個大小相等的段組成了一個partition。
  • offset: 一個連續的用於定位被追加到分區的每一個消息的序列號,最大值爲64位的long大小,19位數字字符長度。
  • massage: kafka中最基本的傳遞對象,有固定格式。
  • zookeeper:kafka 通過 zookeeper 來存儲集羣的 meta 信息。
  • controller:kafka 集羣中的其中一個服務器,用來進行 leader election 以及 各種 failover。

partition、segment、offset都是爲topic服務的,每個topic可以分爲多個partition,一個partition相當於一個大目錄,每個partition下面有多個大小相等的segment文件,這個segment是由message組成的,而每一個的segment不一定由大小相等的message組成。segment大小及生命週期在server.properties文件中配置。offset用於定位位於段裏的唯一消息,這個offset用於定位partition中的唯一性。

kafka的總體數據流程

(圖來自:https://www.jianshu.com/p/d3e963ff8b70):
在這裏插入圖片描述

Producers往Brokers裏面的指定Topic中寫消息,Consumers從Brokers裏面拉去指定Topic的消息,然後進行業務處理。圖中有兩個topic,topic 0有兩個partition,topic 1有一個partition,三副本備份。可以看到consumergroup1中的consumer2沒有分到partition處理,這是有可能出現的。關於broker、topics、partitions的一些元信息用zk來存,監控和路由啥的也都會用到zk。

生產

生產流程大致如下:
在這裏插入圖片描述
創建一條記錄,記錄中一個要指定對應的topic和value,key和partition可選。 先序列化,然後按照topic和partition,放進對應的發送隊列中。kafka produce都是批量請求,會積攢一批,然後一起發送,不是調send()就進行立刻進行網絡發包。發送到partition分爲兩種情況

  1. 指定了key,按照key進行哈希,相同key去一個partition。
  2. 沒有指定key,round-robin來選partition

生產者將消息發送至該 partition leader。之後生產者會根據設置的 request.required.acks 參數不同,選擇等待或或直接發送下一條消息。

  • request.required.acks = 0 表示 Producer 不等待來自 Leader 的 ACK 確認,直接發送下一條消息。在這種情況下,如果 Leader 分片所在服務器發生宕機,那麼這些已經發送的數據會丟失。

  • request.required.acks = 1 表示 Producer 等待來自 Leader 的 ACK 確認,當收到確認後才發送下一條消息。在這種情況下,消息一定會被寫入到 Leader 服務器,但並不保證 Follow 節點已經同步完成。所以如果在消息已經被寫入 Leader 分片,但是還未同步到 Follower 節點,此時Leader 分片所在服務器宕機了,那麼這條消息也就丟失了,無法被消費到。

  • request.required.acks = -1 表示 Producer 等待來自 Leader 和所有 Follower 的 ACK 確認之後,才發送下一條消息。在這種情況下,除非 Leader 節點和所有 Follower 節點都宕機了,否則不會發生消息的丟失。

文件組織

kafka的數據,實際上是以文件的形式存儲在文件系統的。topic下有partition,partition下有segment,segment是實際的一個個文件,topic和partition都是抽象概念。

在目錄/KaTeX parse error: Expected '}', got 'EOF' at end of input: {topicName}-{partitionid}/下,存儲着實際的log文件(即segment),還有對應的索引文件。

每個segment文件大小相等,文件名以這個segment中最小的offset命名,文件擴展名是.log;segment對應的索引的文件名字一樣,擴展名是.index。有兩個index文件,一個是offset index用於按offset去查message,一個是time index用於按照時間去查,其實這裏可以優化合到一起,下面只說offset index。總體的組織是這樣的:
在這裏插入圖片描述
爲了減少索引文件的大小,降低空間使用,方便直接加載進內存中,這裏的索引使用稀疏矩陣,不會每一個message都記錄下具體位置,而是每隔一定的字節數,再建立一條索引。 索引包含兩部分,分別是baseOffset,還有position。

  • baseOffset:意思是這條索引對應segment文件中的第幾條message。這樣做方便使用數值壓縮算法來節省空間。例如kafka使用的是varint。
  • position:在segment中的絕對位置。

接下來弄清楚segment具體細節之後再說offset:

offset

個人認爲發送的核心是圍繞着offset來的,offset是一個連續的用於定位被追加到分區的每一個消息的序列號。一個消費組消費partition,需要保存offset記錄消費到哪,這樣保證一個partition內部消費的順序性,以前保存在zk中,由於zk的寫性能不好,以前的解決方法都是consumer每隔一分鐘上報一次。這裏zk的性能嚴重影響了消費的速度,而且很容易出現重複消費。在0.10版本後,kafka把這個offset的保存,從zk總剝離,保存在一個名叫__consumeroffsets topic的topic中。寫進消息的key由groupid、topic、partition組成,value是偏移量offset。topic配置的清理策略是compact。總是保留最新的key,其餘刪掉。一般情況下,每個key的offset都是緩存在內存中,查詢的時候不用遍歷partition,如果沒有緩存,第一次就會遍歷partition建立緩存,然後查詢返回。

確定consumer group位移信息寫入__consumers_offsets的哪個partition,具體計算公式:

__consumers_offsets partition = Math.abs(groupId.hashCode() % groupMetadataTopicPartitionCount)   

那麼對於分區中的一個offset例如等於345552怎麼去查找相應的message呢?

先找到該message所在的segment文件,通過二分查找的方式尋找小於等於345552的offset,假如叫S的segment符合要求,如果S等於345552則S上一個segment的最後一個message即爲所求;如果S小於345552則依次遍歷當前segment即可找到。實際上offset的存儲採用了稀疏索引,這樣對於稠密索引來說節省了存儲空間,但代價是查找費點時間。

這裏稍稍總結一下:由於kafka需要保證一個partition順序消費,但是內部又分爲很多個segment,那怎麼保證順序的呢,核心就是offset是不斷遞增的,而segment又是根據offset排序,所以整體是有序的。

kafka支持3種消息投遞語義

  • At most once 消息至多會被髮送一次,但如果產生網絡延遲等原因消息就會有丟失。
  • At least once 消息至少會被髮送一次,上面既然有消息會丟失,那麼給它加一個消息確認機制即可解決,但是消息確認階段也還會出現同樣問題,這樣消息就有可能被髮送兩次。
  • Exactly once 消息只會被髮送一次,這是我們想要的效果。

那麼kafka是怎麼解決的呢?

生產冪等性:思路是這樣的,爲每個producer分配一個pid,作爲該producer的唯一標識。producer會爲每一個<topic,partition>維護一個單調遞增的seq。類似的,broker也會爲每個<pid,topic,partition>記錄下最新的seq。當req_seq == broker_seq+1時,broker纔會接受該消息。因爲:

  • 消息的seq比broker的seq大超過時,說明中間有數據還沒寫入,即亂序了。
  • 消息的seq不比broker的seq小,那麼說明該消息已被保存。

事務性/原子性廣播:引入tid(transaction id),和pid不同,這個id是應用程序提供的,用於標識事務,和producer是誰並沒關係。就是任何producer都可以使用這個tid去做事務,這樣進行到一半就死掉的事務,可以由另一個producer去恢復。同時爲了記錄事務的狀態,類似對offset的處理,引入transaction coordinator用於記錄transaction log。在集羣中會有多個transaction coordinator,每個tid對應唯一一個transaction coordinator。
注:transaction log刪除策略是compact,已完成的事務會標記成null,compact後不保留。

做事務時,先標記開啓事務,寫入數據,全部成功就在transaction log中記錄爲prepare commit狀態,否則寫入prepare abort的狀態。之後再去給每個相關的partition寫入一條marker(commit或者abort)消息,標記這個事務的message可以被讀取或已經廢棄。成功後在transaction log記錄下commit/abort狀態,至此事務結束。

參考地址:

https://blog.csdn.net/sand_clock/article/details/68486599

https://www.jianshu.com/p/d3e963ff8b70(內容大部分來源於此)

https://tech.meituan.com/2015/01/13/kafka-fs-design-theory.html

https://cwiki.apache.org/confluence/display/KAFKA/KIP-98±+Exactly+Once+Delivery+and+Transactional+Messaging#KIP-98-ExactlyOnceDeliveryandTransactionalMessaging-TransactionalGuarantees

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