KAFKA推送消息用到了sendfile碰酝,落盤技術(shù)用到了mmap霎匈,DMA貫穿其中。
先說說零拷貝
零拷貝并不是不需要拷貝砰粹,而是減少不必要的拷貝次數(shù)唧躲。通常是說在IO讀寫過程中。
實(shí)際上碱璃,零拷貝是有廣義和狹義之分弄痹,目前我們通常聽到的零拷貝,包括上面這個(gè)定義減少不必要的拷貝次數(shù)都是廣義上的零拷貝嵌器。其實(shí)了解到這點(diǎn)就足夠了肛真。
我們知道,減少不必要的拷貝次數(shù)爽航,就是為了提高效率蚓让。那零拷貝之前,是怎樣的呢讥珍?
聊聊傳統(tǒng)IO流程
比如:讀取文件历极,再用socket發(fā)送出去
傳統(tǒng)方式實(shí)現(xiàn):
先讀取、再發(fā)送衷佃,實(shí)際經(jīng)過1~4四次copy趟卸。
1、第一次:將磁盤文件氏义,讀取到操作系統(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)方式,讀取磁盤文件并進(jìn)行網(wǎng)絡(luò)發(fā)送规惰,經(jīng)過的四次數(shù)據(jù)copy是非常繁瑣的。實(shí)際IO讀寫泉蝌,需要進(jìn)行IO中斷歇万,需要CPU響應(yīng)中斷(帶來上下文切換),盡管后來引入DMA來接管CPU的中斷請(qǐng)求勋陪,但四次copy是存在“不必要的拷貝”的贪磺。(什么是DMA?
其實(shí)DMA技術(shù)很容易理解诅愚,本質(zhì)上寒锚,DMA技術(shù)就是我們在主板上放?塊獨(dú)立的芯片。在進(jìn)行內(nèi)存和I/O設(shè)備的數(shù)據(jù)傳輸?shù)臅r(shí)候违孝,我們不再通過CPU來控制數(shù)據(jù)傳輸,而直接通過 DMA控制器(DMA?Controller,簡稱DMAC)川无。這塊芯片务荆,我們可以認(rèn)為它其實(shí)就是一個(gè)協(xié)處理器(Co-Processor))
重新思考傳統(tǒng)IO方式,會(huì)注意到實(shí)際上并不需要第二個(gè)和第三個(gè)數(shù)據(jù)副本校坑。應(yīng)用程序除了緩存數(shù)據(jù)并將其傳輸回套接字緩沖區(qū)之外什么都不做拣技。相反,數(shù)據(jù)可以直接從讀緩沖區(qū)傳輸?shù)教捉幼志彌_區(qū)耍目。
顯然膏斤,第二次和第三次數(shù)據(jù)copy 其實(shí)在這種場景下沒有什么幫助反而帶來開銷,這也正是零拷貝出現(xiàn)的意義邪驮。
這種場景:是指讀取磁盤文件后莫辨,不需要做其他處理,直接用網(wǎng)絡(luò)發(fā)送出去耕捞。試想衔掸,如果讀取磁盤的數(shù)據(jù)需要用程序進(jìn)一步處理的話,必須要經(jīng)過第二次和第三次數(shù)據(jù)copy俺抽,讓應(yīng)用程序在內(nèi)存緩沖區(qū)處理敞映。
為什么Kafka這么快
kafka作為MQ也好,作為存儲(chǔ)層也好磷斧,無非是兩個(gè)重要功能振愿,一是Producer生產(chǎn)的數(shù)據(jù)存到broker捷犹,二是 Consumer從broker讀取數(shù)據(jù);我們把它簡化成如下兩個(gè)過程:
1冕末、網(wǎng)絡(luò)數(shù)據(jù)持久化到磁盤 (Producer 到 Broker)
2萍歉、磁盤文件通過網(wǎng)絡(luò)發(fā)送(Broker 到 Consumer)
下面,先給出“kafka用了磁盤档桃,還速度快”的結(jié)論
1枪孩、順序讀寫
磁盤順序讀或?qū)懙乃俣?00M/s,能夠發(fā)揮磁盤最大的速度藻肄。
隨機(jī)讀寫蔑舞,磁盤速度慢的時(shí)候十幾到幾百K/s。這就看出了差距嘹屯。
kafka將來自Producer的數(shù)據(jù)攻询,順序追加在partition,partition就是一個(gè)文件州弟,以此實(shí)現(xiàn)順序?qū)懭搿?/p>
Consumer從broker讀取數(shù)據(jù)時(shí)钧栖,因?yàn)樽詭Я似屏浚又洗巫x取的位置繼續(xù)讀婆翔,以此實(shí)現(xiàn)順序讀拯杠。
順序讀寫,是kafka利用磁盤特性的一個(gè)重要體現(xiàn)浙滤。
2阴挣、零拷貝 sendfile(in,out)
數(shù)據(jù)直接在內(nèi)核完成輸入和輸出,不需要拷貝到用戶空間再寫出去纺腊。
kafka數(shù)據(jù)寫入磁盤前畔咧,數(shù)據(jù)先寫到進(jìn)程的內(nèi)存空間。
3揖膜、mmap文件映射
虛擬映射只支持文件誓沸;
在進(jìn)程 的非堆內(nèi)存開辟一塊內(nèi)存空間,和OS內(nèi)核空間的一塊內(nèi)存進(jìn)行映射壹粟,
kafka數(shù)據(jù)寫入拜隧、是寫入這塊內(nèi)存空間,但實(shí)際這塊內(nèi)存和OS內(nèi)核內(nèi)存有映射趁仙,也就是相當(dāng)于寫在內(nèi)核內(nèi)存空間了洪添,且這塊內(nèi)核空間、內(nèi)核直接能夠訪問到雀费,直接落入磁盤干奢。
這里,我們需要清楚的是:內(nèi)核緩沖區(qū)的數(shù)據(jù)盏袄,flush就能完成落盤忿峻。
我們來重點(diǎn)探究 kafka兩個(gè)重要過程薄啥、以及是如何利用兩個(gè)零拷貝技術(shù)sendfile和mmap的。
網(wǎng)絡(luò)數(shù)據(jù)持久化到磁盤 (Producer 到 Broker)
傳統(tǒng)方式實(shí)現(xiàn):
先接收生產(chǎn)者發(fā)來的消息逛尚,再落入磁盤垄惧。
數(shù)據(jù)落盤通常都是非實(shí)時(shí)的,kafka生產(chǎn)者數(shù)據(jù)持久化也是如此绰寞。Kafka的數(shù)據(jù)并不是實(shí)時(shí)的寫入硬盤到逊,它充分利用了現(xiàn)代操作系統(tǒng)分頁存儲(chǔ)來利用內(nèi)存提高I/O效率。
對(duì)于kafka來說滤钱,Producer生產(chǎn)的數(shù)據(jù)存到broker蕾管,這個(gè)過程讀取到socket buffer的網(wǎng)絡(luò)數(shù)據(jù),其實(shí)可以直接在OS內(nèi)核緩沖區(qū)菩暗,完成落盤。并沒有必要將socket buffer的網(wǎng)絡(luò)數(shù)據(jù)旭蠕,讀取到應(yīng)用進(jìn)程緩沖區(qū)停团;在這里應(yīng)用進(jìn)程緩沖區(qū)其實(shí)就是broker,broker收到生產(chǎn)者的數(shù)據(jù)掏熬,就是為了持久化佑稠。
在此特殊場景下:接收來自socket buffer的網(wǎng)絡(luò)數(shù)據(jù),應(yīng)用進(jìn)程不需要中間處理旗芬、直接進(jìn)行持久化時(shí)舌胶。——可以使用mmap內(nèi)存文件映射疮丛。
Memory Mapped Files
簡稱mmap幔嫂,簡單描述其作用就是:將磁盤文件映射到內(nèi)存, 用戶通過修改內(nèi)存就能修改磁盤文件。
它的工作原理是直接利用操作系統(tǒng)的Page來實(shí)現(xiàn)文件到物理內(nèi)存的直接映射誊薄。完成映射之后你對(duì)物理內(nèi)存的操作會(huì)被同步到硬盤上(操作系統(tǒng)在適當(dāng)?shù)臅r(shí)候)履恩。
通過mmap,進(jìn)程像讀寫硬盤一樣讀寫內(nèi)存(當(dāng)然是虛擬機(jī)內(nèi)存)呢蔫,也不必關(guān)心內(nèi)存的大小有虛擬內(nèi)存為我們兜底切心。
使用這種方式可以獲取很大的I/O提升,省去了用戶空間到內(nèi)核空間復(fù)制的開銷片吊。
mmap也有一個(gè)很明顯的缺陷——不可靠绽昏,寫到mmap中的數(shù)據(jù)并沒有被真正的寫到硬盤,操作系統(tǒng)會(huì)在程序主動(dòng)調(diào)用flush的時(shí)候才把數(shù)據(jù)真正的寫到硬盤俏脊。Kafka提供了一個(gè)參數(shù)——producer.type來控制是不是主動(dòng)flush全谤;如果Kafka寫入到mmap之后就立即flush然后再返回Producer叫同步(sync);寫入mmap之后立即返回Producer不調(diào)用flush叫異步(async)联予。
Java NIO對(duì)文件映射的支持
Java NIO啼县,提供了一個(gè) MappedByteBuffer 類可以用來實(shí)現(xiàn)內(nèi)存映射材原。
MappedByteBuffer只能通過調(diào)用FileChannel的map()取得,再?zèng)]有其他方式季眷。
FileChannel.map()是抽象方法余蟹,具體實(shí)現(xiàn)是在 FileChannelImpl.c 可自行查看JDK源碼,其map0()方法就是調(diào)用了Linux內(nèi)核的mmap的API子刮。
使用 MappedByteBuffer類要注意的是:mmap的文件映射威酒,在full gc時(shí)才會(huì)進(jìn)行釋放。當(dāng)close時(shí)挺峡,需要手動(dòng)清除內(nèi)存映射文件葵孤,可以反射調(diào)用sun.misc.Cleaner方法。
磁盤文件通過網(wǎng)絡(luò)發(fā)送(Broker 到 Consumer)
傳統(tǒng)方式實(shí)現(xiàn):
先讀取磁盤橱赠、再用socket發(fā)送尤仍,實(shí)際也是進(jìn)過四次copy。
而 Linux 2.4+ 內(nèi)核通過 sendfile 系統(tǒng)調(diào)用狭姨,提供了零拷貝宰啦。磁盤數(shù)據(jù)通過 DMA 拷貝到內(nèi)核態(tài) Buffer 后,直接通過 DMA 拷貝到 NIC Buffer(socket buffer)饼拍,無需 CPU 拷貝赡模。這也是零拷貝這一說法的來源。除了減少數(shù)據(jù)拷貝外师抄,因?yàn)檎麄€(gè)讀文件 - 網(wǎng)絡(luò)發(fā)送由一個(gè) sendfile 調(diào)用完成漓柑,整個(gè)過程只有兩次上下文切換,因此大大提高了性能叨吮。零拷貝過程如下圖所示辆布。
相比于文章開始,對(duì)傳統(tǒng)IO 4步拷貝的分析茶鉴,sendfile將第二次谚殊、第三次拷貝,一步完成蛤铜。
其實(shí)這項(xiàng)零拷貝技術(shù)嫩絮,直接從內(nèi)核空間(DMA的)到內(nèi)核空間(Socket的)、然后發(fā)送網(wǎng)卡围肥。
應(yīng)用的場景非常多剿干,如Tomcat、Nginx穆刻、Apache等web服務(wù)器返回靜態(tài)資源等置尔,將數(shù)據(jù)用網(wǎng)絡(luò)發(fā)送出去,都運(yùn)用了sendfile氢伟。
簡單理解 sendfile(in,out)就是榜轿,磁盤文件讀取到操作系統(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);
把磁盤文件讀取OS內(nèi)核緩沖區(qū)后的fileChannel,直接轉(zhuǎn)給socketChannel發(fā)送飞傀;底層就是sendfile皇型。消費(fèi)者從broker讀取數(shù)據(jù),就是由此實(shí)現(xiàn)砸烦。
具體來看弃鸦,Kafka 的數(shù)據(jù)傳輸通過 TransportLayer 來完成,其子類 PlaintextTransportLayer 通過Java NIO 的 FileChannel 的 transferTo 和 transferFrom 方法實(shí)現(xiàn)零拷貝幢痘。
注: transferTo 和 transferFrom 并不保證一定能使用零拷貝唬格。實(shí)際上是否能使用零拷貝與操作系統(tǒng)相關(guān),如果操作系統(tǒng)提供 sendfile 這樣的零拷貝系統(tǒng)調(diào)用颜说,則這兩個(gè)方法會(huì)通過這樣的系統(tǒng)調(diào)用充分利用零拷貝的優(yōu)勢西轩,否則并不能通過這兩個(gè)方法本身實(shí)現(xiàn)零拷貝。
Kafka總結(jié)
總的來說Kafka快的原因:
1脑沿、partition順序讀寫,充分利用磁盤特性马僻,這是基礎(chǔ)庄拇;
2、Producer生產(chǎn)的數(shù)據(jù)持久化到broker韭邓,采用mmap文件映射措近,實(shí)現(xiàn)順序的快速寫入;
3女淑、Customer從broker讀取數(shù)據(jù)瞭郑,采用sendfile,將磁盤文件讀到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將磁盤文件映射到內(nèi)存焰盗,支持讀和寫璧尸,對(duì)內(nèi)存的操作會(huì)反映在磁盤文件上。
RocketMQ 在消費(fèi)消息時(shí)熬拒,使用了 mmap爷光。kafka 使用了 sendFile。
關(guān)于DMA
為什么那么快梦湘?一起來看Kafka的實(shí)現(xiàn)原理
1瞎颗、它究竟是怎么利用DMA的?
Kafka是一個(gè)用來處理實(shí)時(shí)數(shù)據(jù)的管道捌议,我們常常用它來做一個(gè)消息隊(duì)列哼拔,或者用來收集和落地海量的日志。作為一個(gè)處理實(shí)時(shí)數(shù)據(jù)和日志的管道瓣颅,瓶頸自然也在I/O層面倦逐。
2、Kafka里面兩種常用的海量數(shù)據(jù)傳輸?shù)那闆r是什么宫补?
Kafka里面會(huì)有兩種常用的海量數(shù)據(jù)傳輸?shù)那闆r檬姥。一種是從網(wǎng)絡(luò)絡(luò)中接收上游的數(shù)據(jù),然后需要落地到本地的磁盤上粉怕,確保數(shù)據(jù)不丟失健民。
另一種情況呢,則是從本地磁盤上讀取出來贫贝,通過網(wǎng)絡(luò)發(fā)送出去秉犹。
我們來看一看后一種情況,從磁盤讀數(shù)據(jù)發(fā)送到網(wǎng)絡(luò)上去稚晚。如果我們自己寫一個(gè)簡單的程序崇堵,最直觀的辦法,自然是用個(gè)一件讀操作客燕,從磁盤上把數(shù)據(jù)讀到內(nèi)存里面來鸳劳,
然后再用個(gè)Socket,把這些數(shù)據(jù)發(fā)送到網(wǎng)絡(luò)上去也搓。
3赏廓、我們只是要“搬運(yùn)”一份數(shù)據(jù),結(jié)果卻整整搬運(yùn)了四次
在這個(gè)過程中傍妒,數(shù)據(jù)一共發(fā)生了四次傳輸?shù)倪^程楚昭。其中兩次是DMA的傳輸,另外兩次拍顷,則是通過CPU控制的傳輸抚太。下面我們來具體看看這個(gè)過程。
第一次傳輸,是從硬盤上尿贫,讀到操作系統(tǒng)內(nèi)核的緩沖區(qū)里电媳。這個(gè)傳輸是通過DMA搬運(yùn)的。
第二次傳輸庆亡,需要從內(nèi)核緩沖區(qū)里面的數(shù)據(jù)匾乓,復(fù)制到我們應(yīng)用分配的內(nèi)存里面。這個(gè)傳輸是通過CPU搬運(yùn)的又谋。
第三次傳輸拼缝,要從我們應(yīng)用的內(nèi)存里面,再寫到操作系統(tǒng)的Socket的緩沖區(qū)里面去彰亥。這個(gè)傳輸咧七,還是由CPU搬運(yùn)的。
最后一次傳輸任斋,需要再從Socket的緩沖區(qū)里面继阻,寫到網(wǎng)卡的緩沖區(qū)里面去。這個(gè)傳輸又是通過DMA搬運(yùn)的废酷。
這個(gè)時(shí)候瘟檩,你可以回過頭看看這個(gè)過程。我們只是要“搬運(yùn)”?份數(shù)據(jù)澈蟆,結(jié)果卻整整搬運(yùn)了四次墨辛。而且這里面,從內(nèi)核的讀緩沖區(qū)傳輸?shù)綉?yīng)用的內(nèi)存里趴俘,
再從應(yīng)用的內(nèi)存里傳輸?shù)絊ocket的緩沖區(qū)里睹簇,其實(shí)都是把同一份數(shù)據(jù)在內(nèi)存里面搬運(yùn)來搬運(yùn)去,特別沒有效率哮幢。
4、我們就需要盡可能地減少數(shù)據(jù)搬運(yùn)的需求
像Kafka這樣的應(yīng)用場景志珍,其實(shí)一部分最終利用到的硬件資源橙垢,其實(shí)又都是在干這個(gè)搬運(yùn)數(shù)據(jù)的事兒。所以伦糯,我們就需要盡可能地減少數(shù)據(jù)搬運(yùn)的需求柜某。
事實(shí)上,Kafka做的事情就是敛纲,把這個(gè)數(shù)據(jù)搬運(yùn)的次數(shù)喂击,從上面的四次,變成了兩次淤翔,并且只有DMA來進(jìn)行數(shù)據(jù)搬運(yùn)翰绊,而不需要CPU。
Kafka的代碼調(diào)用了Java NIO庫,具體是FileChannel里面的transferTo方法监嗜。我們的數(shù)據(jù)并沒有讀到中間的應(yīng)用內(nèi)存里面谐檀,而是直接通過Channel,寫入到對(duì)應(yīng)的網(wǎng)絡(luò)設(shè)備里裁奇。
并且桐猬,對(duì)于Socket的操作,也不是寫入到Socket的Buffer里面刽肠,而是直接根據(jù)描述符(Descriptor)寫到到網(wǎng)卡的緩沖區(qū)里面溃肪。于是,在這個(gè)過程之中音五,我們只進(jìn)行了兩次數(shù)據(jù)傳輸惫撰。
5、同一份數(shù)據(jù)傳輸?shù)拇螖?shù)從四次變成了兩次
第一次放仗,是通過DMA润绎,從硬盤直接讀到操作系統(tǒng)內(nèi)核的讀緩沖區(qū)里面。第二次诞挨,則是根據(jù)Socket的描述符信息莉撇,直接從讀緩沖區(qū)里面,寫入到網(wǎng)卡的緩沖區(qū)里面惶傻。
這樣棍郎,我們同一份數(shù)據(jù)傳輸?shù)拇螖?shù)從四次變成了兩次,并且沒有通過CPU來進(jìn)行數(shù)據(jù)搬運(yùn)银室,所有的數(shù)據(jù)都是通過DMA來進(jìn)行傳輸?shù)摹?/p>
6涂佃、什么是零拷貝?
在這個(gè)方法里面蜈敢,我們沒有在內(nèi)存層面去“復(fù)制(Copy)”數(shù)據(jù)辜荠,所以這個(gè)方法,也被稱之為零拷貝(Zero-Copy)抓狭。IBM Developer Works里面有一篇文章伯病,專們寫過程序來測試過在同樣的硬件下,使用零拷貝能夠帶來的性能提升否过。我在這里放上這篇文章鏈接午笛。在這篇文章最后,你可以看到苗桂,無論傳輸數(shù)據(jù)量的大小药磺,傳輸同樣的數(shù)據(jù),使用了零拷貝能夠縮短65%的時(shí)間煤伟,大幅度提升了機(jī)器傳輸數(shù)據(jù)的吞吐量癌佩。想要深入了解零拷貝木缝,建議你可以仔細(xì)讀讀讀這篇文章。
DMA總結(jié)
講到這里驼卖,相信你對(duì)DMA的原理氨肌、作用和效果都有所理解了。那么酌畜,我們?起來回顧總結(jié)一下怎囚。、
如果我們始終讓CPU來進(jìn)行各種數(shù)據(jù)傳輸工作桥胞,會(huì)特別浪費(fèi)恳守。一方面,我們的數(shù)據(jù)傳輸工作用不到多少CPU核新的“計(jì)算”功能贩虾。另一方面催烘,CPU的運(yùn)轉(zhuǎn)速度也比I/O操作要快很多。
所以缎罢,我們希望能夠給CPU“減負(fù)”伊群。
于是,工程師們就在主板上放上了DMAC這樣一個(gè)協(xié)處理器芯片策精。通過這個(gè)芯片舰始,CPU只需要告訴DMAC,我們要傳輸什么數(shù)據(jù)咽袜,從哪里來丸卷,到哪里去,就可以放心離開了询刹。
后續(xù)的實(shí)際數(shù)據(jù)傳輸工作谜嫉,都會(huì)有DMAC來完成。隨著現(xiàn)代計(jì)算機(jī)各種外設(shè)硬件越來越多凹联,光一個(gè)通用的DMAC芯片不夠了沐兰,我們在各個(gè)外設(shè)上都加上了DMAC芯片,
使得CPU很少再需要關(guān)注數(shù)據(jù)傳輸?shù)墓ぷ髁恕?/p>
在我們實(shí)際的系統(tǒng)開發(fā)過程中蔽挠,利用好DMA的數(shù)據(jù)傳輸機(jī)制住闯,也可以大幅提升I/O的吞吐率。最典型的例子就是Kafka象泵。
傳統(tǒng)地從硬盤讀取數(shù)據(jù)寞秃,然后再通過網(wǎng)卡上向外發(fā)送斟叼,我們需要進(jìn)行四次數(shù)據(jù)傳輸偶惠,其中有兩次是發(fā)生在內(nèi)存里的緩沖區(qū)和對(duì)應(yīng)的硬件設(shè)備之間,我們沒法節(jié)省掉朗涩。
但是還有兩次忽孽,完全是通過CPU在內(nèi)存里面進(jìn)行數(shù)據(jù)復(fù)制。
在Kafka里,通過Java的NIO里面FileChannel的transferTo方法調(diào)用兄一,我們可以不用把數(shù)據(jù)復(fù)制到我們應(yīng)用程序的內(nèi)存里面厘线。通過DMA的方式,
我們可以把數(shù)據(jù)從內(nèi)存緩沖區(qū)直接寫到網(wǎng)卡的緩沖區(qū)里面出革。在使用了這樣的零拷貝的方法之后呢造壮,我們傳輸同樣數(shù)據(jù)的時(shí)間,可以縮減為原來的1/3骂束,相當(dāng)于提升了3倍的吞吐率耳璧。
這也是為什么,Kafka是目前實(shí)時(shí)數(shù)據(jù)傳輸管道的標(biāo)準(zhǔn)解決方案