Kafka提升--內部工作原理

       如果只是爲了開發 Kafka 應用程序,或者只是在生產環境使用 Kafka,那麼瞭解 Kafka 的 內部工作原理不是必需的。不過,瞭解 Kafka 的內部工作原理有助於理解 Kafka 的行爲, 也有助於診斷問題。下面不會涵蓋 Kafka 的每一個設計和實現細節,而是集中討論以下 3 個有意思的話題:
• Kafka 如何進行復制;

• Kafka 如何處理來自生產者和消費者的請求;

• Kafka 的存儲細節,比如文件格式和索引。

       在對 Kafka 進行調優時,深入理解這些問題是很有必要的。瞭解了內部機制,可以更有目 的性地進行深入的調優,而不只是停留在表面,隔靴搔癢。

1、集羣成員關係

       Kafka使用Zookeeper來維護集羣成員的信息。每個broker都有一個唯一標識符,這個標識符可以在配置文件裏指定,也可以自動生成。在broker啓動的時候,它通過創建臨時節點把自己的ID註冊到Zookeeper。Kafka組件訂閱Zookeeper的/brokers/ids路徑(broker在Zookeeper上的註冊路徑),當有broker加入集羣或退出集羣時,這些組件就可以獲得通知。

       如果你要啓動另一個具有相同ID的broker,會得到一個錯誤——新broker會試着進行註冊,但不會成功,因爲Zookeeper裏已經有一個具有相同ID的broker。

       在broker停機、出現網絡分區或長時間垃圾回收停頓時,broker會從Zookeeper上斷開連接,此時broker在啓動時創建的臨時節點會自動從Zookeeper上移除。監聽broker列表的Kafka組件會被告知該broker已移除。

       在關閉broker時,它對應的節點也會消失,不過它的 ID 會繼續存在於其他數據結構中。 例如,主題的副本列表(下面會介紹)裏就可能包含這些白。在完全關閉一個 broker 之 後,如果使用相同的 m 啓動另一個全新的 broker,它會立即加入集羣,井擁有與舊 broker 相同的分區和主題。

2、控制器

       控制器其實就是一個 broker,只不過它除了具有一般 broker 的功能之外,還負責分區 首領的選舉。集羣裏第一個啓動的 broker 通過在 Zookeeper 裏創建一個臨時節點/controller讓自己成爲控制器。其他 broker 在啓動時也 會嘗試創建這個節點,不過它們會收到一個“節點已存在”的異常,然後“意識”到控制器節點已存在,也就是說集羣裏已經有一個控制器了。其他 broker 在控制器節點上創建 Zookeeper watch 對象,這樣它們就可以收到這個節點的變更通知。這種方式可以確保集羣 裏一次只有一個控制器存在。 

       如果控制器被關閉或者與 Zookeeper 斷開連接, Zookeeper 上的臨時節點就會消失。集羣裏的其他 broker 通過 watch 對象得到控制器節點消失的通知,它們會嘗試讓自己成爲新的控制器。第一個在 Zookeeper 裏成功創建控制器節點的 broker 就會成爲新的控制器,其他 節點會收到“節點已存在”的異常,然後在新的控制器節點上再次創建 watch 對象。每個新選出的控制器通過 Zookeeper 的條件遞增操作獲得一個全新的、數值更大的 controller epoch。其他 broker 在知道當前 controller  epoch 後,如果收到由控制器發出的包含較舊 epoch 的消息,就會忽略它們。

       當控制器發現一個 broker 已經離開集羣(通過觀察相關的 Zookeeper 路徑),它就知道,那些失去首領的分區需要一個新首領(這些分區的首領剛好是在這個 broker 上)。控制器遍 歷這些分區,並確定誰應該成爲新首領(簡單來說就是分區副本列表裏的下一個副本), 然後向所有包含新首領或現有跟隨者的 broker 發送請求。該請求消息包含了誰是新首領以 及誰是分區跟隨者的信息。隨後,新首領開始處理來自生產者和消費者的請求,而跟隨者開始從新首領那裏複製消息。

       簡而言之, Kafka 使用 Zookeeper 的臨時節點來選舉控制器, 並在節點加入集羣或退出集 羣時通知控制器。控制器負責在節點加入或離開集羣時進行分區首領選舉。控制器使用 epoch 來避免“腦裂”。“腦裂”是指兩個節點同時認爲自己是當前的控制器。

