什么是io
Linux 最經(jīng)典的一句話是:「一切皆文件」,不僅普通的文件和目錄,就連塊設(shè)備、管道、socket等,也都是統(tǒng)一交給文件系統(tǒng)管理的。
io其實是input和output的縮寫,顧名思義就是輸入和輸出。而我們經(jīng)常接觸的文件、目錄都是存儲在磁盤的,相對應(yīng)的就是讀取磁盤的信息read()和將數(shù)據(jù)寫入磁盤write()。
io類型
其實在我寫這邊文章時候?qū)o的讀寫了解很少(我是菜雞),大概的寫數(shù)據(jù)就是從應(yīng)用程序?qū)懭氲酱疟P,讀數(shù)據(jù)就是從磁盤將數(shù)據(jù)讀取應(yīng)用程序,中間具體的細(xì)節(jié)根本不知道。
那么這遍文章的主要目的就是對讀寫io有個更細(xì)致的了解,先從io的基本類型說起。
緩存io和直接io
我們都知道磁盤 I/O 是非常慢的,所以 Linux 內(nèi)核為了減少磁盤 I/O 次數(shù),在系統(tǒng)調(diào)用后,會把進(jìn)程內(nèi)的用戶數(shù)據(jù)拷貝到進(jìn)程的內(nèi)核空間中緩存起來,這個內(nèi)核空間緩存也就是「頁緩存」,這種io方式就叫做緩存io。
那么,根據(jù)是「否利用內(nèi)核緩存」,可以把文件 I/O 分為緩存 I/O 與直接 I/O。
直接 I/O就是不會發(fā)生內(nèi)核緩存和用戶空間的數(shù)據(jù)復(fù)制,而是直接經(jīng)過文件系統(tǒng)訪問磁盤。
如果你在使用文件操作類的系統(tǒng)調(diào)用函數(shù)時,指定了 O_DIRECT標(biāo)志,則表示使用直接 I/O。如果沒有設(shè)置過,默認(rèn)使用的是非直接 I/O。
緩存io的讀寫過程
讀過程read():
- 首先由用戶空間上下文切換到內(nèi)核空間,查詢內(nèi)核緩存是否存在。
- 如果內(nèi)核空間存在查詢數(shù)據(jù),則直接將內(nèi)核空間的數(shù)據(jù)copy到用戶空間同時伴隨著上下文切換到了用戶空間,用戶空間copy數(shù)據(jù)完成后讀取結(jié)束。
- 如果內(nèi)核空間不存在查詢數(shù)據(jù)則通過文件系統(tǒng)獲取磁盤數(shù)據(jù)并copy到內(nèi)核空間,當(dāng)內(nèi)核空間數(shù)據(jù)準(zhǔn)備完成后,接著將內(nèi)存空間的數(shù)據(jù)在copy到用戶空間同時伴隨著上下文切換到用戶空間,用戶空間copy數(shù)據(jù)完成后讀取結(jié)束。
下圖為讀數(shù)據(jù)時未命中內(nèi)核緩存的時完整流程圖:

