Kafka零拷貝

Kafka除了具備消息隊(duì)列MQ的特性和使用場(chǎng)景外,它還有一個(gè)重要用途,就是做存儲(chǔ)層。

用kafka做存儲(chǔ)層,為什么呢?一大堆可以做數(shù)據(jù)存儲(chǔ)的 MySQL、MongoDB、HDFS……

因?yàn)閗afka數(shù)據(jù)是持久化磁盤(pán)的,還速度快;還可靠、支持分布式……

啥!用了磁盤(pán),還速度快?。?!

沒(méi)錯(cuò),kafka就是速度無(wú)敵,本文將探究kafka無(wú)敵性能背后的秘密。

首先要有個(gè)概念,kafka高性能的背后,是多方面協(xié)同后、最終的結(jié)果,kafka從宏觀架構(gòu)、分布式partition存儲(chǔ)、ISR數(shù)據(jù)同步、以及“無(wú)孔不入”的高效利用磁盤(pán)/操作系統(tǒng)特性,這些多方面的協(xié)同,是kafka成為性能之王的必然結(jié)果。

本文將從kafka零拷貝,探究其是如何“無(wú)孔不入”的高效利用磁盤(pán)/操作系統(tǒng)特性的。


先說(shuō)說(shuō)零拷貝

零拷貝并不是不需要拷貝,而是減少不必要的拷貝次數(shù)。通常是說(shuō)在IO讀寫(xiě)過(guò)程中。

實(shí)際上,零拷貝是有廣義和狹義之分,目前我們通常聽(tīng)到的零拷貝,包括上面這個(gè)定義減少不必要的拷貝次數(shù)都是廣義上的零拷貝。其實(shí)了解到這點(diǎn)就足夠了。

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

聊聊傳統(tǒng)IO流程

比如:讀取文件,再用socket發(fā)送出去
傳統(tǒng)方式實(shí)現(xiàn):
先讀取、再發(fā)送,實(shí)際經(jīng)過(guò)1~4四次copy。

buffer = File.read 
Socket.send(buffer)

1、第一次:將磁盤(pán)文件,讀取到操作系統(tǒng)內(nèi)核緩沖區(qū);
2、第二次:將內(nèi)核緩沖區(qū)的數(shù)據(jù),copy到application應(yīng)用程序的buffer;
3、第三步:將application應(yīng)用程序buffer中的數(shù)據(jù),copy到socket網(wǎng)絡(luò)發(fā)送緩沖區(qū)(屬于操作系統(tǒng)內(nèi)核的緩沖區(qū));
4、第四次:將socket buffer的數(shù)據(jù),copy到網(wǎng)卡,由網(wǎng)卡進(jìn)行網(wǎng)絡(luò)傳輸。

傳統(tǒng)方式,讀取磁盤(pán)文件并進(jìn)行網(wǎng)絡(luò)發(fā)送,經(jīng)過(guò)的四次數(shù)據(jù)copy是非常繁瑣的。實(shí)際IO讀寫(xiě),需要進(jìn)行IO中斷,需要CPU響應(yīng)中斷(帶來(lái)上下文切換),盡管后來(lái)引入DMA來(lái)接管CPU的中斷請(qǐng)求,但四次copy是存在“不必要的拷貝”的。

重新思考傳統(tǒng)IO方式,會(huì)注意到實(shí)際上并不需要第二個(gè)和第三個(gè)數(shù)據(jù)副本。應(yīng)用程序除了緩存數(shù)據(jù)并將其傳輸回套接字緩沖區(qū)之外什么都不做。相反,數(shù)據(jù)可以直接從讀緩沖區(qū)傳輸?shù)教捉幼志彌_區(qū)。

顯然,第二次和第三次數(shù)據(jù)copy 其實(shí)在這種場(chǎng)景下沒(méi)有什么幫助反而帶來(lái)開(kāi)銷(xiāo),這也正是零拷貝出現(xiàn)的意義。

