1. 程式人生 > >【轉】ZFS讀快取深入研究:ARC

【轉】ZFS讀快取深入研究:ARC

本文對ZFS的ARC機制講的比較清晰易懂,轉載學習下。【轉自】http://blog.chinaunix.net/uid-28466562-id-3837685.html

ZFS 讀快取深入研究:ARC

在Solaris ZFS 中實現的ARC(Adjustable Replacement Cache)讀快取淘汰演算法真是很有意義的一塊軟體程式碼。它是基於IBM的Megiddo和Modha提出的ARC(Adaptive Replacement Cache)淘汰演算法演化而來的。但是ZFS的開發者們對IBM 的ARC演算法做了一些擴充套件,以更適用於ZFS的應用場景。ZFS ARC的最早實現展現在FAST 2003的會議上,並在雜誌《;Login:》的一篇文章中被詳細描述。

注:關於雜誌《;Login:》,可參考這個連結:https://www.usenix.org/publications/login/2003-08/index.html

ZFS ARC真是一個優美的設計。在接下來的描述中,我將盡量簡化一些機制,以便於大家更容易理解ZFS ARC的工作原理。關於ZFS ARC的權威描述,可以參考這個連結:http://src.opensolaris.org/source/xref/onnv/onnv-gate/usr/src/uts/common/fs/zfs/arc.c。在接下來的段落中,我將試著給大家深入講解一下ZFS 讀快取的內部工作原理。我將關注點放在資料如何進入快取,快取如何調整它自己以適應I/O模式的變化,以及“Adjustable Replacement Cache”這個名字是如何來的。

快取

嗯,在一些檔案系統快取中實現的標準的LRU淘汰演算法是有一些缺點的。例如,它們對掃描讀模式是沒有抵抗性的。但你一次順序讀取大量的資料塊時,這些資料塊就會填滿整個快取空間,即使它們只是被讀一次。當快取空間滿了之後,你如果想向快取放入新的資料,那些最近最少被使用的頁面將會被淘汰出去。在這種大量順序讀的情況下,我們的快取將會只包含這些新讀的資料,而不是那些真正被經常使用的資料。在這些順序讀出的資料僅僅只被使用一次的情況下,從快取的角度來看,它將被這些無用的資料填滿。

另外一個挑戰是:一個快取可以根據時間進行優化(快取那些最近使用的頁面),也可以根據頻率進行優化(快取那些最頻繁使用的頁面)。但是這兩種方法都不能適應所有的workload。而一個好的快取設計是能自動根據workload來調整它的優化策略。

ARC的內部工作原理

在ARC原始的實現(IBM的實現)和ZFS中的擴充套件實現都解決了這些挑戰,或者說現存問題。我將描述由Megiddo和Modha提出的Adaptive Replacement Cache的一些基本概念,ZFS的實現版本作為這個實現機制的一個擴充套件來介紹。這兩種實現(原始的Adaptive Replacement Cache和ZFS Adjustable Replacement Cache)共享一些基本的操作原理,所以我認為這種簡化是一種用來解釋ZFS ARC切實可行的途徑。

首先,假設我們的快取中有一個固定的頁面數量。簡單起見,假設我們有一個8個頁面大小的快取。為了是ARC可以工作,在快取中,它需要一個2倍大小的管理表。

這個管理表分成4個連結串列。頭兩個連結串列是顯而易見的:

·         最近最多使用的頁面連結串列 (LRU list)

·         最近最頻繁使用的頁面連結串列(LFU list)

另外兩個連結串列在它們的角色上有些奇怪。它們被稱作ghost連結串列。那些最近被淘汰出去的頁面資訊被儲存在這兩個連結串列中:

·         儲存那些最近從最近最多使用連結串列中淘汰的頁面資訊 (Ghost list for LRU)

·         儲存那些最近從最近最頻繁使用連結串列中淘汰的頁面資訊(Ghost list for LFU)