其實圖中是最傳統(tǒng)的且沒有DMA時的io模型,整個數(shù)據(jù)的傳輸過程,都要需要 CPU 親自參與搬運數(shù)據(jù)的過程,而且這個過程,CPU 是不能做其他事情的。io模型的演進(jìn)可以看這篇文章零拷貝。
可以發(fā)現(xiàn)未命中頁緩存時的緩存io的讀取涉及到倆次的的上下文切換和倆次的數(shù)據(jù)復(fù)制;而命中頁緩存的情況下設(shè)計到倆次的的上下文切換和一次的數(shù)據(jù)復(fù)制。
寫過程write():
- 上下文由用戶空間切換到內(nèi)核空間上同時將用戶空間數(shù)據(jù)copy到內(nèi)核空間緩存上。
- 內(nèi)核空間數(shù)據(jù)copy完成后,將上下文切換到用戶空間并完成寫入響應(yīng)。
- 寫到內(nèi)核空間緩存上的數(shù)據(jù)會在合適的時機將緩存刷到磁盤上。
可以發(fā)現(xiàn)緩存io的寫操作涉及到倆次的的上下文切換和一次的數(shù)據(jù)復(fù)制。
直接io的讀寫過程
- Write 操作:由于其不使用內(nèi)核緩存(頁緩存),所以其進(jìn)行寫文件,如果返回成功,數(shù)據(jù)就真的落盤了(不考慮磁盤自帶的緩存);
- Read 操作:由于其不使用 page cache,每次讀操作是真的從磁盤中讀取,不會從文件系統(tǒng)的緩存中讀取。
內(nèi)核緩存的刷盤時機
如果用了緩存 I/O 進(jìn)行寫數(shù)據(jù)操作,內(nèi)核什么情況下才會把緩存數(shù)據(jù)寫入到磁盤?以下幾種場景會觸發(fā)內(nèi)核緩存的數(shù)據(jù)寫入磁盤:
在調(diào)用 write 的最后,當(dāng)發(fā)現(xiàn)內(nèi)核緩存的數(shù)據(jù)太多的時候,內(nèi)核會把數(shù)據(jù)寫到磁盤上;
內(nèi)核緩存的數(shù)據(jù)的緩存時間超過某個時間時,也會把數(shù)據(jù)刷到磁盤上;
用戶主動調(diào)用 sync,內(nèi)核緩存會刷到磁盤上;
內(nèi)存緩存(頁緩存)也是占用物理內(nèi)存的,當(dāng)內(nèi)存十分緊張,無法再分配進(jìn)程時,會使用swap內(nèi)存交換技術(shù),將內(nèi)核緩存的數(shù)據(jù)刷到磁盤上;
疑問?
了解了緩存io和直接io后,你可能會有個疑問:既然緩存io避免了直接讀寫磁盤,讀寫速度會比直接io快很多,那么直接io還是適用的場景嗎?還是目前都適用緩存io來實現(xiàn)讀寫了?
首先明確的告訴你:有的場景只適用直接io而不適用緩存io,直接io還是有用武之地的,這里先賣個關(guān)子,具體原因繼續(xù)往下看吧。
阻塞與非阻塞 I/O
先說明一點,阻塞與非阻塞都是基于上面介紹的緩存io來開講的。
阻塞io
先來看看阻塞 I/O,當(dāng)用戶程序執(zhí)行 read ,線程會被阻塞,一直等到內(nèi)核數(shù)據(jù)準(zhǔn)備好,并把數(shù)據(jù)從內(nèi)核緩沖區(qū)拷貝到應(yīng)用程序的緩沖區(qū)中,當(dāng)拷貝過程完成,read 才會返回。
線程被阻塞的過程cpu是不能執(zhí)行別的任務(wù)的,只能執(zhí)行完當(dāng)前線程的任務(wù),cpu才會被釋放。
上面介紹的緩存io和直接io都是屬于阻塞io。
非阻塞io
非阻塞的 read 請求在數(shù)據(jù)未準(zhǔn)備好的情況下立即返回,可以繼續(xù)往下執(zhí)行,此時應(yīng)用程序不斷輪詢內(nèi)核,直到數(shù)據(jù)準(zhǔn)備好,內(nèi)核將數(shù)據(jù)拷貝到應(yīng)用程序緩沖區(qū),read 調(diào)用才可以獲取到結(jié)果。過程如下圖:

