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.傳統的上下文切換
涉及的步驟是:
使用中間內核緩沖區(而不是將數據直接傳輸到用戶緩沖區)可能看起來效率低下。但是中間內核緩沖區被引入進程來提高性能。在讀取端使用中間緩沖區允許內核緩沖區充當“預讀緩存”,當應用程序沒有要求與內核緩沖區一樣多的數據時。當請求的數據量小于內核緩沖區大小時,這會顯著提高性能。寫入側的中間緩沖區允許寫入異步完成。
不幸的是,如果所請求數據的大小遠遠大于內核緩沖區的大小,這種方法本身可能會成為性能瓶頸。在磁盤,內核緩沖區和用戶緩沖區最終傳遞到應用程序之前,數據被復制多次。
零拷貝通過消除這些冗余數據副本來提高性能。
數據傳輸:零拷貝方法
如果您重新檢查傳統方案,則會注意到第二個和第三個數據副本實際上不是必需的。除了緩存數據并將其傳回到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中使用時所采取的步驟如下:
這是一個改進:我們已經將上下文切換次數從四次減少到兩次,并將數據副本的數量從四個減少到三個(其中只有一個涉及CPU)。但是這還沒有使我們達到零拷貝的目標。如果底層網絡接口卡支持收集操作,我們可以進一步減少內核的數據重復。在Linux內核2.4和更高版本中,套接字緩沖區描述符被修改以適應這個要求。這種方法不僅減少了多個上下文切換,還消除了需要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.性能比較:傳統方法與零拷貝
| 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()相比于從一個通道讀取并將相同數據寫入另一個通道的性能優點?。中間緩沖區副本 - 即使是那些隱藏在內核中的副本 - 也會有可測量的成本。在通道之間大量復制數據的應用程序中,零復制技術可以顯著提高性能。
總結
以上是生活随笔為你收集整理的kafka通过零拷贝实现高效的数据传输的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 转载-Qualcomm MSM8953启
- 下一篇: Unity 3D光源-Direction