天天看點

為什麼 Kafka 速度那麼快?

為什麼 Kafka 速度那麼快?

Kafka的消息是儲存或緩存在磁盤上的,一般認為在磁盤上讀寫資料是會降低性能的,因為尋址會比較消耗時間,但是實際上,Kafka的特性之一就是高吞吐率。

即使是普通的伺服器,Kafka也可以輕松支援每秒百萬級的寫入請求,超過了大部分的消息中間件,這種特性也使得Kafka在日志處理等海量資料場景廣泛應用。

針對Kafka的基準測試可以參考,Apache Kafka基準測試:每秒寫入2百萬(在三台廉價機器上)

下面從資料寫入和讀取兩方面分析,為什麼為什麼Kafka速度這麼快。

寫入資料

Kafka會把收到的消息都寫入到硬碟中,它絕對不會丢失資料。為了優化寫入速度Kafka采用了兩個技術, 順序寫入 和 MMFile 。

順序寫入

磁盤讀寫的快慢取決于你怎麼使用它,也就是順序讀寫或者随機讀寫。在順序讀寫的情況下,某些優化場景磁盤的讀寫速度可以和記憶體持平(注:此處有疑問, 不推敲細節,參考

http://searene.me/2017/07/09/Why-is-Kafka-so-fast/

)。

因為硬碟是機械結構,每次讀寫都會尋址->寫入,其中尋址是一個“機械動作”,它是最耗時的。是以硬碟最讨厭随機I/O,最喜歡順序I/O。為了提高讀寫硬碟的速度,Kafka就是使用順序I/O。

而且Linux對于磁盤的讀寫優化也比較多,包括read-ahead和write-behind,磁盤緩存等。如果在記憶體做這些操作的時候,一個是JAVA對象的記憶體開銷很大,另一個是随着堆記憶體資料的增多,JAVA的GC時間會變得很長,使用磁盤操作有以下幾個好處:

磁盤順序讀寫速度超過記憶體随機讀寫

JVM的GC效率低,記憶體占用大。使用磁盤可以避免這一問題

系統冷啟動後,磁盤緩存依然可用

為什麼 Kafka 速度那麼快?

上圖就展示了Kafka是如何寫入資料的, 每一個Partition其實都是一個檔案 ,收到消息後Kafka會把資料插入到檔案末尾(虛框部分)。

這種方法有一個缺陷—— 沒有辦法删除資料 ,是以Kafka是不會删除資料的,它會把所有的資料都保留下來,每個消費者(Consumer)對每個Topic都有一個offset用來表示 讀取到了第幾條資料 。

為什麼 Kafka 速度那麼快?

兩個消費者,Consumer1有兩個offset分别對應Partition0、Partition1(假設每一個Topic一個Partition);Consumer2有一個offset對應Partition2。這個offset是由用戶端SDK負責儲存的,Kafka的Broker完全無視這個東西的存在;一般情況下SDK會把它儲存到zookeeper裡面。(是以需要給Consumer提供zookeeper的位址)。

如果不删除硬碟肯定會被撐滿,是以Kakfa提供了兩種政策來删除資料。一是基于時間,二是基于partition檔案大小。具體配置可以參看它的配置文檔。

Memory Mapped Files

即便是順序寫入硬碟,硬碟的通路速度還是不可能追上記憶體。是以Kafka的資料并 不是實時的寫入硬碟 ,它充分利用了現代作業系統 分頁存儲 來利用記憶體提高I/O效率。

Memory Mapped Files(後面簡稱mmap)也被翻譯成 記憶體映射檔案 ,在64位作業系統中一般可以表示20G的資料檔案,它的工作原理是直接利用作業系統的Page來實作檔案到實體記憶體的直接映射。完成映射之後你對實體記憶體的操作會被同步到硬碟上(作業系統在适當的時候)。

