一般的IO調用
首先來看一下一般的IO調用。在傳統(tǒng)的文件IO操作中休溶,我們都是調用操作系統(tǒng)提供的底層標準IO系統(tǒng)調用函數(shù) read()代赁、write() ,此時調用此函數(shù)的進程(在JAVA中即java進程)由當前的用戶態(tài)切換到內核態(tài)兽掰,然后OS的內核代碼負責將相應的文件數(shù)據(jù)讀取到內核的IO緩沖區(qū)芭碍,然后再把數(shù)據(jù)從內核IO緩沖區(qū)拷貝到進程的私有地址空間中去,這樣便完成了一次IO操作孽尽。如下圖所示窖壕。
注意兩點:
- OS的read函數(shù)會在內核IO緩沖區(qū)中預讀取數(shù)據(jù),減少磁盤IO操作(Step2)
- Java的BufferedReader或BufferedInputStream的緩沖區(qū)的作用是減少系統(tǒng)調用(Step1)
Java的IO讀寫大致分為三種:
1、普通IO(java.io)
例如FileWriter瞻讽、FileReader等鸳吸,普通IO是傳統(tǒng)字節(jié)傳輸方式,讀寫慢阻塞速勇,單向一個Read對應一個Write 晌砾。
2、文件通道 FileChannel(java.nio)
FileChannel fileChannel = new RandomAccessFile(new File("data.txt"), "rw").getChannel()
全雙工通道快集,采用內存緩沖區(qū)ByteBuffer且是線程安全的
使用FileChannel為什么會比普通IO快贡羔?
一般情況FileChannel在一次寫入4kb的整數(shù)倍數(shù)時,才能發(fā)揮出實際的性能个初,益于FileChannel采用了ByteBuffer這樣的內存緩沖區(qū)乖寒。這樣可以精準控制寫入磁盤的大小,這是普通IO無法實現(xiàn)FileChannel是直接把ByteBuffer的數(shù)據(jù)直接寫入磁盤院溺?
ByteBuffer 中的數(shù)據(jù)和磁盤中的數(shù)據(jù)還隔了一層楣嘁,這一層便是 PageCache,是用戶內存和磁盤之間的一層緩存珍逸。我們都知道磁盤 IO 和內存 IO 的速度可是相差了好幾個數(shù)量級逐虚。我們可以認為 filechannel.write 寫入 PageCache 便是完成了落盤操作,但實際上谆膳,操作系統(tǒng)最終幫我們完成了 PageCache 到磁盤的最終寫入叭爱,理解了這個概念,你就應該能夠理解 FileChannel 為什么提供了一個 force() 方法漱病,用于通知操作系統(tǒng)進行及時的刷盤买雾,同理使用FileChannel時同樣經歷磁盤->PageCache->用戶內存三個階段
3、內存映射MMAP(java.nio)
MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, position, fileSize)
mmap 把文件映射到用戶空間里的虛擬內存杨帽,省去了從內核緩沖區(qū)復制到用戶空間的過程漓穿,文件中的位置在虛擬內存中有了對應的地址,可以像操作內存一樣操作這個文件注盈,相當于已經把整個文件放入內存晃危,但在真正使用到這些數(shù)據(jù)前卻不會消耗物理內存,也不會有讀寫磁盤的操作老客,只有真正使用這些數(shù)據(jù)時僚饭,也就是圖像準備渲染在屏幕上時,虛擬內存管理系統(tǒng) VMS
MMAP 并非是文件 IO 的銀彈胧砰,它只有在一次寫入很小量數(shù)據(jù)的場景下才能表現(xiàn)出比 FileChannel 稍微優(yōu)異的性能浪慌。緊接著我還要告訴你一些令你沮喪的事,至少在 JAVA 中使用 MappedByteBuffer 是一件非常麻煩并且痛苦的事朴则,主要表現(xiàn)為三點:
MMAP 使用時必須實現(xiàn)指定好內存映射的大小,并且一次 map 的大小限制在 1.5G 左右,重復 map 又會帶來虛擬內存的回收乌妒、重新分配的問題汹想,對于文件不確定大小的情形實在是太不友好了。
MMAP 使用的是虛擬內存撤蚊,和 PageCache 一樣是由操作系統(tǒng)來控制刷盤的古掏,雖然可以通過 force() 來手動控制,但這個時間把握不好侦啸,在小內存場景下會很令人頭疼槽唾。
MMAP 的回收問題,當 MappedByteBuffer 不再需要時光涂,可以手動釋放占用的虛擬內存庞萍,但…方式非常的詭異
OS 的 PageCache機制
PageCache是OS對文件的緩存,用于加速對文件的讀寫忘闻。一般來說钝计,程序對文件進行順序讀寫的速度幾乎接近于內存的讀寫訪問,這里的主要原因就是在于OS使用PageCache機制對讀寫訪問操作進行了性能優(yōu)化齐佳,將一部分的內存用作PageCache
1私恬、對于數(shù)據(jù)文件的讀取
如果一次讀取文件時出現(xiàn)未命中(cache miss)PageCache的情況,OS從物理磁盤上訪問讀取文件的同時炼吴,會順序對其他相鄰塊的數(shù)據(jù)文件進行預讀缺久(ps:順序讀入緊隨其后的少數(shù)幾個頁面)。這樣硅蹦,只要下次訪問的文件已經被加載至PageCache時荣德,讀取操作的速度基本等于訪問內存
1、對于數(shù)據(jù)文件的寫入
OS會先寫入至Cache內提针,隨后通過異步的方式由pdflush內核線程將Cache內的數(shù)據(jù)刷盤至物理磁盤上
對于文件的順序讀寫操作來說命爬,讀和寫的區(qū)域都在OS的PageCache內,此時讀寫性能接近于內存辐脖。RocketMQ的大致做法是饲宛,將數(shù)據(jù)文件映射到OS的虛擬內存中(通過JDK NIO的MappedByteBuffer),寫消息的時候首先寫入PageCache嗜价,并通過異步刷盤的方式將消息批量的做持久化(同時也支持同步刷盤)艇抠;訂閱消費消息時(對CommitLog操作是隨機讀取)久锥,由于PageCache的局部性熱點原理且整體情況下還是從舊到新的有序讀家淤,因此大部分情況下消息還是可以直接從Page Cache(cache hit)中讀取,不會產生太多的缺頁(Page Fault)中斷而從磁盤讀取:
PageCache機制也不是完全無缺點的瑟由,當遇到OS進行臟頁回寫絮重,內存回收,內存swap等情況時,就會引起較大的消息讀寫延遲青伤。
對于這些情況督怜,RocketMQ采用了多種優(yōu)化技術,比如內存預分配狠角,文件預熱号杠,mlock系統(tǒng)調用等,來保證在最大可能地發(fā)揮PageCache機制優(yōu)點的同時丰歌,盡可能地減少其缺點帶來的消息讀寫延遲
RocketMQ存儲優(yōu)化技術
對于RocketMQ來說姨蟋,它是把內存映射文件串聯(lián)起來,組成了鏈表立帖;因為內存映射文件本身大小有限制眼溶,只能是2G(默認1G);所以需要把多個內存映射文件串聯(lián)成一個鏈表厘惦;這里介紹RocketMQ存儲層采用的幾項優(yōu)化技術方案在一定程度上可以減少PageCache的缺點帶來的影響偷仿,主要包括內存預分配,文件預熱和mlock系統(tǒng)調用
1宵蕉、預分配MappedFile
在消息寫入過程中(調用CommitLog的putMessage()方法)愿险,CommitLog會先從MappedFileQueue隊列中獲取一個 MappedFile宋税,如果沒有就新建一個谷醉;這里一睁,MappedFile的創(chuàng)建過程是將構建好的一個AllocateRequest請求(具體做法是,將下一個文件的路徑稼稿、下下個文件的路徑薄榛、文件大小為參數(shù)封裝為AllocateRequest對象)添加至隊列中,后臺運行的AllocateMappedFileService服務線程(在Broker啟動時让歼,該線程就會創(chuàng)建并運行)敞恋,會不停地run,只要請求隊列里存在請求谋右,就會去執(zhí)行MappedFile映射文件的創(chuàng)建和預分配工作硬猫,分配的時候有兩種策略,一種是使用Mmap的方式來構建MappedFile實例改执,另外一種是從TransientStorePool堆外內存池中獲取相應的DirectByteBuffer來構建MappedFile(ps:具體采用哪種策略啸蜜,也與刷盤的方式有關)。并且辈挂,在創(chuàng)建分配完下個MappedFile后衬横,還會將下下個MappedFile預先創(chuàng)建并保存至請求隊列中等待下次獲取時直接返回。RocketMQ中預分配MappedFile的設計非常巧妙终蒂,下次獲取時候直接返回就可以不用等待MappedFile創(chuàng)建分配所產生的時間延遲
2 文件預熱 && mlock系統(tǒng)調用(TransientStorePool)
mlock系統(tǒng)調用
其可以將進程使用的部分或者全部的地址空間鎖定在物理內存中蜂林,防止其被交換到swap空間遥诉。對于RocketMQ這種的高吞吐量的分布式消息隊列來說,追求的是消息讀寫低延遲噪叙,那么肯定希望盡可能地多使用物理內存突那,提高數(shù)據(jù)讀寫訪問的操作效率。
文件預熱
預熱的目的主要有兩點:
第一點构眯,由于僅分配內存并進行mlock系統(tǒng)調用后并不會為程序完全鎖定這些內存,因為其中的分頁可能是寫時復制的早龟。因此惫霸,就有必要對每個內存頁面中寫入一個假的值。其中葱弟,RocketMQ是在創(chuàng)建并分配MappedFile的過程中壹店,預先寫入一些隨機值至Mmap映射出的內存空間里。
第二芝加,調用Mmap進行內存映射后硅卢,OS只是建立虛擬內存地址至物理地址的映射表,而實際并沒有加載任何文件至內存中藏杖。程序要訪問數(shù)據(jù)時OS會檢查該部分的分頁是否已經在內存中将塑,如果不在,則發(fā)出一次缺頁中斷蝌麸。這里点寥,可以想象下1G的CommitLog需要發(fā)生多少次缺頁中斷,才能使得對應的數(shù)據(jù)才能完全加載至物理內存中(ps:X86的Linux中一個標準頁面大小是4KB)来吩?
RocketMQ的做法是:
在做Mmap內存映射的同時進行madvise系統(tǒng)調用敢辩,目的是使OS做一次內存映射后對應的文件數(shù)據(jù)盡可能多的預加載至內存中,從而達到內存預熱的效果弟疆。
參考資料
https://my.oschina.net/u/3180962/blog/3064148
https://blog.csdn.net/linxdcn/article/details/72903422
http://www.reibang.com/p/6d0c118c17de