3、複製

       複製功能是 Kafka 架構的核心。在 Kafka 的文檔裏, Kafka 把自己描述成“一個分佈式的、 可分區的、可複製的提交日誌服務”。複製之所以這麼關鍵,是因爲它可以在個別節點失 效時仍能保證 Kafka 的可用性和持久性。
       Kafka 使用主題來組織數據,每個主題被分爲若干個分區,每個分區有多個副本。那些副本被保存在 broker 上,每個 broker 可以保存成百上千個屬於不同主題和分區的副本。 

副本有以下兩種類型。

首領副本

       每個分區都有一個首領副本。 爲了保證一致性,所有生產者請求和消費者請求都會經過這個副本。
跟隨者副本

       首領以外的副本都是跟隨者副本。跟隨者副本不處理來自客戶端的請求,它們唯一的任務就是從首領那裏複製消息,保持與首領一致的狀態。如果首領發生崩漬,其中的一個跟隨者會被提升爲新首領。 

       首領的另一個任務是搞清楚哪個跟隨者的狀態與自己是一致的。跟隨者爲了保持與首領的 狀態一致、在有新消息到達時嘗試從首領那裏複製消息,不過有各種原因會導致同步失敗。 例如,網絡擁塞導致複製變慢, broker 發生崩橫導致複製滯後,直到重啓 broker 後複製纔會繼續。 爲了與首領保持同步,跟隨者向首領發送獲取數據的請求,這種請求與消費者爲了讀取消息而發送的請求是一樣的。首領將響應消息發給跟隨者。請求消息裏包含了跟隨者想要獲取消息的偏移量,而且這些偏移量總是有序的。 

       一個跟隨者副本先請求消息 1 ,接着請求消息 2,然後請求消息 3,在收到這 3 個請求的響應之前,它是不會發送第 4 個請求消息的。如果跟隨者發送了請求消息 4,那麼首領就知道它已經收到了前面 3 個請求的響應。 通過查看每個跟隨者請求的最新偏移量,首領就會知道每個跟隨者複製的進度。如果跟隨者在 10s 內沒有請求任何消息,或者雖然在請求消息,但在 10s 內沒有請求最新的數據,那麼它就會被認爲是不同步的。如果一個副本無陸 與首領保持一致,在首領發生失效時,它就不可能成爲新首領一一畢竟它沒有包含全部的消息。 相反,持續請求得到的最新消息 副本被稱爲同步的副本在首領發生失效時,只有同步副 本纔有可能被選爲新首領。

       跟隨者的正常不活躍時間或在成爲不同步副本之前的時間是通過 replica.lag.time.max.ms 參數來配置的。這個時間間隔直接影響着首領選舉期間的客戶端行爲和數據保留機制。
       除了當前首領之外,每個分區都有一個首選首領—— 創建主題時選定的首領就是分區的首選首領。之所以把它叫作首選首領,是因爲在創建分區時,需要在 broker 之間均衡首領 (後面會介紹在 broker 間分佈副本和首領的算法)。因此,我們希望首選首領在成爲真正的首領時, broker 間的負載最終會得到均衡。默認情況下, Kafka 的 auto.leader.rebalance.enable 被設爲 true,它會檢查首選首領是不是當前首領, 如果不是,並且該副本是同步 的,那麼就會觸發首領選舉,讓首選首領成爲當前首領。

找到首選首領 

       從分區的副本清單裏可以很容易找到首選首領(可以使用 kafka.topics.sh 工具查看副本和分區的詳細信息)。清單裏的 第一個副本一般就是首選首領。不管當前首領是哪一個副本,都不會改變這個事實,即使使用副本分配工具將副本重新分配給其他 broker。要記住,如果你手動進行副本分配,第一個指定的副本就是首選首領,所以要確保首選首領被傳播到其他 broker 上,避免讓包含了首領的 broker 負載過重,而其他broker卻無法爲它們分擔負載。

4、處理請求

       broker 的大部分工作是處理客戶端、分區副本和控制器發送給分區首領的請求。 Kafka 提供了一個二進制協議(基於 TCP),指定了請求消息的格式以及 broker 如何對請求作出響應一一包括成功處理請求或在處理請求過程中遇到錯誤。客戶端發起連接併發送請求, broker 處理請求並作出響應。 broker 按照請求到達的順序來處理它們一一這種順序保證讓 Kafka 具有了消息隊列的特性,同時保證保存的消息也是有序的。

所有的請求消息都包含一個標準消息頭
•  Request type (也就是 API key)

