如何解決高併發中的 I-O 瓶頸?

我們都知道,在當前的大數據時代背景下,I/O 的速度比內存要慢,尤其是性能問題與 I/O 相關的問題更加突出。

在許多應用場景中,I/O 讀寫操作已經成爲系統性能的一個重要瓶頸,這是不能忽視的。

什麼是 I/O?

I/O 作爲機器獲取和交換信息的主要渠道,流是執行 I/O 操作的主要方法。

在計算機中,流表示信息的傳輸。流保持順序,因此針對特定的機器或應用程序,我們通常將從外部獲得的信息稱爲輸入流(InputStream),將從機器或應用程序發送出去的信息稱爲輸出流(OutputStream)。

它們一起被稱爲輸入 / 輸出流(I/O 流)。

當機器或程序交換信息或數據時,它們通常首先將對象或數據轉換爲一種特定形式的流。

然後,通過流的傳輸,數據到達指定的機器或程序。在目標位置,流被轉換回對象數據。

因此,流可以被視爲一種攜帶數據的手段,促進數據的交換和傳輸。

Java 的 I/O 操作類位於java.io包中。其中,InputStreamOutputStreamReaderWriter類是 I/O 包中的四個基本類。

它們分別處理字節流和字符流。下面的圖表說明了這一點:

+-------------+  
|   InputStream   |  
+------+------+
^  
|  
+---------+---------+
|       FileInputStream     |
+-----------------------+
+-------------+  
|   OutputStream  |  
+------+------+
^  
|  
+---------+---------+
|     FileOutputStream   |
+-----------------------+
+-------------+  
|       Reader        |  
+------+------+
^  
|  
+----------+---------+
|     FileReader         |
+-----------------------+
+-------------+  
|       Writer         |  
+------+------+
^  
|  
+----------+---------+
|    FileWriter         |
+-----------------------+

無論是文件讀寫還是網絡傳輸 / 接收,信息的最小存儲單元始終是字節。那麼爲什麼 I/O 流操作被分類爲字節流操作和字符流操作呢?

我們知道,將字符轉換爲字節需要編碼,而這個過程可能是耗時的。

如果我們不知道編碼類型,很容易遇到字符亂碼等問題。因此,I/O 流提供了與字符直接工作的接口,使我們在日常工作中可以方便地進行字符流操作。

字節流。

InputStreamOutputStream是字節流的抽象類,這兩個抽象類派生出了幾個子類,每個子類都設計用於不同類型的操作。

根據具體要求,您可以選擇不同的子類來實現相應的功能。

• 如果需要執行文件讀寫操作,可以使用FileInputStreamFileOutputStream。它們適用於從文件讀取數據和將數據寫入文件。• 如果要使用數組進行讀寫操作,可以使用ByteArrayInputStreamByteArrayOutputStream。這些類允許您將數據讀取和寫入字節數組。• 如果要進行常規字符串讀寫操作,並希望引入緩衝以提高性能,可以使用BufferedInputStreamBufferedOutputStream。這些類在讀寫過程中引入了緩衝區,有效地減少了實際的 I/O 操作次數,從而提高了效率。

字符流。

ReaderWriter是字符流的抽象類,這兩個抽象類也派生出了幾個子類,每個子類都設計用於不同類型的操作。具體細節如下圖所示:

+---------+  
|   Reader    |  
+------+------+
^  
|  
+---------+---------+
|   InputStreamReader   |
+-----------------------+
|      FileReader          |
+-----------------------+
|      CharArrayReader   |
+-----------------------+
+---------+  
|    Writer    |  
+------+------+
^  
|  
+---------+---------+
|   OutputStreamWriter   |
+-----------------------+
|      FileWriter          |
+-----------------------+
|      CharArrayWriter   |
+-----------------------+

I/O 性能問題。

我們知道,I/O 操作可以分爲磁盤 I/O 操作和網絡 I/O 操作。

前者涉及將數據從磁盤源讀取到內存中,然後將讀取的信息持久化到物理磁盤中。

後者涉及將網絡中的信息獲取到內存中,最終將信息傳輸回網絡。

然而,無論是磁盤 I/O 還是網絡 I/O,在傳統 I/O 系統中都會遇到顯着的性能問題。

# 1. 多次內存複製。