總結(jié)
當(dāng)涉及到文件 I/O 時,有兩種主要的訪問方式:Page Cache 和 Direct I/O(也稱為裸 I/O)。Page Cache就是緩存io,Direct I/O就是直接io。
而阻塞io和非阻塞io是邏輯上的一個io,Page Cache 和 Direct I/O都屬于阻塞io。
頁緩存
上面介紹了那么多,終于來到正題了,頁緩存。
其實上面已經(jīng)介紹的過了,緩存io讀寫時存儲在內(nèi)核空間的緩存其實就是頁緩存,頁緩存也是占用的物理內(nèi)存的。
頁緩存的特點
頁緩存有倆個非常明顯的特點:預(yù)讀和合并。
預(yù)讀
操作系統(tǒng)為基于頁緩存的讀緩存機制提供預(yù)讀機制。
舉個??說明下什么是預(yù)讀機制:
- 比如用戶線程僅僅請求讀取磁盤上文件 A 的 offset 為 0-3KB 范圍內(nèi)的數(shù)據(jù),由于磁盤的基本讀寫單位為
block(4KB),于是操作系統(tǒng)至少會讀 0-4KB 的內(nèi)容,這恰好可以在一個 page 中裝下。 - 但是操作系統(tǒng)出于
局部性原理會選擇將磁盤塊 offset [4KB,8KB)、[8KB,12KB) 以及 [12KB,16KB) 都加載到內(nèi)存,于是額外在內(nèi)存中申請了3 個 page;
下圖中,應(yīng)用程序利用 read 系統(tǒng)調(diào)動讀取 4KB 數(shù)據(jù),實際上內(nèi)核使用 readahead 機制完成了 16KB 數(shù)據(jù)的讀取。

