系統性能優化-4 磁盤
磁盤作為計算機中速度最慢的硬件之一,常常是系統的性能瓶頸,優化磁盤一般能得到明顯的提升~
文章以如何高效的傳輸文件來討論針對磁盤的優化技術,如零拷貝、直接 IO、異步 IO等。
最簡單的網絡傳輸
最簡單的方式的當然是找到文件存儲的路徑,創建一個固定的大小的緩沖區,每次讀取緩沖區大小的文件內容,再通過網絡 API 發送給客戶端,重復上述流程直到把文件發送完成。
順便一提,緩沖區的設置還是有必要的,java 有單字節的讀取寫入方式(FileInputStream.read()),這樣會很慢,緩沖區可以有效的提升讀取文件的性能。
但是這樣的性能依舊不是很高,原因是這需要進行多次的內容拷貝,會導致大量的上下文切換
- 每次讀取發送都經歷了 4 次用戶態與內核態的上下文切換
- 每次發送的緩存區內容被拷貝了 4 次
接下來就從減少上下文切換次數
和減少內存拷貝次數
來進行優化
零拷貝
讀取磁盤或者操作網卡都由操作系統內核完成。內核負責管理系統上的所有進程,它的權限最高,工作環境與用戶進程完全不同。只要我們的代碼執行 read 或者 write 這樣的系統調用,一定會發生 2 次上下文切換
:首先從用戶態切換到內核態,當內核執行完任務后,再切換回用戶態交由進程代碼執行。
因此如果要減少上下文切換次數,就要減少系統調用次數
。解決方案就是把 read 和 write 系統調用合二為一,直接在內核中完成文件的讀取和發送,可以把原本的 4 次上下文切換減少為 2 次。
那如何減少內存拷貝次數呢?對于常見的文件下載場景,我們并不需要進程對文件進行處理,如添加公司信息等,那么用戶緩沖區就沒有存在的必要,直接由 PageCache 拷貝到 Socket 緩沖區就可以,這可以把原本的 4 次內容拷貝縮短為 3 次,
如果 網卡支持 DMA SG-DMA(The Scatter-Gather Direct Memory Access),那么 PageCache 可以直接拷貝到網卡,可以再減少一次內存拷貝。
其實這就是零拷貝技術,它是操作系統提供的新函數,同時接收文件描述符和 TCP socket 作為輸入參數
,這樣執行時就可以完全在內核態完成內存拷貝
,既減少了內存拷貝次數,也降低了上下文切換次數。
零拷貝使用戶無需關心 socket 緩沖區的大小(因為 socket 緩沖區是動態變化的,它既用于 TCP 滑動窗口,也用于應用緩沖區,還受到整個系統內存的影響)。綜合種種優點,零拷貝可以極大的提升文件傳輸性能。
PageCache 磁盤高速緩存
正常讀取文件時,是先把磁盤文件拷貝到 PageCache 上,再拷貝到進程中。原因是磁盤的讀取速度是最慢的,而在內存中的 PageCache 速度就會快很多,那選擇哪些數據復制到內存呢?根據時間局部性
原理(剛被訪問的數據在短時間內再次被訪問的概率很高),用 PageCache 緩存最近訪問的數據,當空間不足時淘汰最久未被訪問的緩存(即 LRU 算法)。讀磁盤時優先到 PageCache 中找一找,如果數據存在便直接返回,這便大大提升了讀磁盤的性能。
對于機械硬盤來說,需要旋轉磁頭到數據所在的扇區,再開始順序讀取數據。其中,旋轉磁頭耗時很長,為了降低它的影響,PageCache 使用了預讀功能。也就是即使你可能目前只讀 32KB 數據,但內核會把后續的部分數據也讀取到 PageCache,因為這個讀取成本很低,而如果后續一段時間訪問到了這些數據,帶來的收益是很值得的。
PageCache 在 90% 以上場景下都會提升磁盤性能,但在某些情況下,PageCache 會不起作用,甚至由于多做了一次內存拷貝,造成性能的降低。
先說結論:在讀取大文件時,不應使用 PageCache,進而也不應使用零拷貝技術處理。
當用戶訪問大文件時,內核就會把它們載入到 PageCache 中,這些大文件很快會把有限的 PageCache 占滿。一方面這些大文件被再次訪問的概率其實很低,耗費 CPU 多拷貝到 PageCache 一次;另一方面大文件占用 PageCache 會導致熱點小文件無法被加載到 PageCache 中,讀取的速度變慢。
- 比如視頻文件通常按時間順序播放,播放器只會按需加載一定長度的視頻數據。雖然文件本身很大,但只有少部分數據會在某一時間點內被訪問到,其余的數據部分在播放過程中可能根本不會被訪問到。
- 某些大數據庫文件也是按順序或按塊讀取的,而數據庫的查詢操作通常集中在特定區域或范圍內。大文件的其他部分可能在較長時間內不會被訪問,導致它們在 PageCache 中的緩存效果差。
那么高并發場景下該怎么處理大文件呢?
異步 IO + 直接 IO
高并發場景處理大文件時,應當使用異步 IO 和直接 IO 來替換零拷貝技術。
當調用 read 方法讀取文件時,實際上 read 方法會在磁盤尋址過程中阻塞等待,導致進程無法并發地處理其他任務,如下圖所示:
異步 IO(異步 IO 既可以處理網絡 IO,也可以處理磁盤 IO,這里我們只關注磁盤 IO)把讀操作分為兩部分,前半部分向內核發起讀請求,但不等待數據就位就立刻返回,此時進程可以并發地處理其他任務。當內核將磁盤中的數據拷貝到進程緩沖區后,進程將接收到內核的通知,再去處理數據,這是異步 IO 的后半部分。如下圖所示:
異步 IO 并沒有把數據拷貝到 PageCache 中,這其實是異步 IO 實現上的缺陷。經過 PageCache 的 IO 我們稱為緩存 IO
,它與虛擬內存系統耦合太緊,導致異步 IO 從誕生起到現在都不支持緩存 IO。繞過 PageCache 的 IO 是個新物種,我們把它稱為直接 IO
。對于磁盤,異步 IO 只支持直接 IO。
直接 IO 的應用場景為:
- 應用程序已經實現了磁盤文件的緩存,不需要 PageCache 再次緩存,引發額外的性能消耗。比如 MySQL 等數據庫就使用直接 IO
- 高并發下傳輸大文件
缺點為無法享受 PageCache 造成的性能提升(內核會緩存盡量多的連續IO在 PageCache 中,合并為一個更大的 IO 發給磁盤,減少磁盤的尋址操作;內核也會預讀后續的 IO 放在 PageCache 中,減少磁盤操作)
總結
零拷貝技術基于 PageCache(緩存最近讀的數據),合并讀取和發送的系統調用,能夠有效減少傳輸文件過程中的上下文切換次數和內存拷貝次數,同時最大程度利用 socket 緩沖區。但缺點是用戶進程無法對文件做任何修改,比如壓縮后再發送。當文件大小超過某個閾值后,PageCache 還可能引發副作用,因此,實踐中通常會設定一個文件大小閾值,針對大文件使用異步 IO 和直接 IO,而對小文件使用零拷貝
( 例如 Nginx 的 directio 指令)。
文件傳輸場景中的優化可以大概分為三個方向:
- 減少磁盤工作量(PageCache)
- 減少 CPU 工作量 (直接 IO)
- 提高內存利用率(零拷貝)