Kafka零拷貝

Kafka除了具備消息隊列MQ的特性和使用場景外,它還有一個重要用途,就是做存儲層。

用kafka做存儲層,爲什麼呢?一大堆可以做數據存儲的 MySQL、MongoDB、HDFS……

因爲kafka數據是持久化磁盤的,還速度快;還可靠、支持分佈式……

啥!用了磁盤,還速度快!!!

沒錯,kafka就是速度無敵,本文將探究kafka無敵性能背後的祕密。

首先要有個概念,kafka高性能的背後,是多方面協同後、最終的結果,kafka從宏觀架構、分佈式partition存儲、ISR數據同步、以及“無孔不入”的高效利用磁盤/操作系統特性,這些多方面的協同,是kafka成爲性能之王的必然結果。

本文將從kafka零拷貝,探究其是如何“無孔不入”的高效利用磁盤/操作系統特性的。


先說說零拷貝

零拷貝並不是不需要拷貝,而是減少不必要的拷貝次數。通常是說在IO讀寫過程中。

實際上,零拷貝是有廣義和狹義之分,目前我們通常聽到的零拷貝,包括上面這個定義減少不必要的拷貝次數都是廣義上的零拷貝。其實瞭解到這點就足夠了。

我們知道,減少不必要的拷貝次數,就是爲了提高效率。那零拷貝之前,是怎樣的呢?

聊聊傳統IO流程

比如:讀取文件,再用socket發送出去
傳統方式實現:
先讀取、再發送,實際經過1~4四次copy。

buffer = File.read 
Socket.send(buffer)

1、第一次:將磁盤文件,讀取到操作系統內核緩衝區;
2、第二次:將內核緩衝區的數據,copy到application應用程序的buffer;
3、第三步:將application應用程序buffer中的數據,copy到socket網絡發送緩衝區(屬於操作系統內核的緩衝區);
4、第四次:將socket buffer的數據,copy到網卡,由網卡進行網絡傳輸。

傳統方式,讀取磁盤文件並進行網絡發送,經過的四次數據copy是非常繁瑣的。實際IO讀寫,需要進行IO中斷,需要CPU響應中斷(帶來上下文切換),儘管後來引入DMA來接管CPU的中斷請求,但四次copy是存在“不必要的拷貝”的。

重新思考傳統IO方式,會注意到實際上並不需要第二個和第三個數據副本。應用程序除了緩存數據並將其傳輸回套接字緩衝區之外什麼都不做。相反,數據可以直接從讀緩衝區傳輸到套接字緩衝區。

顯然,第二次和第三次數據copy 其實在這種場景下沒有什麼幫助反而帶來開銷,這也正是零拷貝出現的意義。

這種場景:是指讀取磁盤文件後,不需要做其他處理,直接用網絡發送出去。試想,如果讀取磁盤的數據需要用程序進一步處理的話,必須要經過第二次和第三次數據copy,讓應用程序在內存緩衝區處理。


爲什麼Kafka這麼快

kafka作爲MQ也好,作爲存儲層也好,無非是兩個重要功能,一是Producer生產的數據存到broker,二是 Consumer從broker讀取數據;我們把它簡化成如下兩個過程:
1、網絡數據持久化到磁盤 (Producer 到 Broker)
2、磁盤文件通過網絡發送(Broker 到 Consumer)

下面,先給出“kafka用了磁盤,還速度快”的結論

1、順序讀寫
磁盤順序讀或寫的速度400M/s,能夠發揮磁盤最大的速度。
隨機讀寫,磁盤速度慢的時候十幾到幾百K/s。這就看出了差距。
kafka將來自Producer的數據,順序追加在partition,partition就是一個文件,以此實現順序寫入。
Consumer從broker讀取數據時,因爲自帶了偏移量,接着上次讀取的位置繼續讀,以此實現順序讀。
順序讀寫,是kafka利用磁盤特性的一個重要體現。

2、零拷貝 sendfile(in,out)
數據直接在內核完成輸入和輸出,不需要拷貝到用戶空間再寫出去。
kafka數據寫入磁盤前,數據先寫到進程的內存空間。

3、mmap文件映射
虛擬映射只支持文件;
在進程 的非堆內存開闢一塊內存空間,和OS內核空間的一塊內存進行映射,
kafka數據寫入、是寫入這塊內存空間,但實際這塊內存和OS內核內存有映射,也就是相當於寫在內核內存空間了,且這塊內核空間、內核直接能夠訪問到,直接落入磁盤。
這裏,我們需要清楚的是:內核緩衝區的數據,flush就能完成落盤。


我們來重點探究 kafka兩個重要過程、以及是如何利用兩個零拷貝技術sendfile和mmap的。

網絡數據持久化到磁盤 (Producer 到 Broker)

傳統方式實現:

data = socket.read()// 讀取網絡數據 
File file = new File() 
file.write(data)// 持久化到磁盤 
file.flush()

先接收生產者發來的消息,再落入磁盤。
實際會經過四次copy,如下圖的四個箭頭。

數據落盤通常都是非實時的,kafka生產者數據持久化也是如此。Kafka的數據並不是實時的寫入硬盤,它充分利用了現代操作系統分頁存儲來利用內存提高I/O效率。