這兩個ghost連結串列不儲存資料(僅僅儲存頁面資訊,比如offset,dev-id),但是在它們之中的命中對ARC快取工作的行為具有重要的影響,我將在後面介紹。那麼在快取中都發生了什麼呢?

假設我們從磁碟上讀取一個頁面,並把它放入cache中。這個頁面會放入LRU 連結串列中。

接下來我們讀取另外一個不同的頁面。它也會被放入快取。顯然,他也會被放入LRU 連結串列的最近最多使用的位置(位置1):

好,現在我們再讀一次第一個頁面。我們可以看到,這個頁面在快取中將會被移到LFU連結串列中。所有進入LRU連結串列中的頁面都必須至少被訪問兩次。無論什麼時候,一個已經在LFU連結串列中的頁面被再次訪問,它都會被放到LFU連結串列的開始位置(most  frequently used)。這麼做,那些真正被頻繁訪問的頁面將永遠呆在快取中,不經常訪問的頁面會向連結串列尾部移動,最終被淘汰出去。

隨著時間的推移,這兩個連結串列不斷的被填充,快取也相應的被填充。這時,快取已經滿了,而你讀進了一個沒有被快取的頁面。所以,我們必須從快取中淘汰一個頁面,為這個新的資料頁提供位置。這個資料頁可能剛剛才被從快取中淘汰出去,也就是說它不被快取中任何的非ghost連結串列引用著。

假設LRU連結串列已經滿了:

這時在LRU連結串列中,最近最少使用的頁面將會被淘汰出去。這個頁面的資訊會被放進LRU ghost連結串列中。

現在這個被淘汰的頁面不再被快取引用,所以我們可以把這個資料頁的資料釋放掉。新的資料頁將會被快取表引用。

隨著更多的頁面被淘汰,這個在LRU ghost中的頁面資訊也會向ghost連結串列尾部移動。在隨後的一個時間點,這個被淘汰頁面的資訊也會到達連結串列尾部,LRU連結串列的下一次的淘汰過程發生之後,這個頁面資訊也會從LRU ghost連結串列中移除,那是就再也沒有任何對它的引用了。

好的,如果這個頁面在被從LRU ghost連結串列中移除之前,被再一次訪問了,將會發生什麼?這樣的一個讀將會引起一次幽靈(phantom)命中。由於這個頁面的資料已經從快取中移除了,所以系統還是必須從後端儲存媒介中再讀一次,但是由於這個幽靈命中,系統知道,這是一個剛剛淘汰的頁面,而不是第一次讀取或者說很久之前讀取的一個頁面。ARC用這個資訊來調整它自己,以適應當前的I/O模式(workload)。

很顯然,這個跡象說明我們的LRU快取太小了。在這種情況下,LRU連結串列的長度將會被增加一。顯然,LFU連結串列的長度將會被減一。

但是同樣的機制存在於LFU這邊。如果一次命中發生在LFU ghost 連結串列中,它會減少LRU連結串列的長度(減一),以此在LFU 連結串列中加一個可用空間。

利用這種行為,ARC使它自己自適應於工作負載。如果工作負載趨向於訪問最近訪問過的檔案,將會有更多的命中發生在LRU Ghost連結串列中,也就是說這樣會增加LRU的快取空間。反過來一樣,如果工作負載趨向於訪問最近頻繁訪問的檔案,更多的命中將會發生在LFU Ghost連結串列中,這樣LFU的快取空間將會增大。

進一步,這種行為開啟了一個靈活的特性:假設你為處理log檔案而讀取了大量的檔案。你只需要每個檔案一次。一個LRU 快取將會把所有的資料快取住,這樣也就把經常訪問的資料也淘汰出去了。但是由於你僅僅訪問這些檔案一次,它們不會為你帶來任何價值一旦它們填滿了快取。

一個ARC快取的行為是不同的。顯然這樣的工作負載僅僅會很快填滿LRU連結串列空間,而這些頁面很快就會被淘汰出去。但是由於每個這樣的頁面僅僅被訪問一次,它們基本不太可能在為最近訪問的檔案而設計的ghost連結串列中命中。這樣,LRU的快取空間不會因為這些僅讀一次的頁面而增加。

