深入分析零拷貝的原理,徹底掌握Netty、Kafka、RocketMQ高效率讀寫的祕訣

一、前言

衆所周知,常見的如NIO、Netty、Kafka、RocketMQ這些框架保持高效率讀寫的一個原因就是使用了零拷貝技術。如果想要深入研究這些框架,就需要先掌握零拷貝的原理。

比如kafka爲什麼可以如此高效的讀寫數據,主要有如下三個原因:

  1. kafka本身是分佈式集羣,同時又採用了分區技術,具有較高的併發度。

  2. 數據可以順序寫入磁盤,Kafka 的 producer 生產數據,要寫入到 log 文件中,寫的過程是一直追加到文件末端,爲順序寫。

  3. 使用了零拷貝技術。

這三個原因裏,前兩個比較好理解,一方面因爲分佈式集羣模式同時又採用了分區技術,可以有很好的併發讀寫數據能力;另一方面在kafka官網也提供了數據表明在同樣的磁盤下順序寫能到 600M/s,而隨機寫只有 100K/s,這 與磁盤的機械機構有關,順序寫之所以快,是因爲其省去了大量磁頭尋址的時間

但是最後一個零拷貝技術就不是這麼容易理解了,下面讓我們一起追本溯源,從操作系統底層開始探究零拷貝的奧祕!

二、糾正一些網絡上流傳的錯誤說法
2.1、錯誤說法一:零拷貝就是零複製或者零拷貝技術沒有進行數據的複製操作,所以比較快。

從拷貝這個音譯詞彙的翻譯來看,拷貝確實就是複製的意思,但是零拷貝絕對不是零複製,零複製很容易讓人以爲它進行了0次複製,其實是不對的。下面讓我們看下維基百科的解釋:

"Zero-copy" describes computer operations in which the CPU does not perform the task of copying data from one memory area to another.

翻譯過來就是說“零拷貝”是指計算機操作的過程中,CPU不需要爲數據在內存之間的拷貝消耗資源,這個時候cpu可以幹別的事情,至於數據的複製次數只能降低,而不會減少到0(後面會用例子說明)。

到這裏第一個錯誤說法就更正了:零拷貝不是指0次複製而是指0次調用CPU消耗資源。

2.2、錯誤說法二:DMA技術誕生後,零拷貝基於DMA,實現了絕對的零複製…

首先這個觀點直白來講並沒有錯誤,零拷貝技術確實基於DMA才能實現。但是很多文章講零拷貝都會先講傳統的文件輸入輸出,然後引出DMA,最後得出類似的結論。有兩個容易讓人弄混的點需要強調一下:

  1. 即使是傳統的read和write也會用到DMA,DMA並不神祕;
  2. 和錯誤說法一一樣,零拷貝不是0次複製。
三、並不神祕的DMA

DMA全稱是Direct Memory Access,也就是直接存儲器訪問。它允許不同速度的硬件裝置來溝通,而不需要依賴於 CPU 的大量中斷負載。通俗點理解,就是讓硬件可以跳過CPU的調度,直接訪問主內存

比如我們常見的磁盤控制器、顯卡、網卡、聲卡都是支持DMA的,可以說DMA已經徹底融入我們的計算機世界了。

從零拷貝技術的角度來看,DMA的知識我們瞭解這麼多就足夠了,不需要關心它內部是怎麼處理和實現的。

四、從操作系統理解用戶空間和內核空間

如果想要真正弄明白零拷貝技術,就要先熟悉用戶空間內核空間的概念,而熟悉用戶空間和內核空間就需要先掌握用戶態內核態的概念,而熟悉用戶態和內核態就需要從操作系統說起。
禁止套娃

4.1、操作系統

操作系統是由"硬件"和"軟件"兩部分組成。

硬件很好理解,以計算機系統爲例,計算機硬件包括一個或多個處理器(CPU)、內存、鍵盤、顯示器、磁盤、I/O接口以及其他一些外圍設備比如打印機,繪圖儀等等。 總之,計算機硬件部分是一個由多種電子和機械設備組成的硬件系統。

