高效IO解決方案-Mmap「給你想要的快」
隨著技術的不斷進步,電腦的速度越來越快。但是磁碟IO速度往往讓欲哭無淚,和記憶體中的讀取速度有著指數級的差距;然而由於互聯網的普及,網民數量不斷增加,對系統的性能帶來了巨大的挑戰,系統性能往往是無數技術人不斷追求的方向。
CPU,記憶體,IO三者之間速度差異很大。對於高並發,低延遲的系統來說,磁碟IO往往最先成為系統的瓶頸;為了減少其影響,往往會引入快取來提升性能。但是由於記憶體空間有限,往往只能保存部分數據;並且數據需要持久化,所以磁碟IO仍然不可避免。
無論是從HDD(機械硬碟)到SSD(固態硬碟)的硬體提升;還是從BIO(阻塞IO)到 NIO(非阻塞IO)的軟體上的提升;都使得磁碟IO效率得到了很大的提升,但是相比記憶體讀取速度仍然有著接近巨大的差距。今天筆者將介紹一種更加高效的IO解決方案Mmap(記憶體映射文件,memory mapped file)
1. 用戶態和內核態
為了安全,作業系統將虛擬記憶體劃分為兩個模組,即用戶態和內核態。它們之間是相互隔離的,即使用戶程式崩潰了也不會影響系統的運行。
用戶態和內核態包含很多複雜的概念,在此不做過多介紹。簡單來說,用戶態是用戶程式程式碼運行的地方,而內核態則是所有進程共享的空間。所以,當進行數據讀寫操作時,往往需要進行用戶空間和內核空間的交互。
傳統的IO模型進行磁碟數據讀寫時,一般大致需要2個步驟,拿寫入數據為例:1.從用戶空間拷貝到內核空間;2.從內核空間寫入磁碟。
2. Mmap是什麼
Mmap是一種記憶體映射文件的方法,即將一個文件或者其它對象映射到進程的地址空間,實現文件磁碟地址和進程虛擬地址空間中一段虛擬地址的一一對映關係.
對文件進行Mmap後,會在進程的虛擬記憶體分配地址空間,創建與磁碟的映射關係。 實現這樣的映射後,就可以以指針的方式讀寫操作映射的虛擬記憶體,系統則會自動回寫磁碟;相反,內核空間對這段區域的修改也直接反映到用戶空間,從而可以實現不同進程間的數據共享。與傳統IO模式相比,減少了一次用戶態copy到內核態的操作。
3. 性能測試
從實現原理上來看,我們可以大膽預測,Mmap的性能應該是優於傳統IO。為了儘可能保證的數據的確性,筆者使用JMH工具對傳統IO與Mmap的讀和寫進行基準測試。測試程式碼可到筆者github中獲取。
需要注意的是,筆者的測試結果並不嚴謹,真實的差距要比以下結果要明顯的多;原因在於,測試方法運行時間包含了文件的創建,內容初始化以及刪除操作所需要的時間。以下是筆者電腦的測試結果「系統:macOS 處理器:2.6GHz 六核 i7 記憶體:16G 磁碟類型:SSD」
隨機讀性能測試:
隨機寫性能測試:
從讀和寫的結果報告中都不難看出,無論是讀和寫的結果印證了我們的猜想以及理論依據,Mmap的性能要遠優於傳統IO,而在Java中傳統IO中的NIO又優於BIO。
4.Mmap在RocketMQ中的應用
RocketMQ是一個分散式消息和流平台,具有低延遲、高性能和可靠性、萬億級容量和靈活的可伸縮性。那麼問題來了,對於海量消息的處理它是怎麼保證高性能和可靠性的呢?
- RocketMQ的大致執行流程
RocketMQ中消息生產, 存儲和消費流程大致可以分為以下幾個流程:
- 生產者發送消息到Broker「消息中轉角色,負責存儲,轉發消息」
- Broker中將消息存儲在CommitLog中,並在對應的ConsumerQueue中寫入消息的commitLogOffset,msgSize,tagCode等資訊「消息在CommitLog中的位置,大小,以及標籤資訊」
- 消費者從對應的ConsumerQueue中讀取到消息的資訊,根據消息的位置從CommitLog中讀取消息體,然後進行消費
- RocketMQ中的Mmap
CommitLog是消息主體以及元數據的存儲主體,存儲Producer端寫入的消息主體內容,消息內容是不定長的。單個CommitLog文件大小是固定的,默認1G ;文件名長度為20位,左邊補零,剩餘為起始偏移量,比如00000000000000000000代表了第一個文件,起始偏移量為0,文件大小為1G=1073741824;當第一個文件寫滿了,第二個文件為00000000001073741824,起始偏移量為1073741824,以此類推。消息主要是順序寫入日誌文件,當文件滿了,寫入下一個文件。
消息存儲在CommitLog文件中,每個消費者消費消息時,都是根據消息在文件中偏移量, 大小去讀取消息。讀取消息的過程伴隨著隨機訪問讀取,嚴重影響性能。RocketMQ主要通過Mmap技術對CommitLog文件進行讀寫,將對文件的操作轉化為直接對記憶體地址進行操作,從而極大地提高了文件的讀寫效率。
正因為需要使用記憶體映射機制,故RocketMQ的文件存儲都使用定長結構來存儲,方便一次將整個文件映射至記憶體。
5.Q&A
- Mmap為什麼那麼快?
使用Mmap對文件的讀寫操作跨過內核空間,減少1次數據的拷貝,進而提高了文件IO效率。
- 相比磁碟空間,記憶體那麼小,Mmap操作是不是很佔用記憶體空間?
需要注意的是,進行Mmap映射時,並不是直接申請與磁碟文件一樣大小的記憶體空間;而是使用進程的地址空間與磁碟文件地址進行映射,當真正的文件讀取是當進程發起讀或寫操作時。
當進行IO操作時,發現用戶空間內不存在對應數據頁時(缺頁),會先到交換快取空間(swap cache)去讀取,如果沒有找到再去磁碟載入(調頁)。
- Mmap有哪些應用場景?
進程間通訊:從自身屬性來看,Mmap具有提供進程間共享記憶體及相互通訊的能力,各進程可以將自身用戶空間映射到同一個文件的同一片區域,通過修改和感知映射區域,達到進程間通訊和進程間共享的目的。
大數據高效存取: 對於需要管理或傳輸大量數據的場景,記憶體空間往往是夠用的,這時可以考慮使用Mmap進行高效的磁碟IO,彌補記憶體的不足。例如RocketMQ,MangoDB等主流中間件中都用到了Mmap技術;總之,但凡需要用磁碟空間替代記憶體空間的時候都可以考慮使用Mmap。
- Mmap有什麼缺點?
記憶體映射文件需要在進程的佔用一塊很大的連續邏輯地址空間。對於Intel的IA-32的4GB邏輯地址空間,可用的連續地址空間遠遠小於2—3 GiB。
一旦使用記憶體關聯文件,在程式運行期間,程式的執行可能受關聯文件的錯誤影響。相關聯的文件的I/O錯誤(如可拔出驅動器或光碟機被彈出,磁碟滿時寫操作等)的記憶體映射文件會嚮應用程式報異常;而通常的記憶體操作是無需考慮這些異常的。
有記憶體管理單元(MMU)才支援記憶體映射文件。