隨著技術的不斷進步,計算機的速度越來越快。但是磁碟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)才支援記憶體對映檔案。