•  Request version (broker 可以處理不同版本的客戶端請求,並根據客戶端版本作出不同的響應)

  Correlation ID-一個具有唯一性的數字, 用於標識請求消息,同時也會出現在響應消息和錯誤日誌裏(用於診斷問題)

•  Client ID-用於標識發送請求的客戶端

我們不打算在這裏描述該協議,因爲在 Kafka 文檔裏已經有很詳細的說明。不過,瞭解 broker 如何處理請求還是有必要的一一後面在我們討論 Kafka 監控和各種配置選項時,你就會瞭解到那些與隊列和線程有關的度量指標和配置參數。 

broker 會在它所監聽的每一個端口上運行一個 Acceptor 線程,這個線程會創建一個連接, 並把它交給 Processor  線程去處理。 Processor  線程(也被叫作“網絡線程”)的數量是可配置的。網絡線程負責從客戶端獲取請求悄息,把它們放進請求隊列,然後從響應隊列獲取響應消息,把它們發送給客戶端。圖 1 爲 Kafka 處理請求的內部流程。

                                                                     圖1: Kafka 處理請求的內部流程 

請求消息被放到請求隊列後, IO 線程會負責處理它們。下面是幾種最常見的請求類型。

生產請求

        生產者發送的請求,它包含客戶端要寫入 broker 的消息。

獲取請求

        在消費者和跟隨者副本需要從 broker 讀取消息時發送的請求。

生產請求和獲取請求都必須發送給分區的首領副本。如果 broker 收到一個針對特定分區的請求,而該分區的首領在另一個 broker 上,那麼發送請求的客戶端會收到一個“非分區首領”的錯誤響應。當針對特定分區的獲取請求被髮送到一個不含有該分區首領的 broker 上,也會出現同樣的錯誤。 Kafka 客戶端要自己負責把生產請求和獲取請求發送到正確的 broker 上。 

一般情況下,客戶端會把這些信息緩存起來,並直接往目標 broker 上發送生產請求和 獲取請求。它們需要時不時地通過發送元數據請求來刷新這些信息(刷新的時間間隔通過 metadata.max.age.ms 參數來配置),從而知道元數據是否發生了變更一一比如,在新 broker加入集羣時,部分副本會被移動到新的 broker 上(如圖 2 所示)。另外,如果客戶端收到“非首領”錯誤,它會在嘗試重發請求之前先刷新元數據,因爲這個錯誤說明了客戶端正在使用過期的元數據信息,之前的請求被髮到了錯誤的 broker 上。

                                                                    圖 2:客戶端路由請求

4.1 生產請求

在如何配置生產者的時候,提到過 acks 這個配置參數一一該參數指定了需要多少個 broker 確認纔可以認爲一個消息寫入是成功的。不同的配置對“寫入成功”的界定是不一樣的,如果 acks=1,那麼只要首領收到消息就認爲寫入成功;如果 acks=all,那麼需要所有同步副本收到消息纔算寫入成功;如果 acks=0,那麼生產者在把消息發出去之 後,完全不需要等待 broker 的響應。

包含首領副本的 broker 在收到生產請求時,會對請求做一些驗證。 .

   發送數據的用戶是否有主題寫入權限?

   請求裏包含的 acks 值是否有效(只允許出現0、1 或 all) ?

•   如果 acks=all, 是否有足夠多的罔步副本保證消息已經被安全寫入? (我們可以對 broker 進行配置,如果同步副本的數量不足, broker 可以拒絕處理新消息。)

之後,消息被寫入本地磁盤。在 Linux 系統上,消息會被寫到文件系統緩存裏,並不保證它們何時會被刷新到磁盤上。 Kafl< a 不會一直等待數據被寫到磁盤上一一它依賴複製功能來保證消息的持久性。

在消息被寫入分區的首領之後, broker 開始檢查 acks 配置參數一一如果 acks 被設爲 0 或 1, 那麼 broker 立即返回響應;如果 acks 被設爲 all,那麼請求會被保存在一個叫作煉獄的緩衝區裏,直到首領發現所有跟隨者副本都複製了消息,晌應纔會被返回給客戶端。

