如何實現高性能的IO及其原理?


程序運行在內存以及IO的體現

  首先普及一下常識,如圖所示:

            

   1、在整個內存空間中,跑着各種各樣的程序,有Java程序、C程序,他們共用一塊內存空間。

  2、對於Java程序,JVM會申請一塊堆空間,通過Xmx可以設置,其余空間是堆外空間,其中每個線程有自己的線程棧,保證線程內存隔離,堆空間使用完以后,會觸發Full FC,堆外空間所有進程可共享使用,無限制。

  3、所有系統運行的程序都必須通過操作系統內核進行IO操作,操作系統也是程序,也需要一定的內存空間。

一、使用Buffer代替基本IO

  我們寫一個方法,此方法使用了FileWriter進行了文件的寫操作,我們都知道不調用flush()可能會造成數據丟失,那么為什么呢,flush操作到底做了些什么呢?

public void fileIO() throws Exception {

    File file = new File("/Volumes/work/temp/temp.txt");
    if (file.exists()) {
        file.delete();
    }
    file.createNewFile();

    FileInputStream fileInputStream = new FileInputStream(file);
    FileWriter fileWriter = new FileWriter(file);

    fileWriter.write("hello");
    fileWriter.write("world");
    fileWriter.write("\nhello world");

    Thread.sleep(99999); fileWriter.flush();
    fileWriter.close();
}

  我們知道我們在寫數據的時候不管是C還是Java都會有兩個緩沖區,一個是操作系統的緩沖區sys buffer,還有一個是程序的緩沖區program buffer。那么剛剛的flush操作是把程序的緩沖區內容寫到了系統緩沖區,還是把系統緩沖區的內容刷到了硬盤呢?因此我們在調用flush()之前進行了sleep操作,檢查在flush之前,具體的內容並未寫到temp.txt文件中,當我們睡眠時間結束后,可以看到調用flush方法后則把內容寫到了文件中,如圖:

          

  實際上FileWriter基本IO是沒有先寫程序緩存的,那么實際上FileWriter的每次write操作都發生了系統調用,直接寫到了內核的系統緩沖區,然后當調用flush操作時,系統緩沖區的內容再刷到了硬盤上。

  因此IO性能提升第一步:無論是InputStream還是FileWriter,都是底層的IO,是直接調用內核的,因此寫入都是直接寫入到內核的系統buffer,因此在使用IO的時候不要使用這類底層IO,否則發生大量系統調用,降低系統性能,而是應該先寫到程序buffer然后再調用系統IO,當程序buffer滿了后才通過系統調用寫到系統buffer空間中,這樣減少了大量系統調用,提升了性能。

  那么什么時候系統buffer中的數據才寫入到硬盤呢?2種情況:①.系統buffer滿了;②.執行了flush()操作,也就是發生了fsync的系統調用。

public void bufferedIO() throws Exception {
    BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(file), 1024);
    BufferedReader reader = new BufferedReader(new FileReader(file));
    bufferedOutputStream.write("hello world\nhello world".getBytes());
    bufferedOutputStream.flush();
    bufferedOutputStream.close();
    String line = reader.readLine();
    System.out.println(line);
}

  還有另一種是直接寫入到內存的,如代碼:

public void memoryIO() throws Exception {
    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(1024);
    // 字節數組輸出流在內存中創建一個字節數組緩沖區,所有發送到輸出流的數據保存在該字節數組緩沖區中。可以通過toString()和toByteArray()獲取數據
    byteArrayOutputStream.write("hello world".getBytes());
    String string = byteArrayOutputStream.toString();
    System.out.println(string);

    byte[] inData = byteArrayOutputStream.toByteArray();
    ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(inData);
    byte[] data = new byte[1024];
    byteArrayInputStream.read(data);
    System.out.println(new String(data));

    byteArrayOutputStream.flush();
    byteArrayOutputStream.close();
}

  這樣就類似於Redis一樣,是對內存進行直接操作,因此這樣也能提高不少效率。