假如你把這些log檔案與一個大的資料塊聯絡在一起(為了簡單起見,我們假設這個資料塊沒有自己的快取機制)。資料檔案中的資料頁應該會被頻繁的訪問。被LFU ghost連結串列引用的正在被訪問的頁面就很有可能大大的高於LRU ghost連結串列。這樣,經常被訪問的資料庫頁面的快取空間就會增加。最終,我們的快取機制就會向快取資料塊頁面優化,而不是用log檔案來汙染我們的快取空間。

Solaris ZFS ARC的改動(相對於IBM ARC

如我前面所說,ZFS實現的ARC和IBM提出的ARC淘汰演算法並不是完全一致的。在某些方面,它做了一些擴充套件:

·         ZFS ARC是一個快取容量可變的快取演算法,它的容量可以根據系統可用記憶體的狀態進行調整。當系統記憶體比較充裕的時候,它的容量可以自動增加。當系統記憶體比較緊張(其它事情需要記憶體)的時候,它的容量可以自動減少。

·         ZFS ARC可以同時支援多種塊大小。原始的實現假設所有的塊都是相同大小的。

·         ZFS ARC允許把一些頁面鎖住,以使它們不會被淘汰。這個特性可以防止快取淘汰一些正在使用的頁面。原始的設計沒有這個特性,所以在ZFS ARC中,選擇淘汰頁面的演算法要更復雜些。它一般選擇淘汰最舊的可淘汰頁面。

有一些其它的變更,但是我把它們留在對arc.c這個原始檔講解的演講中。

L2ARC

L2ARC保持著上面幾個段落中沒涉及到的一個模型。ARC並不自動地把那些淘汰的頁面移進L2ARC,而是真正淘汰它們。雖然把淘汰頁面自動放入L2ARC是一個看起來正確的邏輯,但是這卻會帶來十分嚴重負面影響。首先,一個突發的順序讀會覆蓋掉L2ARC快取中的很多的頁面,以至於這樣的一次突發順序讀會短時間內淘汰很多L2ARC中的頁面。這是我們不期望的動作。

另一個問題是:讓我們假設一下,你的應用需要大量的堆記憶體。這種更改過的Solaris ARC能夠調整它自己的容量以提供更多的可用記憶體。當你的應用程式申請記憶體時,ARC快取容量必須 變得越來越小。你必須立即淘汰大量的記憶體頁面。如果每個頁面被淘汰的頁面都寫入L2ARC,這將會增加大量的延時直到你的系統能夠提供更多的記憶體,因為你必須等待所有淘汰頁面在被淘汰之前寫入L2ARC。

L2ARC機制用另一種稍微不同的手段來處理這個問題:有一個叫l2arc_feed_thread會遍歷那些很快就會被淘汰的頁面(LRU和LFU連結串列的末尾一些頁面),並把它們寫入一個8M的buffer中。從這裡開始,另一個執行緒write_hand會在一個寫操作中把它們寫入L2ARC。

這個演算法有以下一些好處:釋放記憶體的延時不會因為淘汰頁面而增加。在一次突發的順序讀而引起了大量淘汰頁面的情況下,這些資料塊會被淘汰出去在l2arc——feed_thread遍歷到那兩個連結串列結尾之前。所以L2ARC被這種突發讀汙染的機率會減少(雖然不能完全的避免被汙染)。

結論

Adjustable Replacement Cache的設計比普通的LRU快取設計有效很多。Megiddo和 Modha用它們的Adaptive Replacement Cache得出了更好的命中率。ZFS ARC利用了它們的基本操作理論,所以命中率的好處應該與原始設計差不多。更重要的是:如果這個快取演算法幫助它們得出更好的命中率時,用SSD做大快取的想法就變得更加切實可行。

想了解更多?

1.     1.  The theory of ARC operation in One Up on LRU, written by Megiddo and Modha, IBM Almanden Research Center

2.     2.  ZFS ARC原始碼:arc.c