Kafka 分片存儲機制
Broker:消息中間件處理結點,一個 Kafka 節點就是一個 broker,多個 broker 可以組成一個 Kafka集群。
Topic:一類消息,例如 page view 日志、click 日志等都可以以 topic 的形式存在,Kafka 集群能夠同時負責多個 topic 的分發。
Partition:topic 物理上的分組,一個 topic 可以分為多個 partition,每個 partition 是一個有序的隊列。
Segment:partition 物理上由多個 segment 組成,下面有詳細說明。
offset:每個 partition 都由一系列有序的、不可變的消息組成,這些消息被連續的追加到 partition中。partition 中的每個消息都有一個連續的序列號叫做 offset,用於 partition中唯一標識的這條消息。
topic 中 partition 存儲分布
- 每個 partion(目錄)相當於一個巨型文件被平均分配到多個大小相等 segment(段)數據文件中。但每個段 segment file 消息數量不一定相等,這種特性方便 old 磁盤順序讀取的速度非常快,比隨機讀取要快segment file 快速被刪除。(默認情況下每個文件大小為 1G)
- 每個 partiton 只需要支持順序讀寫就行了,segment 文件生命周期由服務端配置參數決定。
存.index和.log文件,通過索引來定位log文件中的消息
這樣做的好處就是能快速刪除無用文件,有效提高磁盤利用率。
Kafka 消息分發和消費者 push、pull 機制
消息分發
Producer 客戶端負責消息的分發
- kafka 集群中的任何一個 broker 都可以向 producer 提供 metadata 信息,這些 metadata 中包含”集群中存活的 servers 列表”/”partitions leader 列表”等信息;
- 當 producer 獲取到 metadata 信息之后, producer 將會和 Topic 下所有 partition leader 保持socket 連接;
- 消息由 producer 直接通過 socket 發送到 broker,中間不會經過任何”路由層”,事實上,消息被路由到哪個 partition 上由 producer 客戶端決定;比如可以采用”random”“key-hash”“輪詢”等,如果一個 topic 中有多個 partitions,那么在 producer 端實現”消息均衡分發”是必要的。
- 在 producer 端的配置文件中,開發者可以指定 partition 路由的方式。
Producer 消息發送的應答機制
設置發送數據是否需要服務端的反饋,有三個值 0,1,-1
- 0: producer 不會等待 broker 發送 ack
- 1: 當 leader 接收到消息之后發送 ack
- -1: 當所有的 follower 都同步消息成功后發送 ack
request.required.acks=0
消費者 push、pull 機制
作為一個 message system,kafka 遵循了傳統的方式,選擇由 kafka 的 producer 向 broker push 信息,而 consumer 從 broker pull 信息。
consumer 獲取消息,可以使用兩種方式:push 或 pull 模式。下面我們簡單介紹一下這兩種區別:
push 模式
常見的 push 模式如 storm 的消息處理,由 spout 負責消息的推送。該模式下需要一個中心節點,負責消息的分配情況(哪段消息分配給 consumer1,哪段消息分配給 consumer2),同時還要監聽 consumer的 ack 消息用於判斷消息是否處理成功,如果在 timeout 時間內為收到響應可以認為該 consumer 掛掉,需要重新分配 sonsumer 上失敗的消息。這種模式有個問題,不太容易實現我們想要的消息回放功能,因為理想情況下由 consumer 決定我到底要消費什么,而這種模式完全由 master 決定。
pull 模式
pull 模式由 consumer 決定消息的消費情況,這種模式有一個好處是我們不需要返回 ack 消息,因為當 consumer 申請消費下一批消息時就可以認為上一批消息已經處理完畢,也不需要處理超時的問題,consumer 可以根據自己的消費能力來消費消息。但這個還有一個問題,如何保證處理的消息的不會重復呢,kafka 具體做法就是增加隊列的並發度(partition),可以一個 partition 對准一個 consumer。
綜上,kafka 的 consumer 之所以沒有采用 push 模式,是因為 push 模式很難適應消費者速率不同的消費者而且很難實現消息的回放功能,因為消息發送速率是由 broker 決定的。push 模式的目標就是盡可能以最快速度傳遞消息,但是這樣很容易造成 consumer 來不及處理消息,典型的表現就是拒絕服務以及網絡擁塞,而 pull 模式則可以根據 consumer 的消費能力以適當的速率消費 message。
pull 與 push 的區別
pull 技術:
客戶機向服務器請求信息;
kafka 中,consuemr 根據自己的消費能力以適當的速率消費信息
push 技術:
服務器主動將信息發往客戶端的技術;
push 模式的目標就是盡可能以最快的速率傳遞消息。
Kafka 持久化
概述
Kafka 大量依賴文件系統去存儲和緩存消息。對於硬盤有個傳統的觀念是硬盤總是很慢,這使很多人懷疑基於文件系統的架構能否提供優異的性能。實際上硬盤的快慢完全取決於使用它的方式。設計良好的硬盤架構可以和內存一樣快。
在 6 塊 7200 轉的 SATA RAID-5 磁盤陣列的線性寫速度差不多是 600MB/s,但是隨即寫的速度卻是100k/s,差了差不多 6000 倍。現在的操作系統提供了預讀取和后寫入的技術。實際上發現線性的訪問磁盤,很多時候比隨機的內存訪問快得多。
為了提高性能,現代操作系統往往使用內存作為磁盤的緩存,現代操作系統樂於把所有空閑內存用作磁盤緩存,雖然這可能在緩存回收和重新分配時犧牲一些性能。所有的磁盤讀寫操作都會經過這個緩存,這不太可能被繞開除非直接使用 I/O。所以雖然每個程序都在自己的線程里只緩存了一份數據,但在操作系統的緩存里還有一份,這等於存了兩份數據。
基於 jvm 內存有以下缺點:
- Java 對象占用空間是非常大的,差不多是要存儲的數據的兩倍甚至更高。
- 隨着堆中數據量的增加,垃圾回收回變的越來越困難,而且可能導致錯誤
基於以上分析,如果把數據緩存在內存里,因為需要存儲兩份,不得不使用兩倍的內存空間,Kafka 基於JVM,又不得不將空間再次加倍,再加上要避免 GC 帶來的性能影響,在一個 32G 內存的機器上,不得不使用到 28-30G 的內存空間。並且當系統重啟的時候,又必須要將數據刷到內存中( 10GB 內存差不多要用 10 分鍾),就算使用冷刷新(不是一次性刷進內存,而是在使用數據的時候沒有就刷到內存)也會導致最初的時候新能非常慢。
基於操作系統的文件系統來設計有以下好處:
- 可以通過 os 的 pagecache 來有效利用主內存空間,由於數據緊湊,可以 cache 大量數據,並且沒有 gc 的壓力
- 即使服務重啟,緩存中的數據也是熱的(不需要預熱)。而基於進程的緩存,需要程序進行預熱,而且會消耗很長的時間。(10G 大概需要 10 分鍾)
- 大大簡化了代碼。因為在緩存和文件系統之間保持一致性的所有邏輯都在 OS 中。以上建議和設計使得代碼實現起來十分簡單,不需要盡力想辦法去維護內存中的數據,數據會立即寫入磁盤。
總的來說,Kafka 不會保持盡可能多的內容在內存空間,而是盡可能把內容直接寫入到磁盤。所有的數據都及時的以持久化日志的方式寫入到文件系統,而不必要把內存中的內容刷新到磁盤中。
日志數據持久化特性
寫操作:通過將數據追加到文件中實現
讀操作:讀的時候從文件中讀就好了
優勢
✓讀操作不會阻塞寫操作和其他操作(因為讀和寫都是追加的形式,都是順序的,不會亂,所以不會發生阻塞),數據大小不對性能產生影響;
✓ 沒有容量限制(相對於內存來說)的硬盤空間建立消息系統;
✓ 線性訪問磁盤,速度快,可以保存任意一段時間!
文件存儲結構
topic 在邏輯上可以被認為是一個 queue。每條消費都必須指定它的 topic,可以簡單理解為必須指明把這條消息放進哪個 queue 里。為了使得 Kafka 的吞吐率可以水平擴展,物理上把 topic 分成一個或多個partition,每個 partition 在物理上對應一個文件夾,該文件夾下存儲這個 partition 的所有消息和索引文件。
每一個partition目錄下的文件被平均切割成大小相等(默認一個文件是500兆,可以手動去設置)的數據文件,
每一個數據文件都被稱為一個段(segment file),但每個段消息數量不一定相等,這種特性能夠使得老的segment可以被快速清除。
默認保留7天的數據。
首先00000000000000000000.log文件是最早產生的文件,該文件達到1G(因為我們在配置文件里面指定的1G大小,默認情況下是500兆)
之后又產生了新的0000000000000672348.log文件,新的數據會往這個新的文件里面寫,這個文件達到1G之后,數據就會再往下一個文件里面寫,
也就是說它只會往文件的末尾追加數據,這就是順序寫的過程,生產者只會對每一個partition做數據的追加(寫)的操作。
上圖的左半部分是索引文件,里面存儲的是一對一對的key-value,其中key是消息在數據文件(對應的log文件)中的編號,比如“1,3,6,8……”,分別表示在log文件中的第1條消息、第3條消息、第6條消息、第8條消息……,那么為什么在index文件中這些編號不是連續的呢?這是因為index文件中並沒有為數據文件中的每條消息都建立索引,而是采用了稀疏存儲的方式,每隔一定字節的數據建立一條索引。這樣避免了索引文件占用過多的空間,從而可以將索引文件保留在內存中。但缺點是沒有建立索引的Message也不能一次定位到其在數據文件的位置,從而需要做一次順序掃描,但是這次順序掃描的范圍就很小了。
其中以索引文件中元數據3,497為例,其中3代表在右邊log數據文件中從上到下第3個消息(在全局partiton表示第368772個消息),其中497表示該消息的物理偏移地址(位置)為497。
kafka 日志分為 index 與 log,兩個成對出現;index 文件存儲元數據(用來描述數據的數據,這也可能是為什么 index 文件這么大的原因了),log 存儲消息。索引文件元數據指向對應 log 文件中 message的遷移地址;例如 2,128 指 log 文件的第 2 條數據,偏移地址為 128;而物理地址(在 index 文件中指定)+ 偏移地址可以定位到消息。
因為每條消息都被 append 到該 partition 中,是順序寫磁盤,因此效率非常高(經驗證,順序寫磁盤效
率比隨機寫內存還要高,這是 Kafka 高吞吐率的一個很重要的保證)。