關于Zero Copy
概述
很多web應用都會有大量的靜態文件。我們通常是從硬盤讀取這些靜態文件,并將完全相同的文件數據寫到response socket。這樣的操作需要較少的CPU,但是效率有些低,它需要經過如下的過程:kernel從硬盤讀取數據,越過kernel-user邊界將數據傳遞給用戶空間的web應用;用戶空間的web應用再次越過kernel-user邊界將完全相同的數據寫回到kernel空間的socket。在將數據從硬盤傳遞到socket的過程中,用戶空間web應用的角色相當于一個中介,并且有些低效。
數據每次經過kernel-user邊界的時候,都需要被copy一次,這樣會消耗CPU資源及內存帶寬。幸運的是,我們可以使用一種被稱為zero copy的技術來消除這些copy操作。使用zero copy技術的應用會請求kernel直接將數據從硬盤拷貝到socket,而無需再經過應用。zero copy極大地提升了應用的性能,并減少了內核態和用戶態上下文切換的次數。
在Linux和UNIX系統上,Java類庫通過java.nio.channels.FileChannel的transferTo()方法實現了對zero copy的支持。我們可以使用transferTo()方法將讀取到的字節數組直接從被調用的channel傳輸到另一個可寫的channel上,這個過程中數據流轉不需要通過應用。
接下來我們會先講解一下如何使用傳統的多次copy的機制實現數據的傳輸,而后再演示下使用transferTo()方法實現的zero copy技術是如何提升性能的。
傳統數據傳輸方案
思考一下如下的場景:從一個文件讀取數據,通過網絡將數據傳遞給另一個應用程序(這個場景描述了大部分服務器應用的行為,包括處理靜態文件的WEB服務器,FTP服務器,Mail服務器等)。這個操作的核心步驟只有兩步,我們看下代碼:
File.read(fileDesc,buf,len); Socket.send(socket,buf,len);
我們的代碼只有兩行,看起來很簡單,但是服務器完成這個過程卻需要在用戶態和內核態之間進行4次上下文切換,也就是說在這個操作完成之前數據需要被copy 4次。下面的圖片展示了服務器是如何將數據從文件傳輸到socket的。
圖一:傳統模式下數據拷貝過程:
圖二:傳統模式下內核態和用戶態之間的上下文切換
涉及到的步驟包括:
- 調用read()方法導致了用戶態到內核態的切換(參看圖二)。在系統內部是通過sys_read()(或類似的其他方法)從文件讀取數據。第一次copy(參看圖一)是通過直接內存訪問(DMA)引擎實現的,這次copy從硬盤上讀取了文件內容并將之保存在內核空間的緩沖區中。
- 第二次copy發生在數據從內核緩沖區被copy到用戶緩沖區時,此時read()方法也返回了。read()方法的返回導致了從內核態到用戶態一次切換。現在數據是保存在用戶空間的緩沖區中。
- socket調用send()方法再次引起了用戶態到內核態的切換。第三次copy再次將數據放回到內核緩沖區。不過這次的內核緩沖區和上次的不同,這次的緩沖區和目標socket相關。
- 調用的send()方法返回時,產生了內核態到用戶態的上下文切換。這次DMA引擎將數據從內核緩沖區發送到protocol引擎,也就是第四次copy,這是一個獨立異步的操作。
使用內核緩沖區作為中間層(而不是直接將數據傳送到用戶緩沖區)可能看起來有些低效。但是最初將內核緩沖區作為中間層引入進程的目的就是提升性能。在讀取數據的時候,作為中間層的內核緩沖區的角色相當于“預讀取緩存”,也就是說如果應用請求的數據量比內核緩沖區空間小,就會將一部分數據預讀取到作為中間層的內核緩沖區中以供下一次請求使用。很顯然,在請求的數據量比內核緩沖區空間小時,這樣做可以顯著地提升應用性能。在寫數據的時候,多個中間層有助于更好地實現異步寫(先將數據寫到中間緩存,中間層快滿時再批量寫出)。
不幸的是,在請求的數據量大過內核緩沖區很多時,這種方法本身也會成為性能瓶頸:因為數據會在硬盤、內核緩沖區和用戶緩沖區之間多次拷貝。
zero copy可以排除這些多余的copy來提升性能。
zero copy方案
重新思考一下傳統的數據傳輸方案,將會發現第二次和第三次的copy行為實際上是不必要的。在傳統方案里,應用做的事情只不過是緩存數據并將之轉發到socket緩沖區,我們可以考慮直接將數據從讀緩存發送到socket緩沖區中。transferTo()方法能讓我們實現這種操作。
transferTo()方法的定義如下:
public void transferTo(long position, long count, WritableByteChannel target);
transferTo()方法可以將數據從FileChannel發送到指定的WritableByteChannel中。transferTo()方法需要依賴底層操作系統的支持才能實現zero copy。在UNIX系統和各種Linux系統中,支持zero copy的系統方法是sendfile(),這個方法可以將數據從一個文件描述符轉發到另一個文件描述符中。
sendfile()方法定義:
include <sys/socket.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);</pre>
在概述中,我們寫過兩行代碼演示傳統數據傳輸的方法,演示代碼中的file.read()和socket.send()兩個方法的調用可以替換為調用transferTo()方法,示例如下:
transferTo(position, count, writableChannel);下圖演示了調用transferTo()方法時數據傳輸的路徑:
下圖演示了調用transferTo()方法時用戶態和內核態上下文切換的過程:
調用transferTo()方法涉及到的步驟為:
- 調用transferTo()方法產生了第一次copy:DMA引擎將文件內容copy到了讀緩存中。
- 然后系統內核將數據copy到與輸出socket相關的內核緩沖區中。
- 第三次copy發生在DMA引擎將數據從內核socket緩沖區發送到protocol引擎時。
看看效果:
- 將用戶態-內核態上下文切換由四次減少到了兩次;
- 將數據的copy由四次減少到了三次(其中只有一次涉及到CPU)。
不過這樣子還沒有達到使用zero copy的目標。如果底層網卡支持收集操作的話,我們還可以去掉由內核完成的copy(即第二次copy)。在Linux Kernel2.4及以后的版本中,socket緩沖區描述符已經被調整到滿足這種需求了。這樣這個方案不僅僅是減少了上下文切換的次數,也消除了copy過程中對CPU依賴的部分。盡管用戶還是在用transferTo()方法,但是其底層行為已經發生了變化:
- 調用transferTo()方法時,DMA引擎將文件內容copy到內核緩沖區中;
- 不再將數據copy到socket緩沖區中,只是將數據描述符(包含地址信息和長度信息)追加到socket緩沖區。DMA引擎直接將數據從內核緩沖區傳遞到protocol引擎,從而消除了僅剩的CPU copy。
下圖展示了使用transferTo()方法和收集操作時copy的詳情:
構建文件服務器
現在我們練習使用一下zero copy,就演示一下文件在客戶端和服務器之間的傳遞(示例代碼下載地址見文末)。TraditionalClient.java以及TraditionalServer.java是基于傳統方案的實現,和新方法是File.read()和Socket.send()。TraditionalServer.java是一個Server端程序,它監聽著一個特定的端口以讓Client連接,每次會從socket讀取4KB數據。TraditionalClient.java連接到Server上,從一個文件中讀取(使用File.read()方法)4KB數據并通過socket將數據發送(使用Socket.send()方法)給Server。
類似的,TransferToServer.java和TransferToClient.java實現了相同的功能,不過使用的是transferTo()方法(調用了系統的sendfile()方法),將文件數據從Server端發送到了Client端。
性能比較
我們在一臺Linux Kernel版本2.6的機器上執行了示例代碼,以毫秒級的時間尺度比較了傳統方案和transferTo()方案傳輸不同大小的數據文件的速度。下表為測試結果:
File size | Normal file transfer (ms) | 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%的時間消耗。對于需要在IO channel間進行大量數據copy和傳輸的應用(比如WebServer),transferTo()可以顯著地提升性能。
總結
我們演示了使用transferTo()的性能優勢,可以看到中間緩沖區copy(即使是發生在內核中)會有一定的性能損失。對于需要進行channel間大量數據copy的應用,zero copy技術可以顯著地提升性能。
其他
本文譯自: Efficient data transfer through zero copy
來自:http://www.zhyea.com/2017/10/22/zero-copy.html