Terence
Terence

Let's geek and art.

mmap 和disk I/O 在不同情形下的性能分析

mmap(memory mapped file)從字面意思來看,似乎是在節省每次同disk 做I/O 的巨額開銷(expensive overhead),通過把main memory 作為cache 來提升disk I/O 的效率。

但這樣的認知完全是錯的,因為OS(operating system)已經為disk I/O 提供了page cache 這樣的機制,來實現上述基於main memory 的cache 優化

如此,我們不免要進一步地追問,那mmap 到底為何而存在?它能提供哪些優化?在什麼樣的場景下,mmap 會為program 帶來更好的性能?而什麼情形下引入mmap 反而會給program 帶來更大的overhead ?

如上所述,由於page cache 的存在,mmap 所提供的cache 功能並不能作為其performance 由於disk I/O 的理由。於是,我們需要分別考察這兩種機制的data pipeline。

對於mmap 來講,它通過system call mmap()將disk partition 映射為某段virtual memory address。需要注意的是,mmap 採用了copy-on-write 的lazy execution 優化機制,並不會在第一次的mapping 階段就分配出physical memory address。它要等到第一次對這段virtual memory address 的訪問時,通過引發page fault 這樣的system exception,來間接觸發physical memory address 的分配。

也即是,每一次對新的disk part 的映射,都會在它第一次access 時引發一次page fault。這里之所以要單獨提出page fault 這件事,是因為page fault 通常具有high expensive overhead,甚至會高過下面會提到的user/kernel space 之間的data copy。

對於disk I/O 來講,它首先需要為disk 分配出相應的main memory cache,也即是page cache,供disk I/O 做batch read/write 的優化使用。顯然,對於OS 來講,disk 作為外部的hardware resource 只能在kernel space 做交互。而page cache 需要同disk 做直接交互,這就意味著page cache 是kernel space 下的data container。

對於application 來講,它們只能通過system call read()write()來觸發對disk 的交互,而它們又在user space 下,這就意味著page cache 這樣的在kernel space 下的數據需要被copy 到user space 下的application data buffer。 read()需要從kernel space --> user space 的copy,而write()需要user space --> kernel space 的copy。

如此,如果我們單純考察對於data 的使用,即:data read 的部分,那麼mmap 和disk I/O 這兩種機制,分別會在什麼樣的情形下,會讓各自具備更好的performance 呢?

面對engineering problem,“哪種方案更好” 通常不是一個好的問題的提法。更好的提問方式,應該是詢問各自會在什麼樣的情形下展現出更好的performance。

後面這種提法,更容易讓你保持open mind,更深入細緻地弄清楚每種解決方案的使用情景、邊界與適用機制,而不是無腦地神吹某種特定情形下的妥協方案。

須知,在工程領域,永遠只有基於特定情形下的balance,沒有silver bullet。

情形0x01

假設我們每次讀入的都是新數據,即:new disk data,那麼這就意味,對於這些new data 的使用,mmap 每一次都需要承受page fault 帶來的overhead,而disk I/O 需要承受read()帶來的user/kernel space copy 的overhead。由於每次都是新數據,顯然這些讀入的數據都無法被復用,每一次都會周而復始地承擔這兩種開銷。