4.2 獲取請求

       broker 處理獲取請求的方式與處理生產請求的方式很相似。客戶端發送請求,向 broker 請求主題分區裏具有特定偏移量的消息,好像在說: “請把主題 Test 分區 0 偏移量從 53 開始 的消息以及主題 Test 分區 3 偏移量從 64 開始的消息發給我。”客戶端還可以指定 broker 最多可以從一個分區裏返回多少數據。這個限制是非常重要的,因爲客戶端需要爲 broker 返 回的數據分配足夠的內存。如果沒有這個限制, broker 返回的大量數據有可能耗盡客戶端 的內存。

       我們之前討論過,請求需要先到達指定的分區首領上,然後客戶端通過查詢元數據來確保 請求的路由是正確的。首領在收到請求時,它會先檢查請求是否有效一一比如,指定的偏移量在分區上是否存在?如果客戶端請求的是已經被刪除的數據,或者請求的偏移量不存在,那麼 broker 將返回一個錯誤。 如果請求的偏移量存在, broker 將按照客戶端指定的數量上限從分區裏讀取消息,再把消 息返回給客戶端。 Kafka 使用零複製技術向客戶端發送消息一一也就是說, Kafka 直接把消息從文件(或者更確切地說是 Linux 文件系統緩存)裏發送到網絡通道,而不需要經過任何中間緩衝區。這是 Kafka 與其他大部分數據庫系統不一樣的地方,其他數據庫在將數據發送給客戶端之前會先把它們保存在本地緩存裏。這項技術避免了字節複製,也不需要管理內存緩衝區,從而獲得更好的性能。
       客戶端除了可以設置 broker 返回數據的上限,也可以設置下限。例如,如果把下限設置爲 10KB,就好像是在告訴 broker:“等到有 10KB 數據的時候再把它們發送給我。”在主題消息流量不是很大的情況下,這樣可以減少 CPU 和網絡開銷。客戶端發送一個請求, broker 等到有足夠的數據時才把它們返回給客戶端,然後客戶端再發出請求,而不是讓客戶端每 隔幾毫秒就發送一次請求,每次只能得到很少的數據甚至沒有數據。(如圖 3 所示。)對比這兩種情況,它們最終讀取的數據總量是一樣的,但前者的來回傳送次數更少,因此開銷也更小。

                                                   圖 3: broker 延遲作出響應以便累積足夠的數據

       當然,我們不會讓客戶端一直等待 broker 累積數據。在等待了一段時間之後,就可以把可用的數據拿回處理,而不是一直等待下去。所以,客戶端可以定義一個超時時間,告訴broker:“如果你無法在 X毫秒內累積滿足要求的數據量,那麼就把當前這些數據返回 給我。

       有意思的是,並不是所有保存在分區首領上的數據都可以被客戶端讀取大部分客戶端只能讀取已經被寫入所有同步副本的消息(跟隨者副本也不行,儘管它們也是消費者 否則複製功能就無法工作)。分區首領知道每個消息會被複制到哪個副本上,在消息還沒有被寫入所有同步副本之前,是不會發送給消費者的一一嘗試獲取這些消息的請求會得到空的響應而不是錯誤。
       因爲還沒有被足夠多副本複製的消息被認爲是“不安全”的一一如果首領發生崩潰,另一 個副本成爲新首領,那麼這些消息就丟失了。如果我們允許消費者讀取這些消息,可能就會破壞一致性。試想, 一個消費者讀取並處理了這樣的一個消息,而另一個消費者發現這個消息其實並不存在。 所以,我們會等到所有同步副本複製了這些消息,才允許消費者讀取它們 (如圖 4 所示)。 這也意味着,如果 broker 間的消息複製因爲某些原因變慢,那麼消息到達消費者的時間也會隨之變長(因爲我們會先等待消息複製完畢)。延遲時間可以通過參數 replica.lag.time.max.ms 來配置,它指定了副本在複製消息時可被允許的最大延遲時間。

                                              圖 4:消費者只能看到已經複製到 ISR 的消息

 