通過mmap,程序像讀寫硬碟一樣讀寫記憶體(當然是虛拟機記憶體),也不必關心記憶體的大小有虛拟記憶體為我們兜底。

使用這種方式可以擷取很大的I/O提升, 省去了使用者空間到核心空間 複制的開銷(調用檔案的read會把資料先放到核心空間的記憶體中,然後再複制到使用者空間的記憶體中。)也有一個很明顯的缺陷——不可靠, 寫到mmap中的資料并沒有被真正的寫到硬碟,作業系統會在程式主動調用flush的時候才把資料真正的寫到硬碟。

Kafka提供了一個參數——producer.type來控制是不是主動flush,如果Kafka寫入到mmap之後就立即flush然後再傳回Producer叫 同步 (sync);寫入mmap之後立即傳回Producer不調用flush叫 異步 (async)。

讀取資料

Kafka在讀取磁盤時做了哪些優化?

基于sendfile實作Zero Copy

傳統模式下,當需要對一個檔案進行傳輸的時候,其具體流程細節如下:

調用read函數,檔案資料被copy到核心緩沖區

read函數傳回,檔案資料從核心緩沖區copy到使用者緩沖區

write函數調用,将檔案資料從使用者緩沖區copy到核心與socket相關的緩沖區。

資料從socket緩沖區copy到相關協定引擎。

以上細節是傳統read/write方式進行網絡檔案傳輸的方式,我們可以看到,在這個過程當中,檔案資料實際上是經過了四次copy操作:

硬碟—>核心buf—>使用者buf—>socket相關緩沖區—>協定引擎

而sendfile系統調用則提供了一種減少以上多次copy,提升檔案傳輸性能的方法。

在核心版本2.1中,引入了sendfile系統調用,以簡化網絡上和兩個本地檔案之間的資料傳輸。sendfile的引入不僅減少了資料複制,還減少了上下文切換。

sendfile(socket, file, len);

運作流程如下:

sendfile系統調用,檔案資料被copy至核心緩沖區

再從核心緩沖區copy至核心中socket相關的緩沖區

最後再socket相關的緩沖區copy到協定引擎

相較傳統read/write方式,2.1版本核心引進的sendfile已經減少了核心緩沖區到user緩沖區,再由user緩沖區到socket相關緩沖區的檔案copy,而在核心版本2.4之後,檔案描述符結果被改變,sendfile實作了更簡單的方式,再次減少了一次copy操作。

在apache,nginx,lighttpd等web伺服器當中,都有一項sendfile相關的配置,使用sendfile可以大幅提升檔案傳輸性能。

Kafka把所有的消息都存放在一個一個的檔案中,當消費者需要資料的時候Kafka直接把檔案發送給消費者,配合mmap作為檔案讀寫方式,直接把它傳給sendfile。

批量壓縮

在很多情況下,系統的瓶頸不是CPU或磁盤,而是網絡IO,對于需要在廣域網上的資料中心之間發送消息的資料流水線尤其如此。進行資料壓縮會消耗少量的CPU資源,不過對于kafka而言,網絡IO更應該需要考慮。

如果每個消息都壓縮,但是壓縮率相對很低,是以Kafka使用了批量壓縮,即将多個消息一起壓縮而不是單個消息壓縮

Kafka允許使用遞歸的消息集合,批量的消息可以通過壓縮的形式傳輸并且在日志中也可以保持壓縮格式,直到被消費者解壓縮

Kafka支援多種壓縮協定,包括Gzip和Snappy壓縮協定

總結

Kafka速度的秘訣在于,它把所有的消息都變成一個批量的檔案,并且進行合理的批量壓縮,減少網絡IO損耗,通過mmap提高I/O速度,寫入資料的時候由于單個Partion是末尾添加是以速度最優;讀取資料的時候配合sendfile直接暴力輸出。

關注Java技術棧微信公衆号,在背景回複關鍵字:Java,可以擷取一份棧長整理的 Java 最新技術幹貨。