隨着技術的不斷進步,計算機的速度越來越快。但是磁盤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)才支持內存映射文件。