不是每個人都掌握所有這些硬件的,對於大部分人來說,這麼多亂七八糟的硬件放在一起,根本不可能熟練的去使用,那怎麼辦呢?於是,爲了讓所有人都能方便正確的使用這些硬件設備,就需要編寫若干程序來管理它們,正是這些程序組成了計算機的軟件系統。

軟件也可以分爲兩大類:系統軟件和應用軟件。工程師們首先直接在硬件上加載一層程序,用它來管理整個計算機硬件設備以及一些軟件信息資源,同時還爲用戶提供開發應用程序的環境,這就是操作系統軟件和實用軟件

應用軟件是在操作系統支持下,爲實現用戶要求而編制的各種應用程序。

它們的關係可以概括爲下圖這樣:
操作系統

整體的藍圖誕生後,就需要定義一些概念了:

首先CPU、內存和I/O接口組成的主設備稱爲主機,把沒有加載操作系統的主機叫做裸機。裸機與操作系統軟件的接口是由CPU的指令系統和廠商提供的系統BIOS組成。

由於操作系統向用戶隱藏了系統使用的硬件設備,因此操作系統要爲它上面的應用系統軟件提供一組命令或系統調用接口供用戶程序使用。比如我們需要使用磁盤,可以通過系統命名或系統調用來間接完成,而不需要親自手動編寫一個磁盤設備驅動程序。因此對於用戶來說,當計算機加載操作系統後,用戶不直接與計算機硬件打交道,而是利用操作系統提供的命令和功能區使用計算機。

由於操作系統處於硬件和軟件的中央位置,因此我們可以把操作系統稱爲計算機系統軟件的核心,簡稱核心內核

4.2、用戶態和內核態

知道了操作系統的大概組成之後,我們要思考一個問題:怎麼讓操作系統區分”普通用戶“和”超級管理員“,也就是怎麼給操作系統加一個“兒童模式” ?既能讓不關心內核的普通用戶可以任性的幹自己的事情而不損害內核,又能讓內核系統能任性的執行一系列特權指令。

於是,用戶態和內核態誕生了。

從系統安全和保護的角度出發,在進行計算機體系結構設計時,處理機的執行模式一般設定爲兩種:分別稱爲內核模式(內核態)和用戶模式(用戶態)

  1. 內核態:當處理機處於內核模式執行時,意味着系統除了可以執行一般指令外,還可以執行特權指令,即可以執行訪問各種控制寄存器的指令、I/O指令以及程序狀態字。

  2. 用戶態:當處理機處於用戶模式執行時,只能執行一般指令,而不允許執行特權指令。

這樣做可以保護核心代碼不受用戶程序有意和無意的攻擊。可以肯定的是,在系統運行期間需要在內核模式和用戶模式之間不斷的進行切換。

4.3、用戶空間和內核空間

理解了用戶態和內核態之後,用戶空間和內核空間就可以用一句話概括了:用戶空間就是用戶進程所在的內存區域,處於用戶態的程序只能訪問用戶空間;內核空間就是操作系統佔據的內存區域,處於內核態的程序可以訪問用戶空間和內核空間。

五、零拷貝究竟是什麼?

既然已經弄明白了DMA、用戶空間、內核空間,那零拷貝就會變的很容易理解了。下面我們從一個問題出發,層層遞進的來分析。

5.1、初現端倪

問題:如下是最傳統的read和send文件處理操作僞代碼,一共有幾次數據複製和CPU調用?

File.read(file, buf, len);
Socket.send(socket, buf, len);

回答之前先來看一下數據傳遞的過程:
數據傳遞1
答案是:4次數據複製,2次cpu調用
沒想到吧,其實基礎的read和send也用到了DMA技術,不過這個過程是不消耗cpu的。