在傳統 I/O 中,我們可以使用InputStream從源讀取數據,並將數據流輸入到緩衝區中。然後,我們可以使用OutputStream將數據輸出到外部設備,包括磁盤和網絡。

在繼續之前,您可以查看操作系統中輸入操作的具體過程,如下圖所示:

 

•JVM 發起read()系統調用,並向內核發送讀取請求。• 內核向硬件發送讀取命令,等待數據準備好。• 內核將數據複製到自己的緩衝區中。• 操作系統

的內核將數據複製到用戶空間緩衝區中,然後read()系統調用返回。

在此過程中,數據首先從外部設備複製到內核空間,然後從內核空間複製到用戶空間。

這導致了兩次內存複製操作。這些操作導致不必要的數據複製和上下文切換,最終降低了 I/O 的性能。

# 2. 阻塞。

在傳統 I/O 中,InputStreamread()操作通常是使用 while 循環實現的。它持續等待數據準備好後才返回。

這意味着如果沒有準備好的數據,讀取操作將一直等待,導致用戶線程被阻塞。

在連接請求較少的情況下,這種方法效果良好,提供快速的響應時間。

然而,在處理大量連接請求時,創建大量的監聽線程變得必要。在這種情況下,如果線程等待未準備好的數據,它將被阻塞並進入等待狀態。

一旦線程被阻塞,它們將不斷爭奪 CPU 資源,導致頻繁的 CPU 上下文切換。這種情況增加了系統的性能開銷。

這就是爲什麼在具有高併發需求的場景中,由於線程管理和上下文切換的高成本,傳統的阻塞式 I/O 可能變得效率低下的原因。

通常使用異步編程和非阻塞 I/O 技術來緩解這些問題,並提高系統效率。

如何優化 I/O 操作?

# 1. 使用緩衝。

使用緩衝是優化讀寫流操作的有效方法,減少頻繁的磁盤或網絡訪問,從而提高性能。以下是使用緩衝來優化讀寫流操作的一些方法:

使用緩衝流:Java 提供了類似BufferedReaderBufferedWriter的類,可以包裝其他輸入和輸出流,在讀寫操作期間引入緩衝機制。這允許批量讀取或寫入數據,減少了實際 I/O 操作的頻率。• 指定緩衝區大小:在創建緩衝流時,您可以指定緩衝區的大小。根據數據量和性能要求選擇適當的緩衝區大小,可以優化讀寫操作。• 使用 java.nio:Java NIO(新 I/O)庫提供了更靈活和高效的緩衝管理。通過使用諸如ByteBuffer之類的緩衝類,您可以更好地管理內存和數據。• 一次性讀取或寫入多個項:通過使用適當的 API,您可以一次性讀取或寫入多個數據項,減少 I/O 操作次數。• 合併操作:如果需要執行連續的讀取或寫入操作,請考慮將它們合併爲更大的操作,以減少系統調用的開銷。• 及時刷新:對於輸出流,及時調用flush()方法可以確保數據立即寫入目標,而不僅僅停留在緩衝區中。• 使用 try-with-resources:在 Java 7 及更高版本中,使用try-with-resources可以確保在操作完成後自動關閉流並釋放資源,避免資源泄漏。

以下是使用緩衝進行文件讀寫的示例代碼片段:

try (BufferedReader reader = new BufferedReader(new FileReader("input.txt"));
     BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt"))) {
    String line;
    while ((line = reader.readLine()) != null) {
        // 處理行
        writer.write(line);
        writer.newLine(); // 添加新行
    }
} catch (IOException e) {
    e.printStackTrace();
}

# 2. 使用DirectBuffer減少內存複製。

使用DirectBuffer是一種減少 I/O 操作中內存複製的技術,特別是在 Java NIO(新 I/O)的上下文中。

DirectBuffer允許您直接使用非堆內存,這可以導致 Java 和本地代碼之間更有效的數據傳輸。

在涉及大量數據的 I/O 操作中,這可能特別有益。