4.3 其他請求

       到此爲止,我們討論了 Kafka 最爲常見的幾種請求類型:元數據請求、生產請求和獲取請求。重要的是,我們討論的是客戶揣在網絡上使用的通用二進制協議。 Kafka 內置了由開源社區貢獻者實現和維護的 Java 客戶端,同時也有用其他語言實現的客戶端,如 C、 Python、 Go 語言等。 Kafka 網站上有它們的完整潔單,這些客戶端就是使用這個二進制協 議與 broker 通信的。
       另外, broker 之間也使用同樣的通信協議。它們之間的請求發生在 Kafka 內部,客戶端不應該使用這些請求。例如,當一個新首領被選舉出來,控制器會發送 LeaderAndIsr 請求給新首領(這樣它就可以開始接收來自客戶端的請求)和跟隨者(這樣它們就知道要開始跟隨新首領)。
       Kafka 協議可以處理 20 種不同類型的請求,而且會有更多的類型加入進來。協議在持續演化一一隨着客戶端功能的不斷增加,我們需要改進協議來滿足需求。例如,之前的 Kafka 消費者使用 Zookeeper 來跟蹤偏移量,在消費者啓動的時候, 它通過檢查保存在 Zookeeper 上的偏移量就可以知道從哪裏開始處理消息。因爲各種原因,我們決定不再使用 Zookeeper 來保存偏移量,而是把偏移量保存在特定的 Kafka 主題上。爲了達到這個目的,我們不得不往協議裏增加幾種請求類型: OffsetCommitRequest、OffsetFetchRequest 和 ListOffsetsRequest。現在,在應用程序調用 commitOffset()方法 時,客戶端不再把偏移量寫入 Zookeeper,而是往 Kafka 發送 OffsetCommitRequest 請求。
       主題的創建仍然需要通過命令行工具來完成,命令行工具會直接更新 Zookeeper 裏的主題列表, broker 監聽這些主題列表,在有新主題加入時,它們會收到通知。我們正在改進 Kafka,增加了 CreateTopicRequest 請求類型,這樣客戶端(包括那些不支持 Zookeeper 客戶端的編程語言)就可以直接向 broker 請求創建新主題了。
       除了往協議裏增加新的請求類型外,我們也會通過修改已有的請求類型來給它們增加新功能。例如,從 Kafka 0.9.0 到 Kafka 0.10.0,我們希望能夠讓客戶端知道誰是當前的控制器,於是把控制器信息添加到元數據響應消息裏。我們還在元數據請求消息和響應消息裏添加了 一個新的 version 字段。現在, 0.9.0 版本的客戶端發送的元數據請求裏 version 爲 0 (0.9.0 版本客戶端的 version 不會是 1 )。不管是 0.9.0 版本的 broker,還是 0.10.0 版本的 broker,它們 都知道應該返回 version 爲 0 的響應, 也就是不包含控制器信息的響應。 0.9.0 版本的客戶端不需要控制器的信息,而且也沒必要知道如何去解析它。 0.10.0 版本的客戶端會發送 version 爲 1 的元數據請求, 010.0 版本的 broker 會返回 version 爲 1 的響應,裏面包含了控制器的信息。如果 0.10.0 版本的客戶端發送 version 爲 1的請求給 0.9.0 版本的 broker,這個版本的 broker 不知道該如何處理這個請求,就會返回一個錯誤。這就是爲什麼我們建議在升級客戶端之前先升級broker,因爲新的 broker 知道如何處理舊的請求,反過來則不然。
        我們在 0.10.0 版本的 Kafka 里加入了 ApiVersionRequest——客戶端可以詢問 broker 支持哪些版本的請求,然後使用正確的版本與 broker 通信。如果能夠正確使用這個新功能,客戶端就可以與舊版本的 broker 通信,只要 broker 支持這個版本的協議。

5、物理存儲

       Kafka 的基本存儲單元是分區。分區無法在多個 broker 間進行再細分,也無法在同一個 broker 的多個磁盤上進行再細分。 所以,分區的大小受到單個掛載點可用空間的限制(一 個掛載點由單個磁盤或多個磁盤組成,如果配置了 JBOD,就是單個磁盤,如果配置了 RAID,就是多個磁盤。)。

       在配置 Kafka 的時候,管理員指定了一個用於存儲分區的目錄清單一一也就是 log.dirs 參數的值(不要把它與存放錯誤日誌的目錄說淆了,日誌目錄是配置在 log4j.properties 文件裏的)。該參數一般會包含每個掛載點的目錄。
       接下來我們會介紹 Kafka 是如何使用這些目錄來存儲數據的。首先,我們要知道數據是如何被分配到集羣的 broker 上以及 broker 的目錄裏的。然後,我們還要知道 broker 是如何管理這些文件的,特別是如何進行數據保留的。隨後,我們會深入探討文件和索引格式。最後,我們會討論日誌壓縮及其工作原理。日誌壓縮是 Kafka 的一個高級特性,因爲有了這 個特性, Kafka 可以用來長時間地保存數據。