下面爲了加深理解,我們詳細解釋下這4步:

  1. 首先應用程序中調用read() 方法,這裏會涉及到一次上下文切換(用戶態->內核態),底層採用DMA技術讀取磁盤的文件,並把內容存儲到內核空間的讀取緩存區。

  2. 通過前面的學習,我們知道處於用戶空間的應用程序無法讀取內核空間的數據,如果應用程序要操作這些數據,必須把這些內容從讀取緩衝區拷貝到用戶緩衝區。這個時候,read() 調用返回,且引發一次上下文切換(內核態->用戶態),現在數據已經被拷貝到了用戶地址空間緩衝區。(其實這個時候應用程序可以根據需求操作修改這些內容)

  3. 我們最終目的是把這個文件內容通過Socket傳到另一個服務中,調用Socket的send()方法,這裏又涉及到一次上下文切換(用戶態->內核態),這時候,文件內容被進行第三次拷貝,被再次拷貝到內核地址空間緩衝區,但是這次的緩衝區與目標套接字相關聯,與讀取緩衝區沒有任何關係。

  4. send()調用返回,引發第四次的上下文切換,同時進行第四次的數據拷貝,通過DMA把數據從目標套接字相關的緩存區傳到相關的協議引擎進行發送。

通過上面的詳細分析,不難明白,過程1和4是由DMA負責,並不會消耗CPU,只有過程2和3的拷貝需要CPU參與,但是整個過程還是需要進行4次的數據複製。

5.2、優化數據傳輸流程

當我們面對海量的數據需要處理時,頻繁的數據複製會浪費很多資源和時間,那上一小節的數據傳輸能不能進行優化呢?當然可以。

比如,當我們在應用程序中,不需要操作內容,過程2和3就是多餘的,如果可以直接把內核態讀取緩存衝區數據直接拷貝到套接字相關的緩存區,就可以起到優化的作用,如下圖:
數據傳輸2

經過上圖的優化,目前數據的複製次數從4次減少到了3次(2次DMA複製,1次cpu複製),並且上下文的切換次數也從4次減少到了2次。

其實這種優化並非空穴來風,在Java NIO裏,FileChannel的transferTo() 方法就可以實現這個過程,該方法將數據從文件通道傳輸到給定的可寫字節通道。對應的可以把file.read()socket.send() 替換爲 transferTo() "

public void transferTo(long position, long count, WritableByteChannel target);

ps:其實在linux系統和unix系統裏,這個調用被傳遞到 sendfile() 系統調用中,這樣就實現了優化將數據從一個文件描述符傳輸到了另一個文件描述符。

5.3、零拷貝成型

我們知道零拷貝粗略來說就是0次CPU調用的意思,那上一小節,還是調用了1次cpu來複制數據,顯示還達不到真正的零拷貝,那怎麼優化呢?

到這裏,如果還想優化,就需要底層網絡接口支持收集操作纔可以。
所以在Linux 內核 2.4 及後期版本中,工程師們針對套接字緩衝區描述符做了相應調整,使得DMA自帶了收集功能,當然對於用戶方面,用法還是一樣的,但是內部操作已經發生了改變。過程如下圖:
數據傳輸3
這時,數據的傳輸只需要兩步就能搞定:

  1. transferTo() 方法引發 DMA 將文件內容複製到內核讀取緩衝區。

  2. 把包含數據位置和長度信息的描述符追加到套接字緩衝區,避免了內容整體的拷貝,DMA 引擎直接把數據從內核緩衝區傳到協議引擎,全程都是DMA參與,從而消除CPU參與的數據複製消耗。

這樣當傳輸一份數據到一個設備時使用零拷貝技術就可以只對數據複製2次,CPU調用0次,上下文切換2次。

六、總結

總體來說,零拷貝是指將數據直接從磁盤文件複製到內核讀取緩衝區,然後多個消費者可以共用一個緩衝區,然後DMA 引擎直接把數據從內核讀取緩衝區傳到消費者,全程都是DMA參與,從而消除CPU參與的數據複製消耗,也不需要經由應用程序之手,減少了內核空間和用戶空間之間的上下文切換,同時也大大減少了數據複製的次數。

以kafka爲例,如果有100個消費者消費一份數據,在普通的數據傳輸方式下,複製次數一共是100*4 = 400次,cpu調用次數一共是100*2 = 200次;而使用了零拷貝技術之後,複製次數一共是100+1 = 101次(1次數據從磁盤到內核讀取緩衝區,100次發送給100個消費者消費),cpu調用次數一共是0次。極大的提高了數據讀寫效率。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章