<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Netty、Kafka 中的零拷貝技術(shù)到底有多牛?

          共 2047字,需瀏覽 5分鐘

           ·

          2020-12-05 23:59


          • 前言

          • I/O概念

          • Java零拷貝

          • Netty零拷貝

          • 其他零拷貝

          • 總結(jié)



          前言


          從字面意思理解就是數(shù)據(jù)不需要來回的拷貝,大大提升了系統(tǒng)的性能;這個詞我們也經(jīng)常在java nio,netty,kafka,RocketMQ等框架中聽到,經(jīng)常作為其提升性能的一大亮點;下面從I/O的幾個概念開始,進而在分析零拷貝。


          I/O概念


          1.緩沖區(qū)


          緩沖區(qū)是所有I/O的基礎(chǔ),I/O講的無非就是把數(shù)據(jù)移進或移出緩沖區(qū);進程執(zhí)行I/O操作,就是向操作系統(tǒng)發(fā)出請求,讓它要么把緩沖區(qū)的數(shù)據(jù)排干(寫),要么填充緩沖區(qū)(讀);下面看一個java進程發(fā)起read請求加載數(shù)據(jù)大致的流程圖:



          進程發(fā)起read請求之后,內(nèi)核接收到read請求之后,會先檢查內(nèi)核空間中是否已經(jīng)存在進程所需要的數(shù)據(jù),如果已經(jīng)存在,則直接把數(shù)據(jù)copy給進程的緩沖區(qū);如果沒有內(nèi)核隨即向磁盤控制器發(fā)出命令,要求從磁盤讀取數(shù)據(jù),磁盤控制器把數(shù)據(jù)直接寫入內(nèi)核read緩沖區(qū),這一步通過DMA完成;


          接下來就是內(nèi)核將數(shù)據(jù)copy到進程的緩沖區(qū);如果進程發(fā)起write請求,同樣需要把用戶緩沖區(qū)里面的數(shù)據(jù)copy到內(nèi)核的socket緩沖區(qū)里面,然后再通過DMA把數(shù)據(jù)copy到網(wǎng)卡中,發(fā)送出去;


          你可能覺得這樣挺浪費空間的,每次都需要把內(nèi)核空間的數(shù)據(jù)拷貝到用戶空間中,所以零拷貝的出現(xiàn)就是為了解決這種問題的;關(guān)于零拷貝提供了兩種方式分別是:mmap+write方式,sendfile方式;


          2.虛擬內(nèi)存


          所有現(xiàn)代操作系統(tǒng)都使用虛擬內(nèi)存,使用虛擬的地址取代物理地址,這樣做的好處是:


          1.一個以上的虛擬地址可以指向同一個物理內(nèi)存地址, 2.虛擬內(nèi)存空間可大于實際可用的物理地址;利用第一條特性可以把內(nèi)核空間地址和用戶空間的虛擬地址映射到同一個物理地址,這樣DMA就可以填充對內(nèi)核和用戶空間進程同時可見的緩沖區(qū)了,大致如下圖所示:


          省去了內(nèi)核與用戶空間的往來拷貝,java也利用操作系統(tǒng)的此特性來提升性能,下面重點看看java對零拷貝都有哪些支持。


          3.mmap+write方式


          使用mmap+write方式代替原來的read+write方式,mmap是一種內(nèi)存映射文件的方法,即將一個文件或者其它對象映射到進程的地址空間,實現(xiàn)文件磁盤地址和進程虛擬地址空間中一段虛擬地址的一一對映關(guān)系;這樣就可以省掉原來內(nèi)核read緩沖區(qū)copy數(shù)據(jù)到用戶緩沖區(qū),但是還是需要內(nèi)核read緩沖區(qū)將數(shù)據(jù)copy到內(nèi)核socket緩沖區(qū),大致如下圖所示:


          4.sendfile方式


          sendfile系統(tǒng)調(diào)用在內(nèi)核版本2.1中被引入,目的是簡化通過網(wǎng)絡(luò)在兩個通道之間進行的數(shù)據(jù)傳輸過程。sendfile系統(tǒng)調(diào)用的引入,不僅減少了數(shù)據(jù)復(fù)制,還減少了上下文切換的次數(shù),大致如下圖所示:


          數(shù)據(jù)傳送只發(fā)生在內(nèi)核空間,所以減少了一次上下文切換;但是還是存在一次copy,能不能把這一次copy也省略掉,Linux2.4內(nèi)核中做了改進,將Kernel buffer中對應(yīng)的數(shù)據(jù)描述信息(內(nèi)存地址,偏移量)記錄到相應(yīng)的socket緩沖區(qū)當(dāng)中,這樣連內(nèi)核空間中的一次cpu copy也省掉了;


          Java零拷貝


          1.MappedByteBuffer


          java nio提供的FileChannel提供了map()方法,該方法可以在一個打開的文件和MappedByteBuffer之間建立一個虛擬內(nèi)存映射,MappedByteBuffer繼承于ByteBuffer,類似于一個基于內(nèi)存的緩沖區(qū),只不過該對象的數(shù)據(jù)元素存儲在磁盤的一個文件中;


          調(diào)用get()方法會從磁盤中獲取數(shù)據(jù),此數(shù)據(jù)反映該文件當(dāng)前的內(nèi)容,調(diào)用put()方法會更新磁盤上的文件,并且對文件做的修改對其他閱讀者也是可見的;下面看一個簡單的讀取實例,然后在對MappedByteBuffer進行分析:


          public?class?MappedByteBufferTest?{

          ????public?static?void?main(String[]?args)?throws?Exception?{
          ????????File?file?=?new?File("D://db.txt");
          ????????long?len?=?file.length();
          ????????byte[]?ds?=?new?byte[(int)?len];
          ????????MappedByteBuffer?mappedByteBuffer?=?new?FileInputStream(file).getChannel().map(FileChannel.MapMode.READ_ONLY,?0,
          ????????????????len);
          ????????for?(int?offset?=?0;?offset?????????????byte?b?=?mappedByteBuffer.get();
          ????????????ds[offset]?=?b;
          ????????}
          ????????Scanner?scan?=?new?Scanner(new?ByteArrayInputStream(ds)).useDelimiter("?");
          ????????while?(scan.hasNext())?{
          ????????????System.out.print(scan.next()?+?"?");
          ????????}
          ????}
          }


          主要通過FileChannel提供的map()來實現(xiàn)映射,map()方法如下:


          ????public?abstract?MappedByteBuffer?map(MapMode?mode,
          ?????????????????????????????????????????long?position,?long?size)

          ????????throws?IOException
          ;



          分別提供了三個參數(shù),MapMode,Position和size;分別表示:MapMode:映射的模式,可選項包括:READ_ONLY,READ_WRITE,PRIVATE;Position:從哪個位置開始映射字節(jié)數(shù)的位置;Size:從position開始向后多少個字節(jié);


          重點看一下MapMode,請兩個分別表示只讀和可讀可寫,當(dāng)然請求的映射模式受到Filechannel對象的訪問權(quán)限限制,如果在一個沒有讀權(quán)限的文件上啟用READ_ONLY,將拋出NonReadableChannelException;PRIVATE模式表示寫時拷貝的映射,意味著通過put()方法所做的任何修改都會導(dǎo)致產(chǎn)生一個私有的數(shù)據(jù)拷貝并且該拷貝中的數(shù)據(jù)只有MappedByteBuffer實例可以看到;


          該過程不會對底層文件做任何修改,而且一旦緩沖區(qū)被施以垃圾收集動作(garbage collected),那些修改都會丟失;大致瀏覽一下map()方法的源碼:


          ????public?MappedByteBuffer?map(MapMode?mode,?long?position,?long?size)
          ????????throws?IOException
          ????
          {
          ????????????...省略...
          ????????????int?pagePosition?=?(int)(position?%?allocationGranularity);
          ????????????long?mapPosition?=?position?-?pagePosition;
          ????????????long?mapSize?=?size?+?pagePosition;
          ????????????try?{
          ????????????????//?If?no?exception?was?thrown?from?map0,?the?address?is?valid
          ????????????????addr?=?map0(imode,?mapPosition,?mapSize);
          ????????????}?catch?(OutOfMemoryError?x)?{
          ????????????????//?An?OutOfMemoryError?may?indicate?that?we've?exhausted?memory
          ????????????????//?so?force?gc?and?re-attempt?map
          ????????????????System.gc();
          ????????????????try?{
          ????????????????????Thread.sleep(100);
          ????????????????}?catch?(InterruptedException?y)?{
          ????????????????????Thread.currentThread().interrupt();
          ????????????????}
          ????????????????try?{
          ????????????????????addr?=?map0(imode,?mapPosition,?mapSize);
          ????????????????}?catch?(OutOfMemoryError?y)?{
          ????????????????????//?After?a?second?OOME,?fail
          ????????????????????throw?new?IOException("Map?failed",?y);
          ????????????????}
          ????????????}

          ????????????//?On?Windows,?and?potentially?other?platforms,?we?need?an?open
          ????????????//?file?descriptor?for?some?mapping?operations.
          ????????????FileDescriptor?mfd;
          ????????????try?{
          ????????????????mfd?=?nd.duplicateForMapping(fd);
          ????????????}?catch?(IOException?ioe)?{
          ????????????????unmap0(addr,?mapSize);
          ????????????????throw?ioe;
          ????????????}

          ????????????assert?(IOStatus.checkAll(addr));
          ????????????assert?(addr?%?allocationGranularity?==?0);
          ????????????int?isize?=?(int)size;
          ????????????Unmapper?um?=?new?Unmapper(addr,?mapSize,?isize,?mfd);
          ????????????if?((!writable)?||?(imode?==?MAP_RO))?{
          ????????????????return?Util.newMappedByteBufferR(isize,
          ?????????????????????????????????????????????????addr?+?pagePosition,
          ?????????????????????????????????????????????????mfd,
          ?????????????????????????????????????????????????um);
          ????????????}?else?{
          ????????????????return?Util.newMappedByteBuffer(isize,
          ????????????????????????????????????????????????addr?+?pagePosition,
          ????????????????????????????????????????????????mfd,
          ????????????????????????????????????????????????um);
          ????????????}
          ?????}


          大致意思就是通過native方法獲取內(nèi)存映射的地址,如果失敗,手動gc再次映射;最后通過內(nèi)存映射的地址實例化出MappedByteBuffer,MappedByteBuffer本身是一個抽象類,其實這里真正實例話出來的是DirectByteBuffer;


          2.DirectByteBuffer


          DirectByteBuffer繼承于MappedByteBuffer,從名字就可以猜測出開辟了一段直接的內(nèi)存,并不會占用jvm的內(nèi)存空間;


          上一節(jié)中通過Filechannel映射出的MappedByteBuffer其實際也是DirectByteBuffer,當(dāng)然除了這種方式,也可以手動開辟一段空間:


          ByteBuffer?directByteBuffer?=?ByteBuffer.allocateDirect(100);


          如上開辟了100字節(jié)的直接內(nèi)存空間;


          3.Channel-to-Channel傳輸


          經(jīng)常需要從一個位置將文件傳輸?shù)搅硗庖粋€位置,F(xiàn)ileChannel提供了transferTo()方法用來提高傳輸?shù)男剩紫瓤匆粋€簡單的實例:


          public?class?ChannelTransfer?{
          ????public?static?void?main(String[]?argv)?throws?Exception?{
          ????????String?files[]=new?String[1];
          ????????files[0]="D://db.txt";
          ????????catFiles(Channels.newChannel(System.out),?files);
          ????}

          ????private?static?void?catFiles(WritableByteChannel?target,?String[]?files)
          ????????????throws?Exception?
          {
          ????????for?(int?i?=?0;?i?????????????FileInputStream?fis?=?new?FileInputStream(files[i]);
          ????????????FileChannel?channel?=?fis.getChannel();
          ????????????channel.transferTo(0,?channel.size(),?target);
          ????????????channel.close();
          ????????????fis.close();
          ????????}
          ????}
          }


          通過FileChannel的transferTo()方法將文件數(shù)據(jù)傳輸?shù)絊ystem.out通道,接口定義如下:


          ????public?abstract?long?transferTo(long?position,?long?count,
          ????????????????????????????????????WritableByteChannel?target)

          ????????throws?IOException
          ;


          幾個參數(shù)也比較好理解,分別是開始傳輸?shù)奈恢茫瑐鬏數(shù)淖止?jié)數(shù),以及目標(biāo)通道;transferTo()允許將一個通道交叉連接到另一個通道,而不需要一個中間緩沖區(qū)來傳遞數(shù)據(jù);


          注:這里不需要中間緩沖區(qū)有兩層意思:第一層不需要用戶空間緩沖區(qū)來拷貝內(nèi)核緩沖區(qū),另外一層兩個通道都有自己的內(nèi)核緩沖區(qū),兩個內(nèi)核緩沖區(qū)也可以做到無需拷貝數(shù)據(jù);


          Netty零拷貝


          netty提供了零拷貝的buffer,在傳輸數(shù)據(jù)時,最終處理的數(shù)據(jù)會需要對單個傳輸?shù)膱笪模M行組合和拆分,Nio原生的ByteBuffer無法做到,netty通過提供的Composite(組合)和Slice(拆分)兩種buffer來實現(xiàn)零拷貝;看下面一張圖會比較清晰:



          TCP層HTTP報文被分成了兩個ChannelBuffer,這兩個Buffer對我們上層的邏輯(HTTP處理)是沒有意義的。


          但是兩個ChannelBuffer被組合起來,就成為了一個有意義的HTTP報文,這個報文對應(yīng)的ChannelBuffer,才是能稱之為”Message”的東西,這里用到了一個詞”Virtual Buffer”。可以看一下netty提供的CompositeChannelBuffer源碼:


          public?class?CompositeChannelBuffer?extends?AbstractChannelBuffer?{

          ????private?final?ByteOrder?order;
          ????private?ChannelBuffer[]?components;
          ????private?int[]?indices;
          ????private?int?lastAccessedComponentId;
          ????private?final?boolean?gathering;

          ????public?byte?getByte(int?index)?{
          ????????int?componentId?=?componentId(index);
          ????????return?components[componentId].getByte(index?-?indices[componentId]);
          ????}
          ????...省略...


          components用來保存的就是所有接收到的buffer,indices記錄每個buffer的起始位置,lastAccessedComponentId記錄上一次訪問的ComponentId;CompositeChannelBuffer并不會開辟新的內(nèi)存并直接復(fù)制所有ChannelBuffer內(nèi)容,而是直接保存了所有ChannelBuffer的引用,并在子ChannelBuffer里進行讀寫,實現(xiàn)了零拷貝。


          其他零拷貝


          RocketMQ的消息采用順序?qū)懙絚ommitlog文件,然后利用consume queue文件作為索引;RocketMQ采用零拷貝mmap+write的方式來回應(yīng)Consumer的請求;同樣kafka中存在大量的網(wǎng)絡(luò)數(shù)據(jù)持久化到磁盤和磁盤文件通過網(wǎng)絡(luò)發(fā)送的過程,kafka使用了sendfile零拷貝方式;


          總結(jié)



          零拷貝如果簡單用java里面對象的概率來理解的話,其實就是使用的都是對象的引用,每個引用對象的地方對其改變就都能改變此對象,永遠只存在一份對象。


          -END-

          我是武哥,最后給大家免費分享我寫的 10 萬字 Spring Boot 學(xué)習(xí)筆記(帶完整目錄)以及對應(yīng)的源碼。這是我之前在 CSDN 開的一門課,所以筆記非常詳細完整,我準(zhǔn)備將資料分享出來給大家免費學(xué)習(xí),相信大家看完一定會有所收獲(下面有下載方式)。


          可以看出,我當(dāng)時備課非常詳細,目錄非常完整,讀者可以手把手跟著筆記,結(jié)合源代碼來學(xué)習(xí)。現(xiàn)在免費分享出來,有需要的讀者可以下載學(xué)習(xí),就在我下面的公眾號Java禿頭哥里回復(fù):筆記,就行。



          如有文章對你有幫助,

          在看轉(zhuǎn)發(fā)是對我最大的支持



          關(guān)注Java禿頭哥

          只有禿頭才能更強


          點贊是最大的支持?

          瀏覽 12
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  caopengav | 波多野结衣乱码无码视频 | 久久99高清| 少妇白浆视频 | 波多野结衣一区 |