做者:實驗室小陳 / 大數開放實驗室前端
在上一篇文章《內存數據庫解析與主流產品對比(一)》中,咱們介紹了基於磁盤的數據庫管理系統相關知識,並簡述了內存數據庫的技術發展。本篇文章將從數據組織和索引的角度來介紹內存數據庫的特色,並介紹幾款產品實際的技術實現。數據庫
— 數據庫管理系統中的數據組織—編程
定長Block VS 變長Block後端
內存數據庫在內存中對數據進行管理時,雖然再也不須要經過Slotted Page的形式對數據進行組織,但也不能在內存中任意爲數據分配地址空間,依然須要把數據組織成塊(Block/Page)的形式。傳統基於磁盤的DBMS採用Slotted Page的形式組織數據是爲了讀寫性能的考慮,由於磁盤接口是以Block/Page爲讀寫單位。而內存數據庫採用塊的方式組織數據是爲了便於尋址和管理,一般會將數據塊分爲定長數據塊(Fixed-Length Data Block)和變長數據塊(Variable-Length Data Block)兩種。緩存
假設一個數據集已經所有被加載進內存,爲了使用方便,內存數據庫在進行數據組織時會把記錄的定長的屬性所有分出來,放到定長數據塊;全部變長的屬性保存在另外的變長數據塊中。例如,一般將數據表中全部小於8個字節的屬性都放在定長數據塊中,將變長屬性和超過8個字節的屬性單獨放在變長數據塊中,並在定長數據塊中放一個指向其地址的指針。採用定長數據塊管理數據的好處是尋址快,能夠經過記錄長度和編號肯定記錄在數據塊中存儲的位置;記錄地址指針所須要的空間少,使得索引結構或其餘結構中存放這條記錄的內存地址最爲精簡,而且CPU作Pre-Fetch時預測較準。網絡
在傳統基於磁盤的DBMS中,索引葉子節點保存的記錄地址是Page ID + Offset,Page Table負責將Page ID映射到Buffer的Frame;內存數據庫中,索引的葉子節點保存的記錄地址則是直接的內存地址。在傳統基於磁盤的DBMS中,訪問Buffer中的Page時須要對Page進行加鎖/解鎖/修改鎖的操做,因爲現實系統中鎖(Latch)的類型可能會不少,一個線程若是要訪問一個Page,每每要加好幾種類型的Latch。如今內存數據庫中沒有了Buffer,所以就省去了Latch的開銷,性能上有很大提高。數據結構
數據組織:數據分區、多版本、行/列存儲多線程
在多核或多CPU共享內存的系統中,對數據的併發訪問衝突是始終存在的。目前的內存數據庫系統能夠分爲Partition System和Non-Partition System兩種。Partition System是把全部的數據切分紅互不相交的多個Partition,每個Partition被分配給一個核(或分佈式系統中的一個節點),全部操做都是串行執行,沒有併發的數據訪問,理想狀況下能夠得到最好的性能。但這類系統的缺點也很明顯,例如如何劃分Partition以及跨Partition的事務怎麼處理等。對於Non-Partition System,全部的核以及全部的線程均可以訪問全部的數據,所以必定會存在併發訪問衝突,必須採用支持併發訪問的數據結構。目前,通用數據庫更多的是採用Non-Partition System設計,之因此不採用Partition設計的主要緣由是:通用場景下很難對數據進行有效分區,Partition數據庫沒法使用。架構
在Non-Partition System中,若是兩個線程訪問同一個數據項會發生衝突,這時能夠考慮Multi-Version的解決方案。Multi-Version的優點在於能夠提升併發程度,其基本的思想是經過多版本的數據讓全部的讀操做不阻塞寫操做,從而提升整個系統的性能。對於那些讀多寫少的系統,Multi-Version性能會很好,但對於一些Write Heavy的系統,性能並不理想。併發
數據組織還有一個須要考慮的是Row和Column的組織形式。傳統數據庫系統在磁盤上維護數據時,分爲行式存儲和列式存儲。顧名思義,行式存儲是按行存儲數據,列式存儲是按列存儲數據。若是對少許記錄的全部屬性進行操做,行式存儲更加合適,若是隻讀大量記錄的部分列數據,則列式存儲性能比較好。好比一條記錄有100個屬性,本次讀操做須要讀取全部記錄的其中一個屬性,若是按行存儲,Block讀進來後還須要再篩選列;若是按列存儲,能夠只讀取這列數據所對應的Block,因此性能會比較好,適合去作統計分析。但內存數據庫不會有這個問題,全部數據都放在內存,不管行存仍是列存,訪問的代價是差很少的。因此在內存數據庫中,行存/列存是能夠作交換或任意選擇的。固然對於TP應用而言,更多的仍是用行存,由於能夠一次性把全部屬性都讀出來。但即便是列存,性能也並無在基於磁盤的數據庫系統中那麼糟糕。好比SAP HANA就是一個行列混合的存儲,前端的事務引擎是行存儲,經過合併整合之後,後端轉爲了列存儲。
— 內存數據庫系統對比—
接下來從數據組織的角度,簡要介紹一下4個具備表明性的系統:SQL Server的內存數據庫引擎Hekaton、慕尼黑工業大學的內存數據庫系統HyPer、SAP的HANA、圖靈獎得到者Michael Stonebraker的H-Store/VoltDB。
Hekaton
Hekaton是一個Non-Partition的系統,全部線程均可以訪問任意數據。Hekaton的併發控制不採用基於鎖的協議,而是利用多版本機制實現,每條記錄的每一個版本都有開始時間戳和結束時間戳,用於肯定該版本的可見範圍。
Hekaton中每一張表最多有8個索引,能夠是Hash或者Range索引。同時,全部記錄版本在內存中不要求連續存儲,能夠是非連續存儲(No-Clustering),經過指針(Pointer Link)將同一記錄的不一樣版本關聯起來。
上圖所示,圖中有一個包含姓名、城市和金額字段的表,姓名字段上有一個Hash索引,城市字段上有一個B-Tree索引。黑色箭頭表明姓名索引對應的指針,名字John對應的第一條記錄,指向下一個具備相同開頭字母名字的記錄。每條記錄包含有開始和結束時間戳,紅色表示存在一個事務正在更新記錄,事務提交後會替換結束的時間戳。B-Tree索引也是同理,藍色箭頭指針按照城市值串聯。
H-Store/VoltDB
H-Store/VoltDB是Partition System,每一個Partition部署在一個節點,每一個節點上的任務串行執行。H-Store/VoltDB沒有併發控制,但有簡單的鎖控制。一個Partition對應一把鎖,若是某事務要在一個Partition上執行,須要先拿到這個Partition的鎖,才能開始執行。爲了解決跨Partition執行問題,H-Store/VoltDB要求Transaction必須同時拿到全部相關Partition的鎖才能開始執行,至關於同時鎖住全部與事務相關的Partition。
H-Store/VoltDB採用兩層架構:上層是Transaction Coordinator,肯定Transaction是否須要跨Partition執行;下層是執行引擎負責數據的存儲、索引和事務執行,採用的是單版本的行存結構。
H-Store/VoltDB中的數據塊分爲定長和變長兩類:定長數據塊的每條記錄長度都相同,索引中採用8字節地址指向每條記錄在定長數據塊中的位置;變長屬性被保存在變長數據塊中,在定長數據塊的記錄中對應一個指針(Non-Inline Data),指向其在變長數據塊中具體的位置。在這種數據組織方式下,能夠用一個壓縮過的Block Look-Up Table來對數據記錄進行尋址。
HyPer
HyPer是多版本的Non-Partition System,每一個Transaction能夠訪問任何數據。同時HyPer是針對於HTAP業務創建的TP和AP混合處理系統。HyPer經過Copy on Write機制實現TP和AP混合處理。假設當前系統正在對數據集作事務處理,此時若是出現AP請求,HyPer會經過操做系統的Fork功能對數據集作Snapshot,隨後在快照上面作分析。Copy on Write機制並不會對內存中的全部數據進行復制,只有因OLTP業務致使數據發生變化時,快照纔會真正拷貝出原數據,而沒有變化的數據則經過虛擬地址引用到相同的物理內存地址。
此外,Hyper採用多版本控制,全部更新都是基於原記錄的,每條記錄都會維護一個Undo Buffer存儲增量更新數據,並經過Version Vector指出當前最新版本。所以,能夠經過Transaction找到被修改過的記錄,同時能夠經過反向應用增量數據來找回修改前的版本,固然也能夠對數據版本進行按期融合或恢復等操做。
SAP HANA
SAP HANA是一個Non-Partition的混合存儲系統,物理記錄在存儲介質中會通過三個階段:1. 事務處理的記錄存儲在L1-Delta(行存方式);2. 隨後記錄轉化爲列式並存儲在L2-Delta(列式存儲、未排序字典編碼);3. SAP HANA的主存是列存(高度壓縮並採用排序字典編碼)。每條記錄經歷着從行存到列存的映射合併,至關於一個多版本設計。
— 數據庫管理系統中的索引技術—
內存數據庫領域在設計索引時,主要是從面向緩存的索引技術(Cache-Awareness)和多核多CPU的並行處理(Multi-Core and Multi-Socket Parallelism)兩方面進行考慮。
因爲內存數據庫再也不有磁盤的I/O限制,所以索引目的轉變爲加速CPU和內存之間的訪問速度。雖然如今內存價格較低,可是內存速度的增速與CPU主頻的增速相差依然較多,所以對於內存數據庫,索引技術的目的是及時給CPU供數,以儘可能快的速度將所需數據放入CPU的Cache中。
對於多核多CPU的並行處理,80年代就開始考慮若是數據結構和數據都放在內存中,應該如何合理的構造索引。其中,1986年威斯康星大學的MM-DBMS項目提出了自平衡的二叉搜索樹T-Tree索引,每一個二叉節點中存儲取值範圍內的數據記錄,同時有兩個指針指向它的兩個子節點。T-Tree索引結構內存開銷較小,由於在80年代內存昂貴,因此主要的度量不在於性能是否最優,而是是否佔用最小內存空間。T-Tree的缺點是性能問題,須要按期地作負載平衡,而且掃描和指針也會對其性能產生影響。早期商業系統如Times Ten中,採用的即是T-Tree的數據結構。
那麼索引的設計爲何須要考慮Cache-Awareness呢?1999年有研究發現內存訪問中的Cache Stall或者Cache Miss是內存系統最主要的性能瓶頸。該研究進行了一項性能測試,經過對A/B/C/D 4個系統評測,測試如下過程的時間佔比:Computation、Memory Stalls、Branch Mispredicitons和Resource Stalls。Computation表示真正用於計算的時間;Memory Stall是等待內存訪問的時間;Branch Mispredicitons是指CPU指令分支預測失敗的開銷;Resource Stalls是指等待其餘資源的時間開源,如網絡、磁盤等。
能夠看到Memory Stall在不一樣的測試場景都會佔據較大比例開銷。所以對於內存索引結構來講,發展面向緩存的索引的主要目的就是爲了減小Memory Stall的開銷。
CSB+-Tree
這裏介紹幾個典型的內存索引結構例子。第一個是CSB+-Tree,它在邏輯上仍然是B+-Tree,可是作了一些變化。首先每一個Node的大小是一個Cache Line長度的倍數;同時CSB+-Tree將一個節點的全部的子節點組織成Children Group,一個父節點經過一個指針指向它的Children Group,目的是減小數據結構中的指針數量。由於CSB+-Tree的節點與Cache Line長度相匹配,只要依序讀取,就能夠達到較好的pre-fetche性能。當樹分裂時,CSB+-Tree會對內存中的Group從新分配位置,由於CSB+-Tree節點在內存中不須要連續,排好後再建立新的指針連接就能夠。
PB+-Trees
另外一個例子是PB+-Trees(Pre-fetching B+-Tree)。它並非新的結構,只是在內存中實現了B+-Tree,每一個節點的大小等於Cache Line的長度倍數。PB+-Trees比較特殊的是在整個系統實現過程當中,引入了Pre-fetching,經過加入一些額外信息幫助系統預取數據。
PB+-Trees傾向於採用扁平的樹來組織數據,論文中給出了它Search和Scan的性能,其中Search性能提升1.5倍,Scan上提升了6倍。處理Search時的性能相比CSB+-Tree,PB+-Trees的Data Cache Stalls佔比更小。
另一個性能對比是,當沒有采用預取時,讀取一個Node大小等於兩個Cache Line的三級索引須要900個時鐘週期,而加了預取後僅須要480個週期。PB+-Trees還有一個實現是,它會在每一個節點加上Jump Pointer Array,用來判斷作掃描時要跳過多少Cache Line以預取下一個值。
Bw-Tree
Bw-Tree是Hekaton系統中使用的索引,基本思想是經過Compare-and-Swap指令級原子操做比較內存值,若是新舊值相等就更新,若是不等則不更新。好比原值爲20(存儲在磁盤),而內存地址對應30,那麼要是把30更新成40就不會成功。這是一個原子操做,可用於在多線程編程中實現不被打斷的數據交換操做。
Bw-Tree中存在Mapping Table,每個節點都在Mapping Table中有一個存儲位置,Mapping Table會存儲節點在內存中的地址。對於Bw-Tree來說,從父節點到子節點的指針並非物理指針,而是邏輯指針,也就是記錄在Mapping Table中的位置並非真正的內存位置。
Bw-Tree採用的設計是節點的更新並非直接修改節點,而是經過增長Delta Record(增量記錄)來保存修改的內容,而後在Mapping Table中指向Delta Record,若是有新的更新就繼續指向新的Delta Record。在讀取一個節點的內容時,其實是合併全部的Delta Record。由於對Bw-Tree的更新是經過一個原子操做來實現的,發生競爭時只有一個改動能成功,所以是一種Latch-Free結構,只須要靠Compare-and-Swap就可以解決競爭問題,再也不須要依賴鎖機制。
Adaptive Radix Tree
Hyper的索引樹的設計採用了Adaptive Radix Tree。傳統Radix Tree是一個前綴樹,其優點是樹的深度不依賴於被索引的值的個數,而是靠Search Key的長度來決定。它的缺點是每個節點都要維護可能取值的子節點的信息,致使每一個節點的存儲開銷較大。
而在Adaptive Radix Tree中,爲每一個節點提供了不一樣類型長度的格式,分別能夠保存4/16/48/256等不一樣個數的子節點。Node4爲最小的節點類型,最多可存儲4個子節點指針, Key用來表示節點所存儲的值,指針能夠指向葉子節點,也能夠指向下一層內部節點。Node16 和Node4 結構上一致,但 Node16 能夠存放16個 unsigned char 和16個指針,在存放第17個key時則須要將其擴大爲 Node48。Node48結構上和 Node4/16 有所不一樣,有256個索引槽和48個指針,這256個索引槽對應 unsigned char 的0-255,每一個索引槽的值對應指針的位置,分別爲 1-48,若是某個字節不存在的話,那麼它的索引槽的值就是0。當存放第49個key byte 時須要將其擴大爲 Node256。Node256結果較爲簡單,直接存放256個指針,每一個指針對應 unsigned char 的0-255 區間。
好比說在這個例子裏,咱們要索引一個整數(+218237439),整數的二進制表示形式爲32位,隨後將32位bit轉換爲4個Byte,這4個byte十進制表示爲1三、二、九、255,這就是它的Search Key。在索引樹中,第一層爲Node 4,13符合這一層的存儲要求,因而就保留在第一層節點上,後面的位數則進入下一層存儲,下一層爲Node 48,用來存儲2;接下來的每一位都存儲到下面的每一層。因爲本例子中整數爲4個字節表示,故共有4層。能夠看到,每一個節點的結構是不同的,根據字節位數和順序逐一存儲,數值在這一層目前有多少個不一樣的值,就選擇什麼類型的節點。若是當前類型的不夠用,能夠再增長個數,每一個節點能夠容納的 key 是動態變化的,這樣既能夠節省空間,又能夠提升緩存局部性。
另外Adaptive Radix還採用了路徑壓縮機制,若是一條路徑的父節點只有一個子節點就會將之壓縮合並。Adaptive Radix之因此採用這樣的索引結構,是由於每一個節點的大小都等於一個Cache Line,全部操做能夠在一個Cache Line的基礎上實現。
OLFIT on B+-Trees
OLFIT on B+-Trees(Optimistic Latch Free Index Access Protocol)是HANAP*Time採用的索引技術,可以在多核數據庫上保證CPU的Cache Coherence。在多處理器計算機的體系結構中,多個CPU的Cache會緩存同一內存的數據。在內存數據庫中,存儲的數據會先讀到對應Cache再處理;若是緩存數據處理過程當中內存數據發生變化,那Cache的數據會因與內存數據不一致而失效,Cache Coherence就是解決這個不一樣步的問題。
考慮這樣一個場景:以下圖所示,內存中有一個樹形數據結構,由4個CPU處理,每一個CPU有本身的Cache。假設CPU-P1先讀了n一、n二、n4,Cache中便有了n一、n二、n4。隨後CPU-P2讀n一、n2和n5時,假設這個數據結構不是Latch-Free,若是在讀的同時且容許修改,就須要一個Latch來在讀的時候上鎖,讀完再釋放。由於內存數據庫中Latch和數據放在一塊兒,數據雖然沒有變化,可是Latch的狀態發生了改變,計算機的硬件結構會認爲內存發生了變化。因此,當多個CPU讀一樣的數據時,只有最後一次讀取狀態是有效的,前序的讀取會被認爲失效。這就會出現即便都是進行讀操做,可是由於Latch狀態改變致使CPU的Cache失效。所以OLFIT設計了一套機制,要求寫操做申請Latch,讀操做不須要。OLFIT經過版號維護讀寫事務,每一個CPU讀前先把版本號拷貝到本地寄存器,而後等讀完數據後,再判斷此時版本號跟讀前的是否同樣。若是同樣就繼續正常執行,不同就說明Cache失效。所以,讀請求不會引發其餘CPU的Cache失效。
經過這個例子能夠看到,內存數據庫考慮的問題和基於磁盤的數據庫是不同的,沒有了磁盤I/O的因素,就須要考慮其餘方面對性能的限制。
Skiplists
Skiplists是MemSQL的數據處理引擎所用到的技術,它的最底層是一個有序的列表,上層按照必定的機率(P-value)抽取數據,再作一層列表。進行大列表搜索時,從最上層開始一層層遞進,相似於二分查找,粒度能夠根據實際狀況自定義。之因此這樣設計是由於全部對列表的插入操做,都是能夠經過Compare-and-Swap原子操做完成,從而省去了鎖的開銷。
— 本文小結—
本文首先介紹了內存數據庫的數據組織,分別從數據劃分,Partition/Non-Partition的系統差別和存儲方式進行介紹,並對比了四款產品的實際實現。隨後,介紹了六種內存數據庫系統的索引技術,並經過例子簡述了索引查詢原理。下一篇文章將繼續對內存數據庫進行剖析,從併發控制、持久化存儲和編譯查詢的角度,討論內存數據庫對於查詢性能和可用性的優化設計。
注:本文相關內容參照如下資料:
1. Pavlo, Andrew & Curino, Carlo & Zdonik, Stan. (2012). Skew-aware automatic database partitioning in shared-nothing, parallel OLTP systems. Proceedings of the ACM SIGMOD International Conference on Management of Data. DOI: 10.1145/2213836.2213844.
2. Kemper, Alfons & Neumann, Thomas. (2011). HyPer: A hybrid OLTP&OLAP main memory database system based on virtual memory snapshots. Proceedings - International Conference on Data Engineering. 195-206. DOI: 10.1109/ICDE.2011.5767867.
3. Faerber, Frans & Kemper, Alfons & Larson, Per-Åke & Levandoski, Justin & Neumann, Tjomas & Pavlo, Andrew. (2017). Main Memory Database Systems. Foundations and Trends in Databases. 8. 1-130. DOI: 10.1561/1900000058.
4. Sikka, Vishal & Färber, Franz & Lehner, Wolfgang & Cha, Sang & Peh, Thomas & Bornhövd, Christof. (2012). Efficient Transaction Processing in SAP HANA Database –The End of a Column Store Myth. DOI: 10.1145/2213836.2213946.
5. Diaconu, Cristian & Freedman, Craig & Ismert, Erik & Larson, Per-Åke & Mittal, Pravin & Stonecipher, Ryan & Verma, Nitin & Zwilling, Mike. (2013). Hekaton: SQL server's memory-optimized OLTP engine. 1243-1254. DOI: 10.1145/2463676.2463710.