5.1 分區分配

       在創建主題時, Kafka 首先會決定如何在 broker 間分配分區。假設你有 6 個 broker,打算創建一個包含 10 個分區的主題,並且複製係數爲 3。那麼 Kafka 就會有 30 個分區副本, 它們可以被分配給 6 個 broker。在進行分區分配時,我們要達到如下的目標。
    在 broker 間平均地分佈分區副本。對於我們的例子來說,就是要保證每個 broker 可以 分到 5 個副本。

    確保每個分區的每個副本分佈在不同的 broker 上。假設分區 0 的首領副本在 broker 2 上, 那麼可以把跟隨者副本放在 broker 3 和 broker 4 上,但不能放在 broker 2 上,也不能兩個都放在 broker 3 上。

    如果爲 broker 指定了機架信息,那麼儘可能把每個分區的副本分配到不同機架的 broker 上。這樣做是爲了保證一個機架的不可用不會導致整體的分區不可用。
       爲了實現這個目標,我們先隨機選擇一個 broker (假設是 4),然後使用輪詢的方式給每個 broker 分配分區來確定首領分區的位置。於是,首領分區 0 會在 broker 4 上,首領分區 1會在 broker 5 上,首領分區 2 會在 broker 0上(只有 6 個 broker),並以此類推。然後,我們從分區首領開始,依次分配跟隨者副本。如果分區 0 的首領在 broker 4 上,那麼它的 第一個跟隨者副本會在 broker 5 上,第二個跟隨者副本會在 broker 0上。分區 1的首領在 broker 5 上,那麼它的第一個跟隨者副本在 broker 0 上,第二個跟隨者副本在 broker 1上。
       如果配置了機架信息,那麼就不是按照數字順序來選擇 broker 了,而是按照交替機架的方式來選擇 broker。假設 broker 0、 broker1 和 broker2 放置在同一個機架上, broker 3、 broker 4 和 broker 5 分別放置在其他不同的中幾架上。我們不是按照從 0 到 5 的順序來選擇 broker,而是按照 0, 3, 1, 4, 2, 5 的順序來選擇,這樣每個相鄰的 broker 都在不同的機架上(如圖 5 所示)。於是,如果分區 0 的首領在 broker 4 上,那麼第一個跟隨者副本會在 broker 2 上, 這兩個 broker 在不同的機架上。如果第一個機架下線,還有其他副本仍然活躍着,所以分區仍然可用。這對所有副本來說都是一樣的,因此在機架下線時仍然能夠保證可用性。

                                                      圖 5:分配給不同機架 broker 的分區和副本

       爲分區和副本選好合適的 broker 之後,接下來要決定這些分區應該使用哪個目錄。我們單獨爲每個分區分配目錄,規則很簡單: 計算每個目錄裏的分區數量,新的分區總是被添加 到數量最小的那個目錄裏。也就是說,如果添加了一個新磁盤,所有新的分區都會被創建 到這個磁盤上。因爲在完成分配工作之前,新磁盤的分區數量總是最少的。 

注意磁盤空間

       要注意,在爲 broker 分配分區時並沒有考慮可用空間和工作負載問題,但在將分區分配到磁盤上時會考慮分區數量,不過不考慮分區大小。 也就是說, 如果有些 broker 的磁盤空間比其他 broker 要大(有可能是因爲集羣同時使用了舊服務器和新服務器),有些分區異常大,或者同一個 broker 上有大小不同的磁盤,那麼在分配分區時要格外小心。在後面的章節中,我們會討論 Kafka 管理員該如何解決這種 broker 負載不均衡的問題。

5.2 文件管理

       保留數據是 Kafka 的一個基本特性, Kafka 不會一直保留數據,也不會等到所有消費者都讀取了消息之後才刪除消息。相反, Kafka 管理員爲每個主題配置了數據保留期限,規定數據被刪除之前可以保留多長時間,或者清理數據之前可以保留的數據量大小。
       因爲在一個大文件裏查找和刪除消息是很費時的,也很容易出錯,所以我們把分區分成若干個片段。 默認情況下,每個片段包含 1GB 或一週的數據,以較小的那個爲準。在 broker 往分區寫入數據時,如果達到片段上限,就關閉當前文件,並打開一個新文件。 當前正在寫入數據的片段叫作活躍片段。活動片段永遠不會被刪除,所以如果你要保留數據 1 天,但片段裏包含了 5 天的數據,那麼這些數據會被保留 5 天,因爲在片段被關閉之前這些數據無法被刪除。 如果你要保留數據一週,而且每天使用一個新片段,那麼你就會看到,每天在使用一個新片段的同時會刪除一個最老的片段一一所以大部分時間該分區會有7個片段存在。  broker 會爲分區裏的每個片段打開一個文件句柄,哪怕片段是不活躍 的。 這樣會導致打開過多的文件句柄,所以操作系統必須根據實際情況做一些調優。