對於kafka來說,Producer生產的數據存到broker,這個過程讀取到socket buffer的網絡數據,其實可以直接在OS內核緩衝區,完成落盤。並沒有必要將socket buffer的網絡數據,讀取到應用進程緩衝區;在這裏應用進程緩衝區其實就是broker,broker收到生產者的數據,就是爲了持久化。

在此特殊場景下:接收來自socket buffer的網絡數據,應用進程不需要中間處理、直接進行持久化時。——可以使用mmap內存文件映射。

Memory Mapped Files

簡稱mmap,簡單描述其作用就是:將磁盤文件映射到內存, 用戶通過修改內存就能修改磁盤文件。
它的工作原理是直接利用操作系統的Page來實現文件到物理內存的直接映射。完成映射之後你對物理內存的操作會被同步到硬盤上(操作系統在適當的時候)。

通過mmap,進程像讀寫硬盤一樣讀寫內存(當然是虛擬機內存),也不必關心內存的大小有虛擬內存爲我們兜底。
使用這種方式可以獲取很大的I/O提升,省去了用戶空間到內核空間複製的開銷。

mmap也有一個很明顯的缺陷——不可靠,寫到mmap中的數據並沒有被真正的寫到硬盤,操作系統會在程序主動調用flush的時候才把數據真正的寫到硬盤。Kafka提供了一個參數——producer.type來控制是不是主動flush;如果Kafka寫入到mmap之後就立即flush然後再返回Producer叫同步(sync);寫入mmap之後立即返回Producer不調用flush叫異步(async)。

Java NIO對文件映射的支持

Java NIO,提供了一個 MappedByteBuffer 類可以用來實現內存映射。
MappedByteBuffer只能通過調用FileChannel的map()取得,再沒有其他方式。
FileChannel.map()是抽象方法,具體實現是在 FileChannelImpl.c 可自行查看JDK源碼,其map0()方法就是調用了Linux內核的mmap的API。

使用 MappedByteBuffer類要注意的是:mmap的文件映射,在full gc時纔會進行釋放。當close時,需要手動清除內存映射文件,可以反射調用sun.misc.Cleaner方法。

磁盤文件通過網絡發送(Broker 到 Consumer)

傳統方式實現:
先讀取磁盤、再用socket發送,實際也是進過四次copy。

buffer = File.read 
Socket.send(buffer)

而 Linux 2.4+ 內核通過 sendfile 系統調用,提供了零拷貝。磁盤數據通過 DMA 拷貝到內核態 Buffer 後,直接通過 DMA 拷貝到 NIC Buffer(socket buffer),無需 CPU 拷貝。這也是零拷貝這一說法的來源。除了減少數據拷貝外,因爲整個讀文件 - 網絡發送由一個 sendfile 調用完成,整個過程只有兩次上下文切換,因此大大提高了性能。零拷貝過程如下圖所示。

相比於文章開始,對傳統IO 4步拷貝的分析,sendfile將第二次、第三次拷貝,一步完成。

其實這項零拷貝技術,直接從內核空間(DMA的)到內核空間(Socket的)、然後發送網卡。
應用的場景非常多,如Tomcat、Nginx、Apache等web服務器返回靜態資源等,將數據用網絡發送出去,都運用了sendfile。
簡單理解 sendfile(in,out)就是,磁盤文件讀取到操作系統內核緩衝區後、直接扔給網卡,發送網絡數據。

Java NIO對sendfile的支持就是FileChannel.transferTo()/transferFrom()。
fileChannel.transferTo( position, count, socketChannel);
把磁盤文件讀取OS內核緩衝區後的fileChannel,直接轉給socketChannel發送;底層就是sendfile。消費者從broker讀取數據,就是由此實現。

具體來看,Kafka 的數據傳輸通過 TransportLayer 來完成,其子類 PlaintextTransportLayer 通過Java NIO 的 FileChannel 的 transferTo 和 transferFrom 方法實現零拷貝。

@Override
public long transferFrom(FileChannel fileChannel, long position, long count) throws IOException {
   return fileChannel.transferTo(position, count, socketChannel);
}

注: transferTo 和 transferFrom 並不保證一定能使用零拷貝。實際上是否能使用零拷貝與操作系統相關,如果操作系統提供 sendfile 這樣的零拷貝系統調用,則這兩個方法會通過這樣的系統調用充分利用零拷貝的優勢,否則並不能通過這兩個方法本身實現零拷貝。


Kafka總結

總的來說Kafka快的原因:
1、partition順序讀寫,充分利用磁盤特性,這是基礎;
2、Producer生產的數據持久化到broker,採用mmap文件映射,實現順序的快速寫入;
3、Customer從broker讀取數據,採用sendfile,將磁盤文件讀到OS內核緩衝區後,直接轉到socket buffer進行網絡發送。

mmap 和 sendfile總結

1、都是Linux內核提供、實現零拷貝的API;
2、sendfile 是將讀到內核空間的數據,轉到socket buffer,進行網絡發送;
3、mmap將磁盤文件映射到內存,支持讀和寫,對內存的操作會反映在磁盤文件上。
RocketMQ 在消費消息時,使用了 mmap。kafka 使用了 sendFile。

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