這種場(chǎng)景:是指讀取磁盤(pán)文件后,不需要做其他處理,直接用網(wǎng)絡(luò)發(fā)送出去。試想,如果讀取磁盤(pán)的數(shù)據(jù)需要用程序進(jìn)一步處理的話,必須要經(jīng)過(guò)第二次和第三次數(shù)據(jù)copy,讓?xiě)?yīng)用程序在內(nèi)存緩沖區(qū)處理。


為什么Kafka這么快

kafka作為MQ也好,作為存儲(chǔ)層也好,無(wú)非是兩個(gè)重要功能,一是Producer生產(chǎn)的數(shù)據(jù)存到broker,二是 Consumer從broker讀取數(shù)據(jù);我們把它簡(jiǎn)化成如下兩個(gè)過(guò)程:
1、網(wǎng)絡(luò)數(shù)據(jù)持久化到磁盤(pán) (Producer 到 Broker)
2、磁盤(pán)文件通過(guò)網(wǎng)絡(luò)發(fā)送(Broker 到 Consumer)

下面,先給出“kafka用了磁盤(pán),還速度快”的結(jié)論

1、順序讀寫(xiě)
磁盤(pán)順序讀或?qū)懙乃俣?00M/s,能夠發(fā)揮磁盤(pán)最大的速度。
隨機(jī)讀寫(xiě),磁盤(pán)速度慢的時(shí)候十幾到幾百K/s。這就看出了差距。
kafka將來(lái)自Producer的數(shù)據(jù),順序追加在partition,partition就是一個(gè)文件,以此實(shí)現(xiàn)順序?qū)懭搿?br> Consumer從broker讀取數(shù)據(jù)時(shí),因?yàn)樽詭Я似屏?,接著上次讀取的位置繼續(xù)讀,以此實(shí)現(xiàn)順序讀。
順序讀寫(xiě),是kafka利用磁盤(pán)特性的一個(gè)重要體現(xiàn)。

2、零拷貝 sendfile(in,out)
數(shù)據(jù)直接在內(nèi)核完成輸入和輸出,不需要拷貝到用戶(hù)空間再寫(xiě)出去。
kafka數(shù)據(jù)寫(xiě)入磁盤(pán)前,數(shù)據(jù)先寫(xiě)到進(jìn)程的內(nèi)存空間。

3、mmap文件映射
虛擬映射只支持文件;
在進(jìn)程 的非堆內(nèi)存開(kāi)辟一塊內(nèi)存空間,和OS內(nèi)核空間的一塊內(nèi)存進(jìn)行映射,
kafka數(shù)據(jù)寫(xiě)入、是寫(xiě)入這塊內(nèi)存空間,但實(shí)際這塊內(nèi)存和OS內(nèi)核內(nèi)存有映射,也就是相當(dāng)于寫(xiě)在內(nèi)核內(nèi)存空間了,且這塊內(nèi)核空間、內(nèi)核直接能夠訪問(wèn)到,直接落入磁盤(pán)。
這里,我們需要清楚的是:內(nèi)核緩沖區(qū)的數(shù)據(jù),flush就能完成落盤(pán)。


我們來(lái)重點(diǎn)探究 kafka兩個(gè)重要過(guò)程、以及是如何利用兩個(gè)零拷貝技術(shù)sendfile和mmap的。

網(wǎng)絡(luò)數(shù)據(jù)持久化到磁盤(pán) (Producer 到 Broker)

傳統(tǒng)方式實(shí)現(xiàn):

data = socket.read()// 讀取網(wǎng)絡(luò)數(shù)據(jù) 
File file = new File() 
file.write(data)// 持久化到磁盤(pán) 
file.flush()

先接收生產(chǎn)者發(fā)來(lái)的消息,再落入磁盤(pán)。
實(shí)際會(huì)經(jīng)過(guò)四次copy,如下圖的四個(gè)箭頭。


數(shù)據(jù)落盤(pán)通常都是非實(shí)時(shí)的,kafka生產(chǎn)者數(shù)據(jù)持久化也是如此。Kafka的數(shù)據(jù)并不是實(shí)時(shí)的寫(xiě)入硬盤(pán),它充分利用了現(xiàn)代操作系統(tǒng)分頁(yè)存儲(chǔ)來(lái)利用內(nèi)存提高I/O效率。