二、堆外內存mmap直接映射內核空間

  如下圖:

            

  1、如果數據在堆內,那么在寫入磁盤時,會先序列化后拷貝到堆外,然后堆外再write到系統內核緩沖區,內核緩沖區通過系統調用fsync寫入到磁盤;

  2、如果數據是在堆外內存,那么也需要先拷貝到內核緩沖區,在fsync系統調用后也才寫入到磁盤;

  3、通過系統調用mmap申請一塊虛擬的地址空間,這片空間用戶程序和系統內核都可以訪問到。

  如下代碼:

public void randomIO() throws Exception{
    RandomAccessFile randomAccessFile = new RandomAccessFile(file,"rw");
    randomAccessFile.write("hello world\nhello chicago\nhello ChengDu".getBytes());
    FileChannel channel = randomAccessFile.getChannel();

    /**
     * 堆外的數據如果想寫磁盤,通過系統調用,經歷數據從用戶空間拷貝到內核空間
     * 堆外mapedBuffer的數據內核直接處理
     */
    // 分配在了堆上  heap空間
    // ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
    // 分配在了堆外  offheap空間
    // ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);
    //mmap  內核系統調用  堆外空間,直接映射
    MappedByteBuffer byteBuffer = channel.map(FileChannel.MapMode.READ_WRITE,0,2018);
    
    byteBuffer.put("byteBuffer testing".getBytes());

    randomAccessFile.seek(12);
    randomAccessFile.write("*****".getBytes());
}

  可以看到通過FileChannel的map方法實現系統調用,申請mmap直接映射空間,數據無需由用戶空間拷貝到系統空間,節省了一次拷貝的時間損耗,提升了性能。

三、sendfile零拷貝

  在Linux系統中。存儲在文件中的信息通過網絡傳送給客戶這樣的簡單過程中,所涉及的操作。下面是其中的部分簡單代碼:

read(file, tmp_buf, len);
write(socket, tmp_buf, len);

  其實過程中實現了多次拷貝,性能很低,如圖可知:

     

  步驟一:系統調用read導致了從用戶空間到內核空間的上下文切換。DMA模塊從磁盤中讀取文件內容,並將其存儲在內核空間的緩沖區內,完成了第1次復制

  步驟二:數據從內核空間緩沖區復制到用戶空間緩沖區,完成了第2次復制,之后系統調用read返回,這導致了從內核空間向用戶空間的上下文切換。此時,需要的數據已存放在指定的用戶空間緩沖區內(參數tmp_buf),程序可以繼續下面的操作。

  步驟三:系統調用write導致從用戶空間到內核空間的上下文切換。數據從用戶空間緩沖區被再次復制到內核空間緩沖區,完成了第3次復制。不過,這次數據存放在內核空間中與使用的socket相關的特定緩沖區中,而不是步驟一中的緩沖區。

  步驟四:系統調用返回,導致了第4次上下文切換。第4次復制在DMA模塊將數據從內核空間緩沖區傳遞至協議引擎的時候發生,這與我們的代碼的執行是獨立且異步發生的。你可能會疑惑:“為何要說是獨立、異步?難道不是在write系統調用返回前數據已經被傳送了?write系統調用的返回,並不意味着傳輸成功——它甚至無法保證傳輸的開始。調用的返回,只是表明以太網驅動程序在其傳輸隊列中有空位,並已經接受我們的數據用於傳輸。可能有眾多的數據排在我們的數據之前。除非驅動程序或硬件采用優先級隊列的方法,各組數據是依照FIFO的次序被傳輸的(圖1中叉狀的DMA copy表明這最后一次復制可以被延后)。

   因此就誕生了零拷貝:

sendfile(socket, file, len);

  如圖:

    

 

  步驟一:sendfile系統調用導致文件內容通過DMA模塊被復制到內核緩沖區中。

  步驟二:記錄數據位置和長度的描述符被加入到socket緩沖區中,DMA模塊將數據直接從內核緩沖區傳遞給協議引擎。

  基於以上實現,最終實現了“零拷貝”。

高性能IO應用

  在現實應用中,Kafka常用來進行日志處理,存在着大量的IO,其高性能就是建立在IO上的優化,如圖:

          


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM