kafka通過零拷貝實現高效的資料傳輸
許多Web應用程式都提供了大量的靜態內容,這相當於從磁碟讀取資料並將完全相同的資料寫回到響應socket。這個活動可能似乎只需要相對較少的CPU活動,但是效率有些低下:核心從磁碟讀取資料,並將其從核心使用者邊界推送到應用程式,然後應用程式將其推回到核心使用者邊界寫出來的socket。實際上,應用程式作為一個低效的媒介,從磁碟檔案獲取資料到socket。
每次資料遍歷使用者核心邊界時,都必須進行復制,這會消耗CPU週期和記憶體頻寬。幸運的是,您可以通過一種稱為“適當地
- 零拷貝”的技術來消除這些副本。核心使用零拷貝的應用程式要求核心直接將資料從磁碟檔案複製到套接字,而不通過應用程式。零拷貝大大提高了應用程式的效能,減少了核心和使用者模式之間的上下文切換次數。
Java類庫通過transferTo()
in方法在 Linux和UNIX系統上支援零拷貝java.nio.channels.FileChannel
。您可以使用該transferTo()
方法將位元組從其呼叫的通道直接傳輸到另一個可寫位元組通道,而不需要資料流經應用程式。本文首先演示了通過傳統的複製語義進行簡單檔案傳輸所帶來的開銷,然後展示瞭如何使用零複製技術 transferTo()
獲得更好的效能。
日期轉移:傳統的方法
考慮從檔案讀取並通過網路將資料傳輸到另一個程式的情況。(本場景描述了許多伺服器應用程式的行為,包括提供靜態內容的Web應用程式,FTP伺服器,郵件伺服器等等)。操作的核心是清單1中的兩個呼叫(參見
清單1.將檔案中的位元組複製到套接字
1 2 |
File.read(fileDesc,
buf, len);
Socket.send(socket,
buf, len);
|
雖然清單1在概念上很簡單,但在內部,複製操作需要在使用者模式和核心模式之間進行四次上下文切換,並且在操作完成之前將資料複製四次。圖1顯示了資料如何從檔案內部移動到套接字:
圖1.傳統的資料複製方法
圖2顯示了上下文切換:
圖2.傳統的上下文切換
涉及的步驟是:
-
該
read()
呼叫導致從使用者模式到核心模式的上下文切換(參見圖2)。內部sys_read()
-
所請求的資料量從讀取緩衝區複製到使用者緩衝區,然後
read()
呼叫返回。呼叫返回導致另一個上下文從核心切換回使用者模式。現在資料儲存在使用者地址空間緩衝區中。 -
該
send()
插座呼叫導致從使用者模式到核心模式的上下文切換。執行第三個拷貝將資料再次放入核心地址空間緩衝區。但是這一次,資料被放入不同的緩衝區,一個與目標socket相關的緩衝區。 -
該
send()
系統呼叫返回,創造了第四上下文切換。獨立地和非同步地,當DMA引擎將資料從核心緩衝區傳遞到協議引擎時發生第四個副本。
使用中間核心緩衝區(而不是將資料直接傳輸到使用者緩衝區)可能看起來效率低下。但是中間核心緩衝區被引入程序來提高效能。在讀取端使用中間緩衝區允許核心緩衝區充當“預讀快取”,當應用程式沒有要求與核心緩衝區一樣多的資料時。當請求的資料量小於核心緩衝區大小時,這會顯著提高效能。寫入側的中間緩衝區允許寫入非同步完成。
不幸的是,如果所請求資料的大小遠遠大於核心緩衝區的大小,這種方法本身可能會成為效能瓶頸。在磁碟,核心緩衝區和使用者緩衝區最終傳遞到應用程式之前,資料被複制多次。
零拷貝通過消除這些冗餘資料副本來提高效能。
資料傳輸:零拷貝方法
如果您重新檢查傳統方案,則會注意到第二個和第三個資料副本實際上不是必需的。除了快取資料並將其傳回到socket緩衝區之外,應用程式只做其他事情。相反,資料可以直接從讀取緩衝區傳輸到socket緩衝區。該transferTo()
方法可以讓你做到這一點。清單2顯示了以下方法的簽名 transferTo()
:
清單2. transferTo()
方法
1 |
public
void transferTo(long position, long count, WritableByteChannel target);
|
該transferTo()
方法將資料從檔案通道傳輸到給定的可寫位元組通道。在內部,它取決於底層作業系統對零拷貝的支援; 在UNIX和各種Linux中,這個呼叫被路由到sendfile()
系統呼叫,如清單3所示,它將資料從一個檔案描述符傳輸到另一個:
清單3. sendfile()
系統呼叫
1 2 |
#include
< sys /socket.h>
ssize_t
sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
|
清單1中的file.read()
和socket.send()
呼叫的操作可以被一個呼叫所取代 ,如清單4所示:transferTo()
清單4.使用 transferTo()
將資料從磁碟檔案複製到socket
1 |
transferTo(position,
count, writableChannel);
|
圖3顯示了transferTo()
使用該方法時的資料路徑:
圖3.使用資料拷貝 transferTo()
圖4顯示了transferTo()
使用該方法時的上下文切換:
圖4.使用的上下文切換 transferTo()
transferTo()
在清單4中使用時所採取的步驟如下:
-
該
transferTo()
方法使得檔案內容被DMA引擎複製到讀緩衝器中。然後,資料被核心複製到與輸出套接字關聯的核心緩衝區中。 - 第三個副本是在DMA引擎將資料從核心socket緩衝區socket傳遞給協議引擎時發生的。
這是一個改進:我們已經將上下文切換次數從四次減少到兩次,並將資料副本的數量從四個減少到三個(其中只有一個涉及CPU)。但是這還沒有使我們達到零拷貝的目標。如果底層網路介面卡支援收集操作,我們可以進一步減少核心的資料重複。在Linux核心2.4和更高版本中,套接字緩衝區描述符被修改以適應這個要求。這種方法不僅減少了多個上下文切換,還消除了需要CPU參與的重複資料副本。使用者端的使用情況仍然保持不變,但內在因素已經改變:
-
該
transferTo()
方法使得檔案內容被DMA引擎複製到核心緩衝區中。 - 沒有資料被複制到socket緩衝區中。相反,只有帶有關於資料的位置和長度的資訊的描述符被附加到socket緩衝區。DMA引擎直接將資料從核心緩衝區傳遞到協議引擎,從而消除了剩餘的最終CPU副本。
圖5顯示了使用transferTo()
收集操作的資料副本:
圖5. transferTo()
使用和收集操作時的資料拷貝
建立一個檔案伺服器
現在,讓我們將零拷貝付諸實踐,使用在客戶機和伺服器之間傳輸檔案的相同示例(請參閱下載以獲取示例程式碼)。TraditionalClient.java
並 TraditionalServer.java
基於傳統的複製語義,使用File.read()
和Socket.send()
。TraditionalServer.java
是一個伺服器程式,它偵聽特定的埠以供客戶端連線,然後從套接字一次讀取4K位元組的資料。TraditionalClient.java
連線到伺服器,File.read()
從檔案讀取(使用)4K位元組的資料,並socket.send()
通過socket將內容傳送(使用)到伺服器。
類似地,TransferToServer.java
和 TransferToClient.java
執行相同的功能,而是使用transferTo()
方法(以及反過來的sendfile()
系統呼叫)將檔案從伺服器傳送到客戶端。
效能比較
我們在執行2.6核心的Linux系統上執行示例程式,並測量傳統方法和transferTo()
不同尺寸方法的執行時間(以毫秒為單位)。表1顯示了結果:
表1.效能比較:傳統方法與零拷貝
檔案大小 | 正常檔案傳輸(毫秒) | transferTo(ms) |
---|---|---|
7MB | 156 | 45 |
21MB | 337 | 128 |
63MB | 843 | 387 |
98MB | 1320 | 617 |
200MB | 2124 | 1150 |
350MB | 3631 | 1762 |
700MB | 13498 | 4422 |
1GB | 18399 | 8537 |
正如您所看到的,transferTo()
與傳統方法相比,該API將時間縮短了約65%。這對於從一個I
/ O通道向另一個I / O通道複製大量資料的應用程式(如Web伺服器)具有顯著的提高效能的潛力。
概要
我們已經展示了使用transferTo()
相比於從一個通道讀取並將相同資料寫入另一個通道的效能優點 。中間緩衝區副本
- 即使是那些隱藏在核心中的副本 - 也會有可測量的成本。在通道之間大量複製資料的應用程式中,零複製技術可以顯著提高效能。