對(duì)于kafka來(lái)說(shuō),Producer生產(chǎn)的數(shù)據(jù)存到broker,這個(gè)過(guò)程讀取到socket buffer的網(wǎng)絡(luò)數(shù)據(jù),其實(shí)可以直接在OS內(nèi)核緩沖區(qū),完成落盤(pán)。并沒(méi)有必要將socket buffer的網(wǎng)絡(luò)數(shù)據(jù),讀取到應(yīng)用進(jìn)程緩沖區(qū);在這里應(yīng)用進(jìn)程緩沖區(qū)其實(shí)就是broker,broker收到生產(chǎn)者的數(shù)據(jù),就是為了持久化。

在此特殊場(chǎng)景下:接收來(lái)自socket buffer的網(wǎng)絡(luò)數(shù)據(jù),應(yīng)用進(jìn)程不需要中間處理、直接進(jìn)行持久化時(shí)。——可以使用mmap內(nèi)存文件映射。

Memory Mapped Files

簡(jiǎn)稱(chēng)mmap,簡(jiǎn)單描述其作用就是:將磁盤(pán)文件映射到內(nèi)存, 用戶(hù)通過(guò)修改內(nèi)存就能修改磁盤(pán)文件。
它的工作原理是直接利用操作系統(tǒng)的Page來(lái)實(shí)現(xiàn)文件到物理內(nèi)存的直接映射。完成映射之后你對(duì)物理內(nèi)存的操作會(huì)被同步到硬盤(pán)上(操作系統(tǒng)在適當(dāng)?shù)臅r(shí)候)。

通過(guò)mmap,進(jìn)程像讀寫(xiě)硬盤(pán)一樣讀寫(xiě)內(nèi)存(當(dāng)然是虛擬機(jī)內(nèi)存),也不必關(guān)心內(nèi)存的大小有虛擬內(nèi)存為我們兜底。
使用這種方式可以獲取很大的I/O提升,省去了用戶(hù)空間到內(nèi)核空間復(fù)制的開(kāi)銷(xiāo)。

mmap也有一個(gè)很明顯的缺陷——不可靠,寫(xiě)到mmap中的數(shù)據(jù)并沒(méi)有被真正的寫(xiě)到硬盤(pán),操作系統(tǒng)會(huì)在程序主動(dòng)調(diào)用flush的時(shí)候才把數(shù)據(jù)真正的寫(xiě)到硬盤(pán)。Kafka提供了一個(gè)參數(shù)——producer.type來(lái)控制是不是主動(dòng)flush;如果Kafka寫(xiě)入到mmap之后就立即flush然后再返回Producer叫同步(sync);寫(xiě)入mmap之后立即返回Producer不調(diào)用flush叫異步(async)。

Java NIO對(duì)文件映射的支持

Java NIO,提供了一個(gè) MappedByteBuffer 類(lèi)可以用來(lái)實(shí)現(xiàn)內(nèi)存映射。
MappedByteBuffer只能通過(guò)調(diào)用FileChannel的map()取得,再?zèng)]有其他方式。
FileChannel.map()是抽象方法,具體實(shí)現(xiàn)是在 FileChannelImpl.c 可自行查看JDK源碼,其map0()方法就是調(diào)用了Linux內(nèi)核的mmap的API。

使用 MappedByteBuffer類(lèi)要注意的是:mmap的文件映射,在full gc時(shí)才會(huì)進(jìn)行釋放。當(dāng)close時(shí),需要手動(dòng)清除內(nèi)存映射文件,可以反射調(diào)用sun.misc.Cleaner方法。

磁盤(pán)文件通過(guò)網(wǎng)絡(luò)發(fā)送(Broker 到 Consumer)

傳統(tǒng)方式實(shí)現(xiàn):
先讀取磁盤(pán)、再用socket發(fā)送,實(shí)際也是進(jìn)過(guò)四次copy。

buffer = File.read 
Socket.send(buffer)