以下是如何使用DirectBuffer減少內存複製的方法:

  1. 分配 DirectBuffer:不要使用傳統的 Java 堆基數組,而是使用諸如ByteBuffer.allocateDirect()之類的類從本地內存中分配DirectBuffer。2. 包裝現有緩衝區:您還可以使用ByteBuffer.wrap()來包裝現有的本地內存緩衝區,只需指定本地內存地址。3. 與通道 I/O 一起使用:當使用 NIO 通道(FileChannelSocketChannel等)時,可以直接將數據讀入DirectBuffer或直接從DirectBuffer寫入數據,無需額外的複製。4. 與 JNI 一起使用:如果通過 Java 本地接口(JNI)與本機代碼一起工作,使用DirectBuffer可以使您的本機代碼直接訪問和操作數據,而無需昂貴的內存複製。5. 注意內存釋放:請記住,當您使用完DirectBuffer時,需要顯式地釋放直接內存,以防止內存泄漏。調用DirectBuffer上的cleaner()方法以釋放關聯的本地內存。

以下是在ByteBuffer中使用DirectBuffer以進行高效 I/O 的簡化示例:

try (FileChannel channel = FileChannel.open(Paths.get("data.bin"), StandardOpenOption.READ)) {
    int bufferSize = 4096; // 根據需要調整
    ByteBuffer directBuffer = ByteBuffer.allocateDirect(bufferSize);
 int bytesRead;
    while ((bytesRead = channel.read(directBuffer)) != -1) {
        directBuffer.flip(); // 準備讀取
        // 在直接緩衝區中處理數據
        // ...
        directBuffer.clear(); // 準備下一次讀取
    }
} catch (IOException e) {
    e.printStackTrace();
}

# 3. 避免阻塞並優化 I/O 操作。

避免阻塞並優化 I/O 操作是提高系統性能和響應性的關鍵。以下是實現這些目標的一些方法:

  1. 使用非阻塞 I/O:採用非阻塞 I/O 技術,如 Java NIO,允許程序在等待數據準備就緒時繼續執行其他任務。這可以通過選擇器實現,它使單個線程能夠處理多個通道。2. 利用異步 I/O:異步 I/O 允許程序提交 I/O 操作並在完成時得到通知。Java NIO2(Java 7+)提供了異步 I/O 的支持。這減少了線程阻塞,並使其他任務能夠在等待 I/O 完成時執行。3. 使用線程池:有效地利用線程池管理線程資源,避免爲每個連接創建新線程。這減少了線程創建和銷燬的開銷。4. 利用事件驅動模型:利用諸如 Reactor、Netty 等事件驅動框架可以有效地管理連接和 I/O 事件,實現高效的非阻塞 I/O。5. 分離 CPU 密集型和 I/O 操作:將 CPU 密集型任務與 I/O 操作分開,以防止 I/O 阻塞 CPU。可以使用多線程或多進程進行分離。6. 批量處理:將多個小的 I/O 操作合併爲一個更大的批量操作,減少單獨操作的開銷,提高效率。7. 使用緩衝區:使用緩衝區減少頻繁的磁盤或網絡訪問,提高性能。這適用於文件 I/O 和網絡 I/O。8. 定期維護和優化:定期監控和優化磁盤、網絡和數據庫等資源,以確保它們保持良好的性能。9. 使用專門的框架:選擇適當的框架,如NettyVert.x等,這些框架具有高效的非阻塞和異步 I/O 功能。

根據您的應用場景和要求,您可以實現其中一個或多個方法,以避免阻塞,優化 I/O 操作,並增強系統性能和響應性。

# 4. 通道。

正如前面所討論的,傳統的 I/O 最初依賴於InputStreamOutputStream操作流,這些流按字節爲單位工作。

在高併發和大數據的情況下,這種方法很容易導致阻塞,從而導致性能下降。

此外,從用戶空間複製輸出數據到內核空間,然後再複製到輸出設備,增加了系統性能開銷。

爲了解決性能問題,傳統的 I/O 後來引入了緩衝作爲緩解阻塞的手段。

它使用緩衝塊作爲最小單元。然而,即使使用緩衝,整體性能仍然不夠理想。

然後出現了 NIO(新 I/O),它基於緩衝塊單元操作。

在緩衝的基礎上,它引入了兩個組件:“通道” 和 “選擇器”。這些補充使得非阻塞 I/O 操作成爲可能。

NIO非常適合具有大量 I/O 連接請求的情況。這三個組件共同增強了 I/O 的整體性能。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/ndpiurxasv_NNhRtRKCpjw