5.3 文件格式

       我們把 Kafka 的消息和偏移量保存在文件裏。保存在磁盤上的數據格式與從生產者發送過來或者發送給消費者的消息格式是一樣的。因爲使用了相同的消息格式進行磁盤存儲和網絡傳輸, Kafka 可以使用零複製技術給消費者發送消息,同時避免了對生產者已經壓縮過的消息進行解壓和再壓縮。

       除了鍵、值和偏移量外, 消息裏還包含了消息大小、校驗和、消息格式版本號、壓縮算法 (Snappy、 GZip 或 LZ4)和時間戳(在 0.10.0 版本里引入的)。時間戳可以是生產者發送消息的時間,也可以是消息到達 broker 的時間,這個是可配置的。

       如果生產者發送的是壓縮過的消息,那麼同一個批次的消息會被壓縮在一起,被當作“包裝消息”進行發送(如圖 6 所示)。於是, broker 就會收到一個這樣的消息,然後再把它發應給消費者。消費者在解壓這個消息之後,會看到整個批次的消息,它們都有自己的時間戳和偏移量。

                                                                      圖 6:普通消息和包裝消息
       也就是說,如果在生產者端使用了壓縮功能(極力推薦),那麼發送的批次越大,就意味着在網絡傳輸和磁盤存儲方面會獲得越好的壓縮性能,同時意味着如果修改了消費者使用 的消息格式(例如,在消息裏增加了時間戳),那麼網絡傳輸和磁盤存儲的格式也要隨之修改,而且 broker 要知道如何處理包含了兩種消息格式的文件。

       Kafka 附帶了一個叫 DumpLogSegment的工具,可以用它查看片段的內容。它可以顯示每個消息的偏移量、校驗和、魔術數字 節、消息大小和壓縮算桂。運行該工具的方能如下:

                                                                 bin/kafka - run -class .sh kafka.tools.DumpLogSegments

如果使用了--deep-iteration 參數,可以顯示被壓縮到包裝消息裏的消息。

5.4 索引

       消費者可以從 Kafka 的任意可用偏移量位置開始讀取消息。假設消費者要讀取從偏移量 100 開始的 1MB 消息,那麼 broker 必須立即定位到偏移量 100 (可能是在分區的任意一個片段裏),然後開始從這個位置讀取消息。爲了幫助 broker 更快地定位到指定的偏移量, Kafka 爲每個分區維護了一個索引。索引把偏移量映射到片段文件和偏移量在文件裏的位置。 索引也被分成片段,所以在刪除消息時,也可以刪除相應的索引。 Kafka 不維護索引的 校驗和。如果索引出現損壞, Kafka 會通過重新讀取消息並錄製偏移量和位置來重新生成索引。如果有必要,管理員可以刪除索引,這樣做是絕對安全的, Kafka 會自動重新生成這些索引。

5.5 清理

        一般情況下, Kafka 會根據設置的時間保留數據,把超過時效的舊數據刪除掉。不過,試想一下這樣的場景,如果你使用 Kafka 保存客戶的收貨地址,那麼保存客戶的最新地址比保存客戶上週甚至去年的地址要有意義得多,這樣你就不用擔心會用錯舊地址,而且短時間內客戶也不會修改新地址。另外一個場景, 一個應用程序使用 Kafka 保存它的狀態, 每次狀態發生變化,它就把狀態寫入 Kafka。在應用程序從崩潰中恢復時,它從 Kafka 讀取消息來恢復最近的狀態。在這種情況下,應用程序只關心它在崩潰前的那個狀態,而不關心運行過程中的那些狀態。 Kafka 通過改變主題的保留策略來滿足這些使用場景。早於保留時間的舊事件會被刪除, 爲每個鍵保留最新的值,從而達到清理的效果。很顯然,只有當應用程序生成的事件裏包含了鍵值對時,爲這些主題設置 compact 策略纔有意義。如果主題包含 null 鍵, 清理就會失敗

5.6 清理的工作原理

每個日誌片段可以分爲以下兩個部分。
乾淨的部分 : 這些消息之前被清理過,每個鍵只有一個對應的值,這個值是上一次清理時保留下來的。

污濁的部分 : 這些消息是在上一次清理之後寫入的。

兩個部分的日誌片段示意如圖 7 所示。

                                                        圖 7:包含乾淨和污濁兩個部分的分區

       如果在 Kafka 啓動時啓用了清理功能(通過配置 log.cleaner.enabled 參數),每個 broker 會啓動一個清理管理器線程和多個清理線程,它們負責執行清理任務。這些線程會選擇污濁率(污濁消息佔分區總大小的比例)較高的分區進行清理。

       爲了清理分區,清理線程會讀取分區的污濁部分,並在內存裏創建一個 map。 map 裏的每個元素包含了消息鍵的散列值和消息的偏移量鍵的散列值是 16B,加上偏移量總共是 24B。 如果要清理一個 1GB 的日誌片段,並假設每個消息大小爲 1KB,那麼這個片段就包含一百萬個悄息,而我們只需要用 24MB 的 map 就可以清理這個片段。(如果有重複的鍵, 可以重用散列項,從而使用更少的內存。)這是非常高效的!

       管理員在配置 Kafka 時可以對 map 使用的內存大小進行配置。每個線程都有自己的 map, 而這個參數指的是所有線程可使用的內存總大小。如果你爲 map 分配了 lGB 內存,並使用了 5 個清理線程,那麼每個錢程可以使用 200MB 內存來創建自己的 map。 Kafka 並不要求分區的整個污濁部分來適應這個 map 的大小,但要求至少有一個完整的片段必須符合。 如果不符合,那麼 Kafka 就會報錯,管理員要麼分配更多的內存,要麼減少清理線程數 。如果只有少部分片段可以完全符合, Kafka 將從最舊的片段開始清理,等待下一次清理剩餘的部分。 

       清理線程在創建好偏移量 map 後,開始從乾淨的片段處讀取消息,從最舊的消息開始,把它們的內容與 map 裏的內容進行比對。它會檢查消息的鍵是否存在於 map 中,如果不存在, 那麼說明消息的值是最新的,就把消息複製到替換片段上。 如果鍵已存在,消息會被忽略, 因爲在分區的後部已經有一個具有相同鍵的消息存在。在複製完所有的消息之後,我們就將替換片段與原始片段進行交換,然後開始清理下一個片段。完成整個清理過程之後,每個鍵對應一個不同的消息——這些消息的值都是最新的。清理前後的分區片段如圖 8 所示。

                                                           圖 8:清理前後的分區片段
5.7 被刪除的事件

       如果只爲每個鍵保留最近的一個消息,那麼當需要刪除某個特定鍵所對應的所有消息時, 我們該怎麼辦?這種情況是有可能發生的,比如一個用戶不再使用我們的服務,那麼完全可以把與這個用戶相關的所有信息從系統中刪除。 爲了徹底把一個鍵從系統裏刪除,應用程序必須發送一個包含該鍵且值爲 null 的消息。清理線程發現該悄息時,會先進行常規的清理,只保留值爲 null 的消息。該消息(被稱爲墓碑消息)會被保留一段時間,時間長短是可配置的。在這期間,消費者可以看到這個墓碑消息,井且發現它的值已經被刪除。於是,如果消費者往數據庫裏複製 Kafka 的數據, 當它看到這個墓碑消息時,就知道應該要把相關的用戶信息從數據庫裏刪除。在這個時間段過後,清理線程會移除這個墓碑消息,這個鍵也將從 Kafka 分區裏消失。重要的是,要留 給消費者足夠多的時間,讓他看到墓碑消息,因爲如果消費者離線幾個小時並錯過了墓碑消息,就看不到這個鍵,也就不知道它已經從 Kafka 裏刪除,從而也就不會去刪除數據庫裏的相關數據了。

5.8 何時會清理主題

       就像 delete 策略不會刪除當前活躍的片段一樣, compact 策略也不會對當前片段進行清理。 只有舊片段裏的消息纔會被清理。

       在 0.10.0 和更早的版本里, Kafka 會在包含髒記錄的主題數量達到 50% 時進行清理。 這樣做的目的是避免太過頻繁的清理(因爲清理會影響主題的讀寫性能),同時也避免存在太多髒記錄(因爲它們會佔用磁盤空間)。浪費 50% 的磁盤空間給主題存放髒記錄,然後進行一次清理,這是個合理的折中,管理員也可以對它進行調整。 我們計劃在未來的版本中加入寬限期,在寬限期內,我們保證消息不會被清理。對於想看到主題的每個消息的應用程序來說,它們就有了足夠的時間,即使時間有點滯後。

文章出處:《Kafka權威指南》

 

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