三款OLTP資料庫Cache設計之比較

作者| @趙裕眾

Oracle、MySQL、OceanBase三款面向OLTP場景的關係資料庫系統,它們的Cache設計有什麼異同,本文帶你一探究竟。

Oracle的Cache設計

Oracle的內存主要分為SGA(System Global Area)和PGA(Program Global Area)兩部分,SGA由所有服務及後台進程共享,PGA由每個服務及後台進程獨有。附一張Oracle官方文檔中的內存結構圖。

上圖中我們可以看到,在SGA中包含了很多不同的內存結構,如Buffer Cache、Redo Log Buffer、Shared Pool、Large Pool、Java Pool、Strems Pool等等。在Shared Pool中又包含了Library Cache、Data Dictionary Cache、Result Cache、Reserved Pool等數據結構。

單從Cache來說,在Oracle中有Buffer Cache(緩存數據頁)、Library Cache(緩存sql、存儲過程、計劃等對象)、Result Cache(緩存查詢結果)等多種不同類型的Cache,儘管它們在結構上來說都是Cache,但是在實現上卻非常不一樣。例如Buffer Cache中緩存的數據都是定長的內存頁,並且可能會對內存頁進行修改,修改後的頁就成為臟頁,留待後台進程在合適的時候將其刷到磁碟上;而Result Cache中緩存的是變長的查詢結果,通常是只讀的,當數據發生變更時需要使查詢結果失效。因此Oracle將Buffer Cache和其他Cache分在不同的模塊中進行管理,在Oracle 10g之前,需要DBA手工設置SGA中各個內存模塊如Buffer Cache、Redo Log Buffer、Shared Pool、Large Pool等的大小。在Oracle 10g中引入了Automatic Shared Memory Management(ASMM),DBA只需要設置SGA的總大小,Oracle可以對SGA內部各個模塊的內存使用進行自動調整;更進一步地,在Oracle 11g中引入了Automatic Memory Management(AMM),DBA只要設置總內存佔用,Oracle可以對SGA和PGA的大小進行自動調整。

通常來說,在Oracle中Buffer Cache會佔用大部分的內存,在結構上Buffer Cache是一個由定長內存塊(由參數DB_BLOCK_SIZE指定,默認為8KB)組成的鏈表,通過LRU演算法進行淘汰,同時由DBW進程將比較冷的臟頁定期刷到磁碟。在經典LRU演算法中,每次數據讀取都需要把記錄移動到鏈表頭,這意味著每次讀取都需要對鏈表加寫鎖,這對於高並發的數據讀取是不友好的,因此Oracle對經典的LRU演算法做了改進。在Oracle中,一條LRU鏈表在邏輯上被分為兩半,一半為熱鏈,一半為冷鏈。同時會在每個數據塊上記錄Buffer Touch Counts,當一個數據塊被Pin住時(通常是讀取或者是寫入),會判斷這個數據塊的Touch Counts是否在3s之前增加過,如果沒有增加過那麼就對Touch Counts加1,注意在這裡並沒有將數據塊移動到鏈表頭,所以也就不需要對LRU鏈表加鎖,提升了高並發下數據讀取的性能。當對Buffer Cache有新增寫入,但是Buffer Cache中沒有free的內存塊時,Oracle會先從冷鏈尾部尋找可以淘汰的內存塊,如果對應內存塊的Touch Counts較大,那麼就將這個內存塊移動到熱鏈上,同時Touch Counts歸0;如果對應內存塊的Touch Counts較小,那麼就會被淘汰用於新數據的寫入,新寫入的數據會被放入冷鏈頭。

MySQL的Cache設計

目前Innodb是MySQL的默認存儲引擎,因此下面我們只討論Innodb的Cache設計。Innodb的存儲引擎架構和Oracle是比較類似的,也有Buffer Pool來緩存數據頁(對應Oracle的Buffer Cache,默認頁大小16KB),以及Query Cache(對於Oracle的Result Cache)來緩存查詢結果,但是內存管理沒有Oracle做的那麼優雅,對於Buffer Pool及Query Cache的大小需要DBA進行手工設置。Buffer Pool中的數據頁同時承擔讀寫,當內存頁被修改後成為臟頁,當臟頁數量達到一定比例後會有後台線程負責把臟頁刷到磁碟上去。