而 Linux 2.4+ 內(nèi)核通過(guò) sendfile 系統(tǒng)調(diào)用,提供了零拷貝。磁盤(pán)數(shù)據(jù)通過(guò) DMA 拷貝到內(nèi)核態(tài) Buffer 后,直接通過(guò) DMA 拷貝到 NIC Buffer(socket buffer),無(wú)需 CPU 拷貝。這也是零拷貝這一說(shuō)法的來(lái)源。除了減少數(shù)據(jù)拷貝外,因?yàn)檎麄€(gè)讀文件 - 網(wǎng)絡(luò)發(fā)送由一個(gè) sendfile 調(diào)用完成,整個(gè)過(guò)程只有兩次上下文切換,因此大大提高了性能。零拷貝過(guò)程如下圖所示。

相比于文章開(kāi)始,對(duì)傳統(tǒng)IO 4步拷貝的分析,sendfile將第二次、第三次拷貝,一步完成。

其實(shí)這項(xiàng)零拷貝技術(shù),直接從內(nèi)核空間(DMA的)到內(nèi)核空間(Socket的)、然后發(fā)送網(wǎng)卡。
應(yīng)用的場(chǎng)景非常多,如Tomcat、Nginx、Apache等web服務(wù)器返回靜態(tài)資源等,將數(shù)據(jù)用網(wǎng)絡(luò)發(fā)送出去,都運(yùn)用了sendfile。
簡(jiǎn)單理解 sendfile(in,out)就是,磁盤(pán)文件讀取到操作系統(tǒng)內(nèi)核緩沖區(qū)后、直接扔給網(wǎng)卡,發(fā)送網(wǎng)絡(luò)數(shù)據(jù)。

Java NIO對(duì)sendfile的支持就是FileChannel.transferTo()/transferFrom()。
fileChannel.transferTo( position, count, socketChannel);
把磁盤(pán)文件讀取OS內(nèi)核緩沖區(qū)后的fileChannel,直接轉(zhuǎn)給socketChannel發(fā)送;底層就是sendfile。消費(fèi)者從broker讀取數(shù)據(jù),就是由此實(shí)現(xiàn)。

具體來(lái)看,Kafka 的數(shù)據(jù)傳輸通過(guò) TransportLayer 來(lái)完成,其子類(lèi) PlaintextTransportLayer 通過(guò)Java NIO 的 FileChannel 的 transferTo 和 transferFrom 方法實(shí)現(xiàn)零拷貝。

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

注: transferTo 和 transferFrom 并不保證一定能使用零拷貝。實(shí)際上是否能使用零拷貝與操作系統(tǒng)相關(guān),如果操作系統(tǒng)提供 sendfile 這樣的零拷貝系統(tǒng)調(diào)用,則這兩個(gè)方法會(huì)通過(guò)這樣的系統(tǒng)調(diào)用充分利用零拷貝的優(yōu)勢(shì),否則并不能通過(guò)這兩個(gè)方法本身實(shí)現(xiàn)零拷貝。


Kafka總結(jié)

總的來(lái)說(shuō)Kafka快的原因:
1、partition順序讀寫(xiě),充分利用磁盤(pán)特性,這是基礎(chǔ);
2、Producer生產(chǎn)的數(shù)據(jù)持久化到broker,采用mmap文件映射,實(shí)現(xiàn)順序的快速寫(xiě)入;
3、Customer從broker讀取數(shù)據(jù),采用sendfile,將磁盤(pán)文件讀到OS內(nèi)核緩沖區(qū)后,直接轉(zhuǎn)到socket buffer進(jìn)行網(wǎng)絡(luò)發(fā)送。

mmap 和 sendfile總結(jié)

1、都是Linux內(nèi)核提供、實(shí)現(xiàn)零拷貝的API;
2、sendfile 是將讀到內(nèi)核空間的數(shù)據(jù),轉(zhuǎn)到socket buffer,進(jìn)行網(wǎng)絡(luò)發(fā)送;
3、mmap將磁盤(pán)文件映射到內(nèi)存,支持讀和寫(xiě),對(duì)內(nèi)存的操作會(huì)反映在磁盤(pán)文件上。
RocketMQ 在消費(fèi)消息時(shí),使用了 mmap。kafka 使用了 sendFile。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容