為什么要有預(yù)讀這個機制呢?
因為系統(tǒng)出于空間局部性原理考慮,靠近當(dāng)前被訪問數(shù)據(jù)的數(shù)據(jù),在未來很大概率會被訪問到,所以如果使用的是緩存io的話會讀取更多的數(shù)據(jù)加載頁緩存中;
通??拷?dāng)前的數(shù)據(jù)真的大概率會在接下來時間被訪問,那么這些數(shù)據(jù)都不需要再次讀取磁盤了,而是可以直接從頁緩存中讀取了,增加了緩存的命中率。
預(yù)讀并不是只有優(yōu)點,缺點也很明顯,如果小概率那些數(shù)據(jù)沒有被訪問到時,那么多讀取的數(shù)據(jù)就是浪費,主要體現(xiàn)在倆方面:
- 多讀取的那部分?jǐn)?shù)據(jù)也是從磁盤中讀取的,讀io時間相對來說更長。
- 讀取到數(shù)據(jù)都是存儲在頁緩存的,而頁緩存是存儲在屋里內(nèi)存的,也就是說其實多讀取的那部分?jǐn)?shù)據(jù)是浪費內(nèi)存了。
其實預(yù)讀機制還會帶很多問題,比如下面要介紹的預(yù)讀失效和緩存污染。
合并io請求
即內(nèi)核會將許多I/O請求暫時存儲在頁緩存中,然后在適當(dāng)?shù)臅r機將它們合并成更大的I/O請求,再發(fā)送到磁盤上進(jìn)行讀取或?qū)懭氩僮鳌?strong>這個做法的目的是為了減少磁盤的尋址操作,從而提高I/O操作的效率。
讓我通過一個具體的例子來解釋:
假設(shè)你有一個程序,需要從磁盤上讀取多個小文件。如果每個小文件都觸發(fā)一個單獨的磁盤讀取請求,那么磁盤在讀取完一個文件后,需要停下來,重新定位磁頭位置,然后再開始讀取下一個文件。這些尋址操作會導(dǎo)致磁盤的機械部件運動,需要一定的時間。這種情況下,磁盤的性能可能會受到限制,因為尋址操作耗費了相當(dāng)多的時間。
然而,如果操作系統(tǒng)使用了頁緩存,它會將這些小文件的I/O請求暫時保存在緩存中,不立即發(fā)送給磁盤。當(dāng)有足夠多的請求積累在緩存中,操作系統(tǒng)會考慮將它們合并成一個大的I/O請求,然后再發(fā)送給磁盤。這個大的請求涵蓋了多個小文件,這樣磁盤就可以連續(xù)地讀取數(shù)據(jù),而不需要頻繁的尋址操作。因此,通過合并請求,磁盤的機械部件可以更有效地工作,從而提高了I/O操作的速度和效率。
總之,使用頁緩存和合并I/O請求的方法可以減少磁盤的尋址操作,提高I/O操作的效率,從而更有效地利用硬件資源。
頁緩存的優(yōu)點
1.加快數(shù)據(jù)訪問
如果數(shù)據(jù)能夠在內(nèi)存中進(jìn)行緩存,那么下一次訪問就不需要通過磁盤 I/O 了,直接命中內(nèi)存緩存即可。
由于內(nèi)存訪問比磁盤訪問快很多,因此加快數(shù)據(jù)訪問是 Page Cache 的一大優(yōu)勢。
2.減少 I/O 次數(shù),提高系統(tǒng)磁盤 I/O 吞吐量
得益于 Page Cache 的緩存以及預(yù)讀能力,而程序又往往符合局部性原理,因此通過一次 I/O 將多個 page 裝入 Page Cache 能夠減少磁盤 I/O 次數(shù), 進(jìn)而提高系統(tǒng)磁盤 I/O 吞吐量。
頁緩存的缺點
1. 占用物理內(nèi)存
最直接的缺點是需要占用額外物理內(nèi)存空間,物理內(nèi)存在比較緊俏的時候可能會導(dǎo)致頻繁的 swap 操作,最終導(dǎo)致系統(tǒng)的磁盤 I/O 負(fù)載的上升。
2. 應(yīng)用層沒有提供方便的api
另一個缺陷是對應(yīng)用層并沒有提供很好的管理 API,幾乎是透明管理。應(yīng)用層即使想優(yōu)化 Page Cache 的使用策略也很難進(jìn)行。因此一些應(yīng)用選擇在用戶空間實現(xiàn)自己的 page 管理,而不使用 page cache,例如 MySQL InnoDB 存儲引擎以 16KB 的頁進(jìn)行管理。
頁緩存適用的場景和不適用的場景
我們知道頁緩存的優(yōu)點主要是三個:
- 緩存最近被訪問的數(shù)據(jù);
- 預(yù)讀功能;
- 合并io請求
這三個做法,將大大提高讀寫磁盤的性能。
但是,在傳輸大文件(GB 級別的文件)的時候,PageCache 會不起作用。
這是因為如果你有很多 GB 級別文件需要傳輸,每當(dāng)用戶訪問這些大文件的時候,內(nèi)核就會把它們載入 PageCache 中,于是 PageCache 空間很快被這些大文件占滿。
另外,由于文件太大,可能某些部分的文件數(shù)據(jù)被再次訪問的概率比較低,這樣就會帶來 2 個問題:
- PageCache 由于長時間被大文件占據(jù),其他「熱點」的小文件可能就無法充分使用到 PageCache,于是這樣磁盤讀寫的性能就會下降了;
- PageCache 中的大文件數(shù)據(jù),由于沒有享受到緩存帶來的好處,但卻耗費 DMA 多拷貝到 PageCache 一次;
所以,小文件的傳輸適用于頁緩存的方式讀取,而針對大文件的傳輸,不應(yīng)該使用 頁緩存,因為可能由于 PageCache 被大文件占據(jù),而導(dǎo)致「熱點」小文件無法利用到 PageCache,這樣在高并發(fā)的環(huán)境下,會帶來嚴(yán)重的性能問題。
那對于大文件的傳輸適合什么方式呢?
我們先來看看最初的例子,當(dāng)調(diào)用 read 方法讀取文件時,進(jìn)程實際上會阻塞在 read 方法調(diào)用,因為要等待磁盤數(shù)據(jù)的返回,如下圖:

具體過程如下:
- 當(dāng)調(diào)用 read 方法時,會阻塞著,此時內(nèi)核會向磁盤發(fā)起 I/O 請求,磁盤收到請求后,便會尋址,當(dāng)磁盤數(shù)據(jù)準(zhǔn)備好后,就會向內(nèi)核發(fā)起 I/O 中斷,告知內(nèi)核磁盤數(shù)據(jù)已經(jīng)準(zhǔn)備好;
- 內(nèi)核收到 I/O 中斷后,就將數(shù)據(jù)從磁盤控制器緩沖區(qū)拷貝到 PageCache 里;
- 最后,內(nèi)核再把 PageCache 中的數(shù)據(jù)拷貝到用戶緩沖區(qū),于是 read 調(diào)用就正常返回了。
上述是正常使用緩存io即頁緩存的方式的io請求模型,但是對于大文件的請求就不應(yīng)該用上述這種方式了。
應(yīng)該使用直接io的方法,但是直接io的方式是阻塞的,且傳輸大文件時阻塞時間會更長,所以大文件的讀取應(yīng)該使用直接io+異步io的方式,如下圖