Innodb的Buffer Pool也是基於LRU的,同樣也對經典的LRU演算法做了改進。Innodb的Cache淘汰演算法和Oracle非常類似,也是將一條LRU鏈表從邏輯上分為兩部分,一部分Innodb稱為Young Buffer,一部分稱為Old Buffer。Young Buffer存儲那些比較熱的數據,默認占整個Buffer的5/8;Old Buffer存儲那些比較冷的數據,默認占整個Buffer的3/8。當一個新頁被從磁碟讀取到Buffer Pool中時,會將其放到Old Buffer的頭部(也就是整個LRU鏈表的中間5/8的位置);當在Old Buffer中的page被再次訪問,那麼就將其提升到Young Buffer的頭部。注意到對Young Buffer中元素的頻繁訪問並不會對鏈表加鎖,也就提升了對熱數據高並發訪問的性能。

OceanBase的Cache設計

OceanBase的Cache設計與Oracle、MySQL相比有很大的不同。由於OceanBase存儲引擎是基於LSM-tree架構的,所有的修改只會寫入memtable,而sstable是只讀的,這就意味著OceanBase的Cache是一個只讀Cache,沒有刷臟頁的相關邏輯,這相對於傳統資料庫來說會簡單一些;但同時我們對存儲在sstable內的數據會進行數據編碼和壓縮,這意味著我們需要存儲的數據是變長而非定長的,和傳統資料庫相比Cache的內存管理又要複雜很多。除此之外,OceanBase還是一個面向多租戶的分散式資料庫系統,除了和傳統資料庫一樣要處理Cache的內存淘汰之外,在Cache內部還需要進行多租戶的內存隔離。

和Oracle與MySQL類似,在OceanBase內部,也會有很多種不同類型的Cache,除了用於緩存sstable數據的block cache(類似於Oracle和MySQL的buffer cache)之外,還有row cache(用於緩存數據行)、log cache(用於緩存redo log)、location cache(用於緩存數據副本所在的位置)、schema cache(用於緩存表的schema信息)、bloom filter cache(用於緩存靜態數據的bloomfilter,快速過濾空查)等等。類似於Oracle的AMM,我們設計了一套統一的Cache框架,所有不同租戶的不同類型的Cache都由框架統一管理。對於不同類型的Cache,會配置不同的優先順序,不同類型的Cache會根據各自的優先順序以及數據訪問熱度做相互擠占;對於不同租戶,會配置對應租戶內存使用的上限和下限,不同租戶的Cache會根據各自租戶的內存上下限以及Server整體的內存上限做相互擠占。

為了處理變長數據的問題,OceanBase的底層Cache框架將內存劃分為多個2MB大小的內存塊,對內存的申請和釋放都以2MB為單位進行。變長數據被簡單pack在2MB大小的內存塊內。為了支持對數據的快速定位,在Hashmap中存儲了指向對應數據的指針,整體結構如下圖所示:

Cache內存是以2MB為單位整體淘汰的,我們會根據每個2MB內存塊上各個元素的訪問熱度為其計算一個分值,訪問越頻繁的內存塊的分值越高,同時有一個後台線程來定期對所有2M內存塊的分值做排序,淘汰掉分值較低的內存塊。對於一個整體分值不高但是內部存在熱點數據的2MB內存塊,我們會將其熱點數據從它所在的「冷塊」移動到「熱塊」中,避免熱點數據被淘汰。在數據結構上我們並沒有維持類似於Oracle和MySQL的LRU鏈表,因此對於數據讀取,OceanBase的Cache訪問幾乎是無鎖的(除了HashMap上的Bucket鎖之外),對於熱點數據的高並發訪問更加友好。在淘汰時,我們會考慮租戶的內存上限及下限,控制各個租戶中Cache內存的用量。

如上圖所示,在測試的前75s,我們將tenant1的內存上限設置為3G,下限設置為2G,將tenant2的內存上限設置為4G,下限設置為3G;在後75s則將tenant1和tenant2的內存配置互換,可以看到租戶間內存使用可以得到比較好的隔離。

推薦閱讀:

七周成為數據分析師:SQL,從熟練到掌握
SequoiaDB擴容介紹與最佳實踐
傳統關係資料庫高可用的缺失
關係型資料庫 RDBMS 的舊與新 -- 談談 NewSQL
設計數據密集型應用-DDIA中文翻譯

TAG:資料庫 | 分散式存儲 |