OS近距離:mmap給你想要的快!
I/O問題一般不會(huì)被大多數(shù)人關(guān)注,因?yàn)榇蠖鄶?shù)開發(fā)都是在做“業(yè)務(wù)”,也就是在搞計(jì)算節(jié)點(diǎn)的事情,通常遇到的I/O問題,也就是日志打的有點(diǎn)多了,磁盤寫起來有點(diǎn)吃力,所以iowait這個(gè)指標(biāo),關(guān)注的人也不多。
可惜的是,工作并不是只考慮怎么折騰CPU,數(shù)據(jù)總歸要落地的。一旦涉及到高性能的磁盤存儲(chǔ),I/O問題就浮上水面。Redis這么流行,就是為了繞開磁盤性能問題而存在的。
換句話說,如果我的磁盤像內(nèi)存一樣快,那還要內(nèi)存干什么~
今天,我們就來簡單聊一下I/O。當(dāng)然,更主要的還是傾向于和持久化打交道的磁盤I/O。如非特指,我們說的就都是它。
I/O都干了些啥?
I/O干的是啥?對我們使用者來說,簡單來講,就兩點(diǎn)。
- 和操作系統(tǒng)索要數(shù)據(jù),并加載到緩沖區(qū)中。
- 填滿用戶進(jìn)程緩沖區(qū),并交給操作系統(tǒng)刷盤。
但也不是直接讀寫,因?yàn)椴僮飨到y(tǒng)有內(nèi)核進(jìn)程和用戶進(jìn)程之分。為了保護(hù)內(nèi)核的內(nèi)存,用戶進(jìn)程是不能隨便去讀內(nèi)核所操作的數(shù)據(jù)的。想要數(shù)據(jù),拷貝一份。
這一進(jìn)一出,就涉及到用戶態(tài)和內(nèi)核態(tài)的切換,也就是常說的系統(tǒng)調(diào)用,細(xì)節(jié)上肯定會(huì)比較復(fù)雜。
如上圖,就拿讀數(shù)據(jù)來說,我們可以把讀取過程分為以下幾個(gè)階段。
- Java進(jìn)程發(fā)起讀取請求,調(diào)用最底層代碼發(fā)起read()系統(tǒng)調(diào)用。
- 操作系統(tǒng)通過DMA等從磁盤等硬件讀取數(shù)據(jù)。
- DMA讀取相關(guān)數(shù)據(jù),存入到內(nèi)核的緩沖區(qū)中。這部分操作是不需要CPU參與的。
- 接下來內(nèi)核將會(huì)把自己緩沖區(qū)的內(nèi)容,拷貝到Java進(jìn)程的緩沖區(qū)中。
可以看到,由于一個(gè)讀取有操作系統(tǒng)的參與,它的交互過程就變的比較復(fù)雜。典型的,當(dāng)Java進(jìn)程讀取數(shù)據(jù)的時(shí)候,內(nèi)核發(fā)現(xiàn)這部分?jǐn)?shù)據(jù)已經(jīng)存在于緩存中了,那么就直接拷貝出來;當(dāng)內(nèi)核緩存不存在這些數(shù)據(jù)的時(shí)候,那么Java進(jìn)程將會(huì)阻塞在那里,直到所需的數(shù)據(jù)拷貝到用戶空間。
總結(jié)一下:內(nèi)核進(jìn)程所持有的內(nèi)存,是不能直接訪問的,我們需要拷貝一份到用戶進(jìn)程。
虛擬地址來幫忙
從上面的描述中可以看出,磁盤文件上的內(nèi)容,要想被用戶進(jìn)程所使用,就不得不經(jīng)過kernel這個(gè)中轉(zhuǎn)站。既然這樣會(huì)影響效率,那么為什么不直接把這些磁盤上的文件直接發(fā)送到用戶進(jìn)程呢?
這不是能不能做的問題,而是應(yīng)不應(yīng)該做的問題。既然用戶進(jìn)程使用了特定的操作系統(tǒng),就要按照操作系統(tǒng)的規(guī)矩辦事。在Linux操作系統(tǒng)上,把這些繁雜的事務(wù)交給操作系統(tǒng),是最安全、最便捷的編程方式。
那么,我現(xiàn)在就是不想按照規(guī)矩來,把效率看的更重一些,怎么辦?
沒別的辦法,只有開啟一條綠色通道。
如果我能夠在用戶進(jìn)程里,和操作系統(tǒng)內(nèi)核里,讀到的是同一份數(shù)據(jù),操作的是同一份緩沖區(qū),那么目的就算達(dá)到了。
如果讓用戶進(jìn)程直接去訪問內(nèi)核所擁有的物理內(nèi)存地址,是非常危險(xiǎn)的。如何共享這些物理內(nèi)存,這需要借助 虛擬內(nèi)存。這就是所謂的綠色通道。
虛擬內(nèi)存,肯定是相對于物理內(nèi)存來說的。如果你反編譯一個(gè)二進(jìn)制文件的話,可以看到它的引用地址是固定的。虛擬內(nèi)存區(qū)域是進(jìn)程的虛擬地址空間中的一個(gè)同質(zhì)區(qū)間,即具有同樣特性的連續(xù)地址范圍。如下圖,MMU組件就專門負(fù)責(zé)虛擬內(nèi)存到物理內(nèi)存到翻譯,這都是《計(jì)算機(jī)組成結(jié)構(gòu)》的東西,已經(jīng)是現(xiàn)代操作系統(tǒng)的默認(rèn)操作。
借助于虛擬內(nèi)存,我們就可以使用不同的虛擬內(nèi)存地址指向同一塊物理內(nèi)存地址,變相的實(shí)現(xiàn)了內(nèi)存數(shù)據(jù)的共享,避免了kernel和user進(jìn)程之間的數(shù)據(jù)拷貝。
如果我們同時(shí)mapping一個(gè)內(nèi)核空間的虛擬地址和用戶空間的虛擬地址,到同一塊內(nèi)核實(shí)際的物理內(nèi)存地址上,那么我們就能夠同時(shí)操作這一塊內(nèi)存區(qū)域。
典型應(yīng)用mmap
mmap (Memory Mapped Files) 就是這樣處理映射的一種特殊通道。它可以將一個(gè)文件,或者其他對象,映射到進(jìn)程的虛擬地址空間。
當(dāng)我們操作這一段內(nèi)存的時(shí)候,就可以直接影響到最終操作系統(tǒng)上的文件。雖然文件的讀寫仍然由操作系統(tǒng)去做,但明顯的,我們不必再調(diào)用read、write等函數(shù),從操作系統(tǒng)的內(nèi)存中拷貝數(shù)據(jù),這肯定能夠增加文件讀寫的效率。
MMAP也使得進(jìn)程間共享編程型內(nèi)存,進(jìn)程通信成為了可能,也可以和內(nèi)核進(jìn)程進(jìn)行協(xié)同式交互。當(dāng)我們的物理內(nèi)存空間不足的時(shí)候,甚至可以使用磁盤來模擬內(nèi)存。
這就是抽象的魔力。
mmap的映射區(qū)域必須時(shí)候 物理頁大小(page_size)的整數(shù)倍,這也是操作系統(tǒng)為了增加處理效率所采取的批量處理模式(內(nèi)存管理的最小粒度就是頁)。同時(shí),mmap不能映射超過文件大小的區(qū)域,所以當(dāng)文件大小發(fā)生變化時(shí),就需要重新映射。
Java中有專門處理mmap的類MappedByteBuffer,我們可以通過FileChannel的map函數(shù)來獲取這個(gè)變量。
MappedByteBuffer mb = new RandomAccessFile("test", "rw")
.getChannel()
.map(FileChannel.MapMode.READ_WRITE, 0, 256);
//...
public abstract MappedByteBuffer map(MapMode mode,
long position, long size)
可以看到,通過position和size兩個(gè)參數(shù),就可以直接將文件的內(nèi)容映射到mb變量中。假如我們不同的進(jìn)程做了同樣的映射,內(nèi)存的使用量也不會(huì)翻倍,因?yàn)樗鼈兌际翘摂M的地址。
假如你的內(nèi)存非常大40GB,但操作系統(tǒng)的內(nèi)存只有2GB,通過這種方式,依然能夠快速的讀取和修改文件。
在使用top命令的時(shí)候,我們經(jīng)??吹絪wap區(qū)域,也就是使用文件去模擬內(nèi)存的區(qū)域。當(dāng)你使用2GB內(nèi)存去操作40GB的文件時(shí),通常會(huì)引起swap out,內(nèi)存的數(shù)據(jù)要寫入到磁盤中。在mmap模式下,就不必再使用額外的swap去保證這個(gè)操作。當(dāng)需要swap的時(shí)候,操作系統(tǒng)會(huì)直接使用原始文件,這些映射也會(huì)在要操作的目標(biāo)文件上生效。
這整個(gè)過程中,除了操作系統(tǒng)缺頁引起文件讀寫,沒有其他任何的緩沖區(qū)參與,所以是非常高效的。
怎么用?
如果你仔細(xì)翻一下mmap相關(guān)的代碼,可以發(fā)現(xiàn)默認(rèn)提供的函數(shù)非常非常非常的稀少,要拿它來搞事情的話,使用起來各種限制。
所以我們需要配合索引文件,來配合mmap完成高效的操作。
在一些數(shù)據(jù)庫和中間件中,我們經(jīng)常看到mmap的身影,尤其是那些涉及到大文件讀寫的場景。
在kafka和rocketmq中,commitlog需要根據(jù)偏移量讀取數(shù)據(jù),mmap無疑是非常好的加速方式。就拿kafka的索引文件來說,就大量使用了mmap;消費(fèi)時(shí)Kafka 直接把文件發(fā)送給消費(fèi)者,配合mmap 作為文件讀寫方式,直接把它傳給 sendfile。
包括主流的ES,也大量使用了mmap。這是一種作弊的行為。
不要高興的太早。
經(jīng)過很多benchmark的測試,mmap在不同的Linux平臺(tái)上,并不總是有這么好的表現(xiàn)。當(dāng)文件大小不被內(nèi)存所容下的時(shí)候,頻繁的文件交換和缺頁依然會(huì)發(fā)生,這需要經(jīng)過實(shí)際驗(yàn)證才能確認(rèn)服務(wù)真正的表現(xiàn)。
所以,在內(nèi)嵌數(shù)據(jù)庫rocksdb中,mmap相關(guān)的優(yōu)化參數(shù)是默認(rèn)關(guān)閉的。mmap應(yīng)該作為一種魔法存在,而不能作為一種通用的優(yōu)化方法。
allow_mmap_reads=false
allow_mmap_writes=false
另外,mmap在作為寫入時(shí),也并不是十分可靠。因?yàn)閷懭氲絤map中的數(shù)據(jù),并沒有被真正的寫到硬盤,它需要操作系統(tǒng)在調(diào)用flush函數(shù)的時(shí)候才真正的刷到硬盤上。所以,作為數(shù)據(jù)恢復(fù)用的wal日志或者translog、redolog等,并沒有采用mmap這種方式。
mmap另外一個(gè)比較嚴(yán)重的問題,就是不可預(yù)料的I/O停頓。有了操作系統(tǒng)這一環(huán),加上應(yīng)用中的各種Buffer的參與,再加上預(yù)讀這種操作,應(yīng)用在操作文件的時(shí)候,會(huì)比較平滑。但一旦使用了mmap,你可能在不可預(yù)料的情況下被阻塞,或者被不合理的預(yù)讀干擾,發(fā)生頻繁的I/O。
End
性能優(yōu)化從來都是一把雙刃劍。這把劍,到底是能殺掉敵人,還是手殘傷了隊(duì)友,那就要看掌劍人的水平了。mmap也不例外,它有好處,也有缺點(diǎn),多一點(diǎn)敬畏,結(jié)論從實(shí)踐中來,才是正確的態(tài)度。