它把讀操作分為兩部分:
- 前半部分,內(nèi)核向磁盤發(fā)起讀請求,但是可以
不等待數(shù)據(jù)就位就可以返回,于是進(jìn)程此時可以處理其他任務(wù); - 后半部分,當(dāng)內(nèi)核將磁盤中的數(shù)據(jù)拷貝到進(jìn)程緩沖區(qū)后,進(jìn)程將接收到內(nèi)核的通知,再去處理數(shù)據(jù);
通常使用直接 I/O 應(yīng)用場景常見的兩種:
- 應(yīng)用程序已經(jīng)實現(xiàn)了磁盤數(shù)據(jù)的緩存,那么可以不需要 PageCache 再次緩存,減少額外的性能損耗。在 MySQL 數(shù)據(jù)庫中,可以通過參數(shù)設(shè)置開啟直接 I/O,默認(rèn)是不開啟;
- 傳輸大文件的時候,由于大文件難以命中 PageCache 緩存,而且會占滿 PageCache 導(dǎo)致「熱點」文件無法充分利用緩存,從而增大了性能開銷,因此,這時應(yīng)該使用直接 I/O。
疑問
問題:進(jìn)程寫文件(使用緩沖 IO)過程中,寫一半的時候,進(jìn)程發(fā)生了崩潰,已寫入的數(shù)據(jù)會丟失嗎?
答案:不會。
因為進(jìn)程在執(zhí)行 write (使用緩沖 IO)系統(tǒng)調(diào)用的時候,實際上是將文件數(shù)據(jù)寫到了內(nèi)核的 page cache,它是文件系統(tǒng)中用于緩存文件數(shù)據(jù)的緩沖,所以即使進(jìn)程崩潰了,文件數(shù)據(jù)還是保留在內(nèi)核的 page cache,我們讀數(shù)據(jù)的時候,也是從內(nèi)核的 page cache 讀取,因此還是依然讀的進(jìn)程崩潰前寫入的數(shù)據(jù)。
內(nèi)核會找個合適的時機,將 page cache 中的數(shù)據(jù)持久化到磁盤。但是如果 page cache 里的文件數(shù)據(jù),在持久化到磁盤化到磁盤之前,系統(tǒng)發(fā)生了崩潰,那這部分?jǐn)?shù)據(jù)就會丟失了。
當(dāng)然, 我們也可以在程序里調(diào)用 fsync 函數(shù),在寫文文件的時候,立刻將文件數(shù)據(jù)持久化到磁盤,這樣就可以解決系統(tǒng)崩潰導(dǎo)致的文件數(shù)據(jù)丟失的問題。
頁緩存常見的問題
上面的介紹大致了解了頁緩存其實就是內(nèi)核空間的緩存,也了解它的特點以及優(yōu)缺點。
我們知道頁緩存其實是存儲在物理內(nèi)存的,那么頁緩存資源就是有限的,所以你知道它的淘汰策略是什么嗎?
上面有說由于頁緩存預(yù)讀會多讀取更多的磁盤數(shù)據(jù)到頁緩存中,如果這部分?jǐn)?shù)據(jù)在后面的時間里沒有被讀取,那這部分?jǐn)?shù)據(jù)豈不是占用了多余的內(nèi)存資源嗎?
上面有說大文件不不適合使用頁緩存來讀取,因為會占用大量的頁緩存,擠到原來的熱點數(shù)據(jù)。如果仍然使用頁緩存來讀取大文件該怎么解決這個問題呢?
帶著上面這個幾個疑問,繼續(xù)往下看。
在應(yīng)用程序讀取文件的數(shù)據(jù)的時候,Linux 操作系統(tǒng)是會對讀取的文件數(shù)據(jù)進(jìn)行緩存的,會緩存在內(nèi)核空間,該緩存稱為
頁緩存。
由于操作系統(tǒng)的頁緩存對于用戶空間沒有方便的api,所以MySQL Innodb 存儲引擎設(shè)計了在用戶空間的“頁緩存“:Buffer Pool。
傳統(tǒng) LRU 是如何管理內(nèi)存數(shù)據(jù)的?
Linux 的 Page Cache 和 MySQL 的 Buffer Pool 的大小是有限的,并不能無限的緩存數(shù)據(jù),對于一些頻繁訪問的數(shù)據(jù)我們希望可以一直留在內(nèi)存中,而一些很少訪問的數(shù)據(jù)希望可以在某些時機可以淘汰掉,從而保證內(nèi)存不會因為滿了而導(dǎo)致無法再緩存新的數(shù)據(jù),同時還能保證常用數(shù)據(jù)留在內(nèi)存中。
要實現(xiàn)這個,最容易想到的就是 LRU(Least recently used)算法。
LRU 算法一般是用「鏈表」作為數(shù)據(jù)結(jié)構(gòu)來實現(xiàn)的,鏈表頭部的數(shù)據(jù)是最近使用的,而鏈表末尾的數(shù)據(jù)是最久沒被使用的。那么,當(dāng)空間不夠了,就淘汰最久沒被使用的節(jié)點,也就是鏈表末尾的數(shù)據(jù),從而騰出內(nèi)存空間。
傳統(tǒng)的 LRU 算法的實現(xiàn)思路是這樣的:
- 當(dāng)訪問的頁在內(nèi)存里,就直接把該頁對應(yīng)的 LRU 鏈表節(jié)點移動到鏈表的頭部。
- 當(dāng)訪問的頁不在內(nèi)存里,除了要把該頁放入到 LRU 鏈表的頭部,還要淘汰 LRU 鏈表末尾的頁。
比如下圖,假設(shè) LRU 鏈表長度為 5,LRU 鏈表從左到右有編號為 1,2,3,4,5 的頁。