之前我們提到過,page fault 的overhead 是相當巨大的,通常會比user/kernel space copy 的overhead 來得高(可參考Linus 在email-list 中的回答: http://lkml.iu.edu/hypermail/linux/kernel/0802.0/1496.html)。於是,在這種情形下,每一次的data access 都意味著disk I/O 會被mmap 來得高效。因此,這種情形下disk I/O 一定是好得多的解決方案。

情形0x02

基於此,我們再考慮「情形0x01」的變形:不是每次讀取新的數據,而是不斷地重複讀取相同disk 的數據(假設讀取的這部分數據可以被一個memory page 容納;不能容納的情形,我們後面會繼續分析討論)。我們再來分析兩種機制下的data pipeline。

對於mmap 來講,由於每次access 的都是同樣的disk data part,那麼它只需要承擔第一次的page fault overhead,後面的access 都可直接復用已經在memory 中的數據,即變成了較為高效的memory access。所以,mmap 的N 次access 的overhead 就本等價於:1 次page fault + (N-1) 次memory access。

而對於disk I/O 來講,每次都調用read() system call 來訪問同樣的數據(當然,你可以argue,既然是同樣的數據訪問,為啥不使用一次disk I/O,再將後續它保存到某個variable 中,以便在memory 中做復用?對於某些應用場景來講,你並不能一開始就做出如此的判斷;再來是,這樣的優化屬於application layer 的優化,而我們當前僅集中於底層數據流的討論。 ),就意味著每次都需要承擔user/kernel space copy 的overhead。於是,N 次access 的overhead 就是:N 次user-kernel-data-copy。

顯然,當N 大於等於2 時,mmap 的performance 會比disk 好得多。

情形0x03

再來,讓我們考慮「情形0x02」的變形:同樣是不斷地讀取同一份數據,但access 它的頻率卻非常低,例如每天一次。

表面上看,這種情形所經歷的data pipeline 應該同「情形0x02」完全一樣。可是,memory 的資源是非常寶貴的,OS 對memory 的使用有相應的優化。當某塊memory 長久不被使用後,它將被OS swap off 到disk 中,將空出來的memory 供其他task 使用。

此時,mmap 出的disk 部分因為長久未使用的關係,每次都會被swap off 到disk。而再使用它時,又必須重新加載到memory,再經歷page default interrupt 等流程。此時,「情形0x03」就等價為了「情形0x01」,即:每次access,mmap 要經歷page default interrupt,而disk I/O 要經歷user/kernel space data copy。那麼自然,disk I/O 成了更好的選擇。

情形0x04

最後,讓我們考慮更為微妙的一種情形,「情形0x02」的變形:同樣是不斷地讀取同一份數據,但這份數據比較大,或者是memory pressure 比較大,以至於會觸發OS 對部分memory 的swap off 的操作。

此時,memory 不得不將這一份data 分批導入到memory 來做access。這種情形的overhead 就較為微妙,因為它會根據memory pressure 的狀態,時不時地觸發由於swap off 而引發的mmap 的再次page default interrupt。

此時,相比於每一次disk I/O 都需要經歷user/kernel space data copy,到底誰的overhead 更大?我們不得而知,這取決於具體的computer hardware 和runtime memory pressure:

  • 當間歇性的page default interrupt 的overhead 大於每一次user/kernel space data copy overhead 時,disk I/O 是更好的選擇;
  • 而當間歇性的page default interrupt overhead 小於user/kernel space data copy overhead 時,mmap 是更好的選擇。

綜上所述,mmap 和disk I/O 到底在什麼樣的情形下的overhead 更低,取決於兩部分:

  • Part I :mmap 「首次」訪問所引發的page default interrupt overhead。
  • Part II :disk I/O 「每次」訪問的user/kernel space data copy overhead。

降低「Part I」的overhead 的方式,可以是在memory 中復用數據,同時避免因為低頻使用、memory pressure 而引起的swap off,進而引發再次page default interrupt overhead。

但如果無法明顯分辨出「Part I」和「Part II」誰的overhead 更大,則只能通過多次測試的「經驗數據」來做指導,並沒有什麼通用的、確定的理論指導。


Remarks

1、有些材料上講,Kafka 的高吞吐量來自於使用了mmap,這是不正確的。 Kafka 僅將log index file 做成了mmap,而並未將存儲原始數據的log file 做成mmap。

由上述討論可知,如果將log file 做成mmap,則在寫入階段會不斷地引發page default interrupt,其效率反而會遠遠低於disk I/O。如果我們非要討論數據持久化部分的高吞吐,那一定是來自於OS 的page cache 機制,讓disk I/O 變得高效。 mmap 在這種情形下只會起到相反的作用。

由於有了page cache 的存在,正常的disk I/O 通常能夠在大部分情況下成為較為優秀的解決方案。

2、使用mmap 做優化的一個優秀例子是 ElasticSearch [1] ,它 使用mmap 來存放index [2] (對應的disk 文件稱之為segment)。

ElasticSearch 作為搜索引擎,最重要的一個data structure 便是inverted index,它需要被持久化到disk 中做保存[3] 。但顯然,使用inverted index 的頻率也是極高的,每次搜索都需要inverted index 來完成。如此,如果一直使用disk I/O 來做read 操作,顯然是極其低效的。

於是,ElasticSearch 選擇將segment disk 映射為mmap 來做優化。此時,官方文檔中特別指出,需要disable OS 的swapping 功能[4] 。如果從本文的視角來看,停止OS 的swap off 操作是極其必要的。因為一旦segment 被swap 到disk,當它再被reload 到memory 時,就需要經歷disk page default interrupt 的overhead。而它會遠遠高於普通的disk I/O。

3、mmap 除了能夠在特定情形下提供數據復用的performance improvement,還能作為process 之間通信的橋樑。因為它將disk 所映射到的memory,並不私有於任何特定的process,成了各個process 之間的share data。

也因為此,如果以cache 的視角去考察它,它也不會因為某個特定application 的重啟(進而導致process 的kill 和重分配)而讓cache 失效,省去了cache 從cool 到warm 的overhead。

(更適合在PC 上閱讀的格式,可點擊「閱讀原文」)


引用鏈接

[1] ElasticSearch: https://www.elastic.co/guide/en/elasticsearch/reference/6.3/index.html

[2]使用mmap 來存放index: https://www.elastic.co/guide/en/elasticsearch/reference/6.3/vm-max-map-count.html

[3]被持久化到disk 中做保存: https://www.yuque.com/terencexie/geekartt/es-index-store

[4]需要disable OS 的swapping 功能: https://www.elastic.co/guide/en/elasticsearch/reference/6.3/setup-configuration-memory.html



近期回顧

Notes:對話李錄,第十三屆哥倫比亞大學中國商業論壇

「理性投資」被當做玄學的一個緣由

reverse problem 的不同應用

 


如果你喜歡我的文章或分享,請長按下面的二維碼關注我的微信公眾號,謝謝!



CC BY-NC-ND 2.0 版權聲明

喜歡我的文章嗎?
別忘了給點支持與讚賞,讓我知道創作的路上有你陪伴。

載入中…

發布評論