如果訪問了 3 號頁,因為 3 號頁已經(jīng)在內(nèi)存了,所以把 3 號頁移動到鏈表頭部即可,表示最近被訪問了。

而如果接下來,訪問了 8 號頁,因為 8 號頁不在內(nèi)存里,且 LRU 鏈表長度為 5,所以必須要淘汰數(shù)據(jù),以騰出內(nèi)存空間來緩存 8 號頁,于是就會淘汰末尾的 5 號頁,然后再將 8 號頁加入到頭部。

傳統(tǒng)的 LRU 算法并沒有被 Linux 和 MySQL 使用,因為傳統(tǒng)的 LRU 算法無法避免下面這兩個問題:
- 預(yù)讀失效導(dǎo)致緩存命中率下降;
- 緩存污染導(dǎo)致緩存命中率下降;
那么linux操作系統(tǒng)和mysql的buffer pool是怎么改造lru的淘汰策略的呢?繼續(xù)往下看
預(yù)讀失效
我們知道頁緩存會根據(jù)局部性原理,將訪問數(shù)據(jù)的周圍數(shù)據(jù)也會加載到頁緩存中,因為大概率在接下來的時間周圍數(shù)據(jù)也是會被訪問到的。
如果這些周圍數(shù)據(jù),也就是被提前加載進(jìn)來的頁,并沒有被訪問,相當(dāng)于這個預(yù)讀工作是白做了,這個就是預(yù)讀失效。
如果使用傳統(tǒng)的 LRU 算法,就會把「預(yù)讀頁」放到 LRU 鏈表頭部,而當(dāng)內(nèi)存空間不夠的時候,還需要把末尾的頁淘汰掉。
如果這些「預(yù)讀頁」如果一直不會被訪問到,就會出現(xiàn)一個很奇怪的問題,不會被訪問的預(yù)讀頁卻占用了 LRU 鏈表前排的位置,而末尾淘汰的頁,可能是熱點數(shù)據(jù),這樣就大大降低了緩存命中率 。
我們不能因為害怕預(yù)讀失效,而將預(yù)讀機制去掉,大部分情況下,空間局部性原理還是成立的。
linux操作系統(tǒng)是怎么避免預(yù)讀失效?
Linux 操作系統(tǒng)實現(xiàn)兩個了 LRU 鏈表:活躍 LRU 鏈表(active_list)和非活躍 LRU 鏈表(inactive_list);
-
active list活躍內(nèi)存頁鏈表,這里存放的是最近被訪問過(活躍)的內(nèi)存頁; -
inactive list不活躍內(nèi)存頁鏈表,這里存放的是很少被訪問(非活躍)的內(nèi)存頁;
有了這兩個 LRU 鏈表后,預(yù)讀頁就只需要加入到 inactive list 區(qū)域的頭部,當(dāng)頁被真正訪問的時候,才將頁插入 active list 的頭部。如果預(yù)讀的頁一直沒有被訪問,就會從 inactive list 移除,這樣就不會影響 active list 中的熱點數(shù)據(jù)。
接下來,給大家舉個例子。
-
假設(shè) active list 和 inactive list 的長度為 5,目前內(nèi)存中已經(jīng)有如下 10 個頁:
- 現(xiàn)在有個編號為 20 的頁被預(yù)讀了,
這個頁只會被插入到 inactive list 的頭部,而不會插入到active list列表內(nèi),然后 inactive list 末尾的頁(10號)會被淘汰掉。
即使編號為 20 的預(yù)讀頁一直不會被訪問,它也沒有占用到 active list 的位置,這樣就不會將active list中的熱點數(shù)據(jù)給擠出去,造成熱點數(shù)據(jù)命中率下降。
- 如果 20 號頁被預(yù)讀后,立刻被訪問了,那么就會將它插入到
active list的頭部,active list末尾的頁(5號),會被降級到 inactive list,作為 inactive list 的頭部,這個過程并不會有數(shù)據(jù)被淘汰。
可以看出:
- 預(yù)讀的數(shù)據(jù)只有在真正被訪問時才會加載到active list列表。
- active list中的尾部數(shù)據(jù)被淘汰時會降級到inactive list的頭部。
mysql是怎么避免預(yù)讀失效?
MySQL 的 Innodb 存儲引擎的buffer pool是在一個 LRU鏈表上劃分來 2 個區(qū)域,young 區(qū)域 和 old區(qū)域。
這倆種改進(jìn)方式,其是都是將數(shù)據(jù)分為了冷數(shù)據(jù)和熱數(shù)據(jù),然后分別進(jìn)行 LRU 算法。區(qū)別是操作系統(tǒng)的頁緩存的是用倆條煉焦來區(qū)分冷熱數(shù)據(jù),而mysql的buffer poll是使用一個鏈表劃分出倆個區(qū)域來進(jìn)行區(qū)分冷熱數(shù)據(jù)的。
young 區(qū)域在 LRU 鏈表的前半部分,old 區(qū)域則是在后半部分,這兩個區(qū)域都有各自的頭和尾節(jié)點,如下圖:

young 區(qū)域與 old 區(qū)域在 LRU 鏈表中的占比關(guān)系
并不是一比一的關(guān)系,而是 63:37(默認(rèn)比例)的關(guān)系。
劃分這兩個區(qū)域后,預(yù)讀的頁就只需要加入到old 區(qū)域的頭部,當(dāng)頁被真正訪問的時候,才將頁插入young 區(qū)域的頭部。如果預(yù)讀的頁一直沒有被訪問,就會從 old 區(qū)域移除,這樣就不會影響 young 區(qū)域中的熱點數(shù)據(jù)。這個是和頁緩存的操作邏輯是一致的。
接下來,給大家舉個例子。
-
假設(shè)有一個長度為 10 的 LRU 鏈表,其中 young 區(qū)域占比 70 %,old 區(qū)域占比 30 %。
-
現(xiàn)在有個編號為 20 的頁被預(yù)讀了,這個頁只會被插入到 old 區(qū)域頭部,而 old 區(qū)域末尾的頁(10號)會被淘汰掉。
-
如果 20 號頁被預(yù)讀后,立刻被訪問了,那么就會將它插入到 young 區(qū)域的頭部,young 區(qū)域末尾的頁(7號),會被擠到 old 區(qū)域,作為 old 區(qū)域的頭部,這個過程并不會有頁被淘汰。
緩存污染
雖然 Linux (實現(xiàn)兩個 LRU 鏈表)和 MySQL (劃分兩個區(qū)域)通過改進(jìn)傳統(tǒng)的 LRU 數(shù)據(jù)結(jié)構(gòu),避免了預(yù)讀失效帶來的影響。
但是如果還是使用「只要數(shù)據(jù)被訪問一次,就將數(shù)據(jù)加入到活躍 LRU 鏈表頭部(或者 young 區(qū)域)」這種方式的話,那么還存在緩存污染的問題。
當(dāng)我們在批量讀取數(shù)據(jù)或者讀取大文件的時候,由于數(shù)據(jù)被訪問了一次,這些大量數(shù)據(jù)都會被加入到「活躍 LRU 鏈表」或者 young 區(qū)域)里,然后之前緩存在活躍 LRU 鏈表(或者 young 區(qū)域)里的熱點數(shù)據(jù)全部都被淘汰了,如果這些大量的數(shù)據(jù)在很長一段時間都不會被訪問的話,那么整個活躍 LRU 鏈表(或者 young 區(qū)域)就被污染了。
緩存污染會帶來什么問題?
緩存污染帶來的影響就是很致命的,當(dāng)被擠出去的這些熱數(shù)據(jù)又被再次訪問的時候,由于緩存未命中,就會產(chǎn)生大量的磁盤 I/O,系統(tǒng)性能就會急劇下降。
將只訪問了一次的大量數(shù)據(jù)就加入到活躍 LRU 鏈表」或者 young 區(qū)域,而將頻繁訪問的熱點數(shù)據(jù)擠出活躍 LRU 鏈表」或者 young 區(qū)域,顯然是不合理的,也是完全不可接受的。
怎么避免緩存污染造成的影響?
前面的 LRU 算法只要數(shù)據(jù)被訪問一次,就將數(shù)據(jù)加入活躍 LRU 鏈表(或者 young 區(qū)域),這種 LRU 算法進(jìn)入活躍 LRU 鏈表的門檻太低了!正式因為門檻太低,才導(dǎo)致在發(fā)生緩存污染的時候,很容就將原本在活躍 LRU 鏈表里的熱點數(shù)據(jù)淘汰了。
所以,只要我們提高進(jìn)入到活躍 LRU 鏈表(或者 young 區(qū)域)的門檻,就能有效地保證活躍 LRU 鏈表(或者 young 區(qū)域)里的熱點數(shù)據(jù)不會被輕易替換掉。
Linux 操作系統(tǒng)和 MySQL Innodb 存儲引擎分別是這樣提高門檻的:
- Linux 操作系統(tǒng):在內(nèi)存頁被
第一次訪問你的時候是在加載到inactive list,在被訪問第二次的時候,才將頁從inactive list升級到active list里。 - MySQL Innodb:在內(nèi)存頁被
第一次訪問你的時候是在加載到old區(qū)域,再第二次被訪問的時候,并不會馬上將該頁從 old 區(qū)域升級到 young 區(qū)域,因為還要進(jìn)行停留在 old 區(qū)域的時間判斷:- 如果第二次的訪問時間與第一次訪問的時間在
1 秒內(nèi)(默認(rèn)值),那么該頁就不會被從 old 區(qū)域升級到 young 區(qū)域; - 如果第二次的訪問時間與第一次訪問的時間
超過 1 秒,那么該頁就會從 old 區(qū)域升級到 young 區(qū)域;
- 如果第二次的訪問時間與第一次訪問的時間在
提高了進(jìn)入活躍 LRU 鏈表(或者 young 區(qū)域)的門檻后,就很好了避免緩存污染帶來的影響。





