openGauss儲存技術(二)——列儲存引擎和記憶體引擎

Gauss松鼠會發表於2022-11-09


openGauss列儲存引擎

傳統行儲存資料壓縮率低,必須按行讀取,即使讀取一列也必須讀取整行。在分析性的作業以及業務負載的情況下,資料庫往往會遇到針對大量表的複雜查詢,而這種複雜查詢中往往僅涉及一個較寬(表列數較多)的表中個別列。此類場景下,行儲存以行作為操作單位,會引入與業務目標資料無關的資料列的讀取與快取,造成了大量IO 的浪費,效能較差。因此openGauss提供了列儲存引擎的相關功能。建立表的時候,可以指定行儲存還是列儲存。

總體來說,列儲存有以下優勢:

  • 列的資料特徵比較相似,適合壓縮,壓縮比很高,在資料量較大(如資料倉儲) 場景下會節省大量磁碟空間,同時也會提高單位作業下的IO 效率。
  • 當表中列數比較多,但是訪問的列數比較少時,列儲存可以按需讀取列資料,大大減少不必要的讀IO,提高查詢效能。
  • 基於列批次資料向量運算,結合向量化執行引擎,CPU 的快取命中率比較高,效能比較好,更適合 OLAP大資料統計分析的場景。
  • 列儲存表同樣支援 DML操作和 MVCC,功能完備,且在使用角度上做了良好的相容,基本是對使用者透明的,方便使用。

(一)列儲存引擎的總體架構

列儲存引擎的儲存基本單位是 CU(Compression Unit,壓縮單元),即表中一列的一部分資料組成的壓縮資料塊。行儲存引擎中是以行作為單位來管理,而當使用列儲存時,整個表整體按照不同列劃分為若干個 CU,劃分方式如圖1所示。

在這裡插入圖片描述

圖1 CU 劃分方式

如圖1所示,假設以6萬行作為一個單位,則一個12萬行、4列寬的表被劃分為8個 CU,每個 CU 對應一個列上的6萬個列資料。圖中有列0、列1、列2、列3四列,資料按照行切分了兩個行組(Row Group),每個行組有固定的行數。針對每個行組按照列做資料壓縮,形成 CU。每個行組內部各個列的 CU 的行邊界是完全對齊的。當然,大部分時候,CU 在經過壓縮後,因為資料特徵與壓縮率的不同,檔案大小會完全不同,如圖2所示。
在這裡插入圖片描述

圖2 示意圖

為了管理表對應的CU,與執行器層進行對接來提供各種功能,列儲存引擎使用了CUDesc(壓縮單元描述符)表來記錄一個列儲存表中CU 對應的元資訊,如圖3所示。
在這裡插入圖片描述

圖3 列儲存引擎整體架構圖

注:Cmn表示第 m 列的、CUid是n(第n個)的壓縮單元。每個 CU 對應一個 CUDesc的記錄,在 CUDesc裡記錄了整個 CU 的事務時間戳資訊、CU 的大小、儲存位置、magic校驗碼、min/max等資訊。

與此同時,每張列儲存表還配有一張 Delta表,Delta表自身為行儲存表。當有少量的資料插入到一張列儲存表時,資料會被暫時放入 Delta表,等到到達閾值或滿足一定條件或操作時再行整合為 CU 檔案。Delta表可以幫助避免單點資料操作帶來的加重的 CU 操作與開銷。

設計採用級別的多版本併發控制,刪除透過引入虛擬列對映 (Virtual Column Bitmap)來標記刪除。對映(Bitmap)是多版本的。

(二)列儲存的頁面組織結構

上文講到了CUDesc表及其用來記錄元資訊的目的。CUDesc的典型結構如圖4所示。

在這裡插入圖片描述

圖4 CUDesc的典型結構

其中:

  • _rowTupleHeader為傳統行儲存記錄的行頭,其中包含了前面提到過的事務及位置資訊等,用來進行可見性判斷等。
  • cu_mode實際為此 CUDesc對應 CU 的infomask,記錄了一些 CU 的特徵資訊(比如是否為 Full,是否有 NULL等)。
  • magic是 CUDesc與 CU 檔案之間校驗的關鍵資訊。
  • min/max(最小值/最大值)為稀疏索引,後續會進一步展開介紹。
    CU 檔案結構如圖5所示。

在這裡插入圖片描述

圖5 檔案結構

列儲存在 CUDesc表的儲存資訊基礎上設計了一套與上層互動的操作 API。除了上面列儲存的頁面組織結構以及檔案管理中天然可以展示出的結構機制之外,列儲存還有如下一些關鍵的技術特徵:

  • 列儲存的 CU 中資料的刪除,實際上是標記的刪除。刪除操作,相當於更新了CUDesc表中CU 對應CUDesc記錄的刪除點陣圖(delete bitmap)結構,標記列中某行對應資料已被刪除,而CU 檔案資料不會被更改。這樣可以避免刪除操作帶來大量的IO開銷及壓縮、解壓的高額 CPU 開銷。這樣的設計,也可以使得對於同一個 CU 的查詢(select)和刪除(delete)互不阻塞,提升併發能力。
  • 列儲存CU 中資料更新,則是遵循僅允許追加(append-only)原則的,即CU 檔案僅會向後進行延展擴充,抑或是啟用新的 CU 檔案,而不是就對應行在 CU 中的位置就地更新。
  • 由於 CU 以及 CUDesc的後設資料管理模式,原有系統中的 Vacuum 機制實際上並不會非常有效地清除 CU 中已經失效的儲存空間,因為 LazyVacuum(清理資料時,只是標識無用行的狀態,使得空間可以複用,不會影響對錶資料的操作)僅能在CUDesc級別進行操作,在多數場景下無法對 CU檔案本身進行清理。列儲存內部如果要對列儲存資料表進行清理,需要執行 VacuumFull(除了清理無用行,還會合並資料塊,整個過程會鎖定表)操作。

(三)列儲存的 MVCC設計

理解了 CU、CUDesc的基本結構,以及 CUDesc的管理,或者說是其“代理”角色,列儲存的 MVCC設計以及管理,實際上就非常好理解了。

由於列儲存的操作基本單位 CU 是由 CUDesc表中的行進行管理的,因此列儲存表的CU 可見性判斷也是由CUDesc的行頭資訊,按照傳統的行儲存可見性進行判斷的。

同樣的,列儲存可見性的單位也是CU 級別(CUDesc),不同於行儲存的 Tuple級別。

列儲存表的併發控制是 CU 檔案級別的,實際上也等同於其 CUDesc代理表的CUDesc行之間的併發控制。多個事務之間在一個 CU 上的併發管控,實際上取決於其在對應的 CUDesc記錄上是否衝突。例如:

  • 兩個事務併發去讀一個CU 是可行的,兩個事務都可以拿到此CU 對應 CUDesc 行級別的共享鎖(sharelock)。
  • 兩個事務併發去更新一個 CU,會因為在 CUDesc上的鎖衝突而觸發一個事務回滾[當然,如果是讀已提交(read committed)隔離級別並開啟允許併發更新的開關,這裡會做的事情是拿到此 CUDesc最新版 本 的 ctid,然後重執行一部分查詢樹 (queryTree)來進行更新操作。此部分內容,後面文章將會介紹]。
  • 兩個事務並行執行,一個事務對一個 CU 執行了刪除操作並先行提交,則另一個事務在可重讀(repeatableread)的隔離級別下,其獲取的快照只能看到這個CUDesc在操作發生前的版本,這個版本的 CUDesc中的刪除點陣圖(delete_bitmap)對應資料沒有被標記刪除,也由於 CU 的行刪除是標記刪除的機制,因此資料在原有 CU 的資料檔案中依舊可用,此事務依舊可以在其對應的快照下讀到對應行。

刪除 CU 中部分資料所進行的實際操作如圖6所示。

圖6 刪除 CU 中部分資料所進行的實際操作

從上面的幾個例子可以看出,列儲存對於更新的僅允許追加策略以及對於刪除操作的標記刪除方式,對於列儲存事務 ACID的支援,是至關重要的。

(四)列儲存的索引設計

列儲存支援的索引設計有:

  • B樹索引;
  • 稀疏索引;
  • 聚簇索引。

1.列儲存的B樹索引

列儲存引擎在 B樹索引的支援角度,與傳統的行儲存引擎無本質差別。對於一般用於應對大資料批次分析性負載的列儲存引擎來說,B樹索引有助於幫助列儲存大大提升自身的點查效率,更好地適應混合負載。

行儲存相關 B樹索引的索引頁面上,儲存的是key→ctid(鍵→行號)的對映,在列儲存的場景下,這個對映依舊為key→ctid,但列儲存的結構並不能像行儲存一樣,透過ctid中的塊號(block number)和偏移量(offset)直接找到此行資料在資料檔案頁面中的位置。列儲存ctid中記錄的是(cu_id,offset),要透過 CUDesc結構來進行查詢。

在基於 B樹索引的掃描中,從索引中拿到ctid後,需要在對應的 CUDesc表中,根據 CUDesc在cu_id列的索引找到對應的 CUDesc記錄,並由此開啟對應的 CU 檔案,根據偏移量找到資料。

如果此操作設計大量的儲存層效能開銷,因此列儲存的 B樹索引,與列儲存的其他操作一樣,統一都為批次操作,會根據 B樹索引找到ctid的集合,然後對此集合進行排序,再批次地對排序後的ctid進行 CU 檔案級別的查詢與操作。這樣可以做到順序單調地進行索引遍歷,大大減少了反覆操作檔案帶來的 CPU 以及IO 開銷。

2.列儲存的稀疏索引

列儲存引擎每個列自帶 min/max稀疏索引,每個CUDesc儲存該CU 的最小值和最大值。

那麼在查詢的時候,可以根據查詢條件做簡單的 min/max判斷,如果查詢條件不在(min,max)範圍內,肯定不需要讀取這個 CU,可以大大地減少IO 讀取的開銷,稀疏索引如圖7所示。

在這裡插入圖片描述

圖7 稀疏索引

注:txn_info表示事務資訊;CUPtr表示壓縮單元的指標;CU-None表示肯定不命中;CU-Some表示可能有資料匹配;CU_Full表示壓縮單後設資料全命中。

3.列儲存的聚簇索引

列儲存表在建立時可以選擇在列上建立聚簇索引(partial sort index)。

如果業務的初始資料模型較為離散,那麼稀疏索引在不同 CU 之間的 min、max會有大量交集,這種情況下在給定謂詞對列儲存表進行檢索的過程中,會出現大量的CU 誤讀取,甚至可能導致其查詢效率與全表掃描近似。如圖8所示,查詢2基本命中了所有 CU,min/max索引沒有能夠有效篩選。

在這裡插入圖片描述

圖8資料模型較為離散時的查詢效果圖

聚簇索引可以對部分割槽間內的資料做相應的排序(一般區間會包含多個CU所覆蓋的行數),可以保證 CU 之前交集儘量少,可以極大地提升在資料離散場景下稀疏索引的效率。

其示意圖如圖9和圖10所示。
在這裡插入圖片描述

圖9 聚簇索引生效前

在這裡插入圖片描述

圖10 聚簇索引生效後

同時,聚簇索引會使得 CU 內部的資料臨近有序,提升 CU 檔案本身的壓縮比以及壓縮效率。

(五)列儲存自適應壓縮

每個列自適應選擇壓縮,支援差分編碼(delta value encoding)、遊 程 編 碼 (Run length encoding)、字典編碼(dictionary encoding)、LZ4、zlib等混合壓縮。根據資料特性的不同,壓縮比一般可以有3X~20X。

列儲存引擎支援低、中、高三種壓縮級別,使用者在建立表的時候可以指定壓縮級別。

匯入1TB原始資料量,分別測試低、中、高三種壓縮級別,入庫後資料大小分別是100GB、73GB、61GB,如圖11所示。

在這裡插入圖片描述

圖11 壓縮比示意圖

每次資料匯入,首先對每列的資料按照向量組裝,對前幾批資料做取樣壓縮,根據數值型別和字串型別,選擇嘗試不同的壓縮演算法。一旦取樣壓縮完成後,接下來的資料就選擇優選的壓縮演算法了。如圖12所示,面向列的自適應壓縮主要分為數值壓縮和字元壓縮。其中對 Numeric小數型別,會轉換為整數後,再進行數值壓縮。對數值型字串,也會嘗試轉換為整數再進行數值壓縮。

在這裡插入圖片描述

圖12 面向列的自適應壓縮

(六)列儲存的持久化設計

在列儲存的組織結構與 MVCC機制的介紹中提到,列儲存的儲存單位由 CUDesc和CU檔案共同組成,其中 CUDesc記錄了CU相關的元資訊,控制其可見性,實際上充當了一個 “代 理”的角色。但是CUDesc和CU,實質上還是分離的檔案狀態。CUDesc表本質上還是行儲存表,其持久化流程遵從行儲存的共享緩衝區髒頁與 Redo日誌的持久化流程,在事務提交前,CUDesc的改動會被記錄在 Redo日誌中進行持久化。單個 CU 檔案本身,由於含有大量的資料,使用正常的事務日誌進行持久化需要消耗大量的事務日誌,引入非常大的效能開銷,並且恢復也十分緩慢。因此根據其應用場景,僅允許追加(append-only)的屬性及與 CUDesc的對應關係,列儲存的 CU 檔案,為了確保 CUDesc和 CU 持久化狀態的一致,在事務提交、CUDesc對應事務日誌持久化前,會先行強制刷盤(Fsync),來確保事務改動的持久化。

由於資料庫主備例項的同步也依賴事務日誌,而 CU 檔案並不包含在事務日誌內,因此在與列儲存同步時,主備例項之間除去正常的日誌通道外,還有連線的資料通道,用於傳輸列儲存檔案。CUDesc的改動會透過日誌進行同步,而 CU 檔案則會被直接透過資料通道傳輸到備機例項,並透過 BCM(bitchangemap)檔案來記錄主備例項之間檔案的同步狀態。

openGauss記憶體引擎

記憶體引擎作為在openGauss中與傳統基於磁碟的行儲存、列儲存並存的一種高效能儲存引擎,基於全記憶體態資料儲存,為openGauss提供了高吞吐的實時資料處理分析能力及極低的事務處理時延,在不同業務負載場景下可以達到其他引擎事務處理能力的3~10倍。

記憶體引擎之所以有較強的事務處理能力,並不單是因為其基於記憶體而非磁碟所帶來的效能提升,而更多是因為其全面地利用了記憶體中可以實現的無鎖化的資料及索引結構、高效的資料管控、基於 NUMA 架構的記憶體管控、最佳化的資料處理演算法及事務管理機制。

值得一提的是,雖然是全記憶體態儲存,但是並不代表著記憶體引擎中的處理資料會因為系統故障而丟失。相反,記憶體引擎有著與openGauss的原有機制相相容的並行持久化、檢查點能力,使得記憶體引擎有著與其他儲存引擎相同的容災能力以及主備副本帶來的高可靠能力。

記憶體引擎總體架構如圖13所示。

在這裡插入圖片描述

圖13 記憶體引擎總體架構圖

可以看到,記憶體引擎透過原有的 FDW(Foreign Data Wrapper,外部資料封裝器) 擴充套件能力與 openGauss 的最佳化執行流程相互動,透過事務機制的回撥以及與 openGauss相相容的 WAL機制,保證了與其他儲存引擎在這一體系架構內的共存,保 證了整體對外的一致表現;同時透過維護內部的記憶體管理結構、無鎖化索引、樂觀事務 機制來為系統提供極致的事務吞吐能力。

以下將逐步展開講解相關關鍵技術點與設計。

(一)記憶體引擎的相容性設計

由於資料形態的不同以及底層事務機制的差別,此處如何與一個以段頁式為基礎的系統對接是記憶體引擎存在於openGauss中的重點問題之一。

此處openGauss原有的 FDW 機制為記憶體引擎提供了一個很好的對接介面,最佳化器可以透過 FDW 來獲取記憶體引擎內部的元資訊,記憶體引擎的記憶體計算處理機制可以直接透過 FDW 的執行器介面運算元實現直接調起,並透過相同的結構將結果以符合執行器預期的方式[比如掃描(Scan)操作的流水線(pipelining)]將結果反饋回執行器進行進一步處理[如排序、分組(Groupby)]後返回給客戶端應用。

與此同時記憶體引擎自身的錯誤處理機制(ErrorHandling),也可以透過與FDW的互動,提交給上次的系統,以此同步觸發上層邏輯的相應錯誤處理(如回滾事務、執行緒退出等)。

記憶體引擎藉助 FDW 的方式接近無縫地工作在整個系統架構下,與以磁碟為基礎的行、列儲存引擎實現共存。

在記憶體引擎中建立表(CreateTable)的實際操作流程如圖14所示。

在這裡插入圖片描述

圖14 記憶體引擎建立表的操作流程圖

從圖中可以看到,FDW 充當了一個整體互動 API的作用。實現中同時擴充套件了FDW 的機制,使其具有更完備的互動功能,具體包括:

  • 支援 DDL介面;
  • 完整的事務生命週期對接;
  • 支援檢查點操作;
  • 支援持久化 WAL;
  • 支援故障恢復(Redo);
  • 支援 Vacuum 操作。

藉由 FDW 機制,記憶體引擎可以作為一個與原有openGauss程式碼框架異構的儲存引擎存在於整個體系中。

(二)記憶體引擎索引

記憶體引擎的索引結構以及整體的資料組織都是基於 Masstree實現的。其主體結構如圖15所示。

在這裡插入圖片描述

圖15 記憶體引擎索引主體結構

圖15很好地呈現了記憶體引擎索引的組織架構。主鍵索引(primary index)在記憶體引擎的一個表中是必須存在的要素,因此要求表在組織時儘量存在主鍵索引;如果不存在,記憶體引擎也會額外生成代理鍵(surrogatekey)用於生成主鍵索引。主鍵索引指向各個代表各個行記錄的行指標(sentinel),由行指標來對行記錄資料進行記憶體地址的記錄以及引用。二級索引(secondaryindex)索引後指向一對鍵值,鍵的值(value)部分為到對應資料行指標的指標。

Masstree作為並行 B+樹(Concurrent B+tree),整合了大量 B+樹的最佳化策略,並在此基礎上做了進一步的改良和最佳化,其大致實現方式如圖16所示。
在這裡插入圖片描述

圖16 Masstree實現方式

相比於傳統的 B樹,Masstree實際上是一個類似於諸多 B+樹以字首樹(trie)的組織形式堆疊的基數樹(radix tree)模式,以鍵(key)的字首作為索引,每k 個位元組形成一層 B+ 樹結構,在每層中處理鍵中這k 個 字 節 對 應 所 需 的INSERT/LOOKUP/ UPDATE/DELETE流程。圖17為k=8時情況。

在這裡插入圖片描述

圖17 k等於8時的Masstree

Masstree中的讀操作使用了類 OCC(OptimisticConcurrency Control,樂觀併發控制)的實現,而所有的更新(update)鎖僅為本地鎖。在樹的結構上,每層的內部節點(interior node)和葉子節點(leaf node)都會帶有版本,因此可以藉助版本檢查(version validation)來避免細粒度鎖(fine-grained lock)的使用。

Masstree除了無鎖化(lockless)之外,最大的亮點是快取塊(cache line)的高效利用。無鎖化本身在一定程度避免了 LOOKUP/INSERT/UPDATE 操作互相失效共享快取塊(invalidat ecacheline)的情況。而基於字首(prefix)的分層,輔以合適的每層中 B+樹扇出(fanout)的設定,可以最大限度地利用 CPU 預取(prefetch)的結果(尤其是在樹的深度遍歷過程中),減少了與 DRAM 互動所帶來的額外時延。

預取在 Masstree的 設 計 中 顯 得 尤 為 關 鍵,尤 其 是 在 Masstree 從 根 節 點 (tree root)向葉子節點遍歷,也就是樹的下降過程中。此過程中的執行時延大部分由於記憶體

互動的時延組成,因此預取可以有效地提高遍歷(masstreetraverse)操作的執行效率以及快取塊的使用效率(命中)。

(三)記憶體引擎的併發控制

記憶體引擎的併發控制機制採用 OCC,在運算元據衝突少的場景下,併發效能很好。

記憶體引擎的事務週期及併發管控元件結構,如圖18所示。

在這裡插入圖片描述

圖18 記憶體引擎的事務週期及併發管控元件結構

這裡需要解釋一下,記憶體引擎的資料組織為什麼整體是一個接近無鎖化的設計。

除去以上提到的 Masstree本身的無鎖化機制外,記憶體引擎的流程機制也進一步最小化了併發衝突的存在。

每個工作執行緒會將事務處理過程中所有需要讀取的記錄,複製一份至本地記憶體,儲存在讀資料集(read set)中,並在事務的全程基於這些本地資料進行相應計算。相應的運算結果儲存在工作執行緒本地的寫資料集(writeset)中。直至事務執行完畢,工作執行緒會進入嘗試提交流程,對讀資料集和寫資料集進行檢查驗證(validate)操作並在允許的情況下對寫資料集中資料對應的全域性版本進行更新。

這樣的流程,是把事務流程中對於全域性版本的影響縮小到檢查驗證的過程,而在事務進行其他任何操作的過程中都不會影響到其他的併發事務,並且在僅有的檢查驗證過程中,所需要的也並不是傳統意義上的鎖,而僅是記錄頭部資訊中的代表鎖的數位(lock bit)。相應的這些考慮,都是為了最小化併發中可能出現的資源爭搶以及衝突,並更有效地使用 CPU 快取。

同時讀資料集和寫資料集的存在可以良好地支援各個隔離級別,不同隔離級別可以透過在檢查驗證階段對讀資料集和寫資料集進行不同的審查機制來獲得。透過檢查兩個資料集(set)中行記錄在全域性版本中對應的鎖定位(lock bit)以及行頭中的TID結構,可以判斷自己的讀、寫與其他事務的衝突情況,進而判斷自己在不同隔離級別下是否可以提交(commit)或是終止(abort)。同時由於 Masstree的 Trie節點(node)中存在版本記錄,Masstree的結構性改動(insert/delete,插入/刪 除)操作會更改相關Trie節點上面的版本號。因此維護一個範圍查詢(Range query)涉及的節點集(node set),並在檢查驗證(validation)階段對其進行對比校驗,可以比較容易地在事務提交階段檢查此範圍查詢所涉及的子集是否有過變化,從而能夠檢測到幻讀(Phantom)的存在,這是一個時間複雜度很低的操作。

(四) 記憶體引擎的記憶體管控

由於記憶體引擎的資料是全記憶體態的,因此可以按照記錄來組織資料,不需要遵從頁面的資料組織形式,從而從資料操作的衝突粒度這一點上有著很大優勢。擺脫了段頁式的限制,不再需要共享快取區進行快取以及與磁碟間的互動淘汰,設計上不需要考慮IO 以及磁碟效能的最佳化[比如索引 B+樹的高度以及 HDD(HardDiskDrive,磁碟)對應的隨機讀寫問題],資料讀取和運算就可以進行大量的最佳化和併發改良。

由於是全記憶體的資料形態,記憶體資源的管控就顯得尤為重要,記憶體分配機制及實現會在很大程度上影響記憶體引擎的計算吞吐能力。記憶體引擎的記憶體管理主要分為3 層,如圖19所示。

圖19 記憶體引擎的記憶體管理示意圖

下面分別對3層設計進行介紹:

  • 第一層為應用消費者層,為記憶體引擎自身,包含了臨時的記憶體使用以及長期的記憶體使用(資料儲存)。
  • 第二層為應用物件資源池層,主要負責為第一層物件,如表、索引、行記錄、鍵值以及行指標提供記憶體。該層從底層索取大塊記憶體,再進行細粒度的分配。
  • 第三層為記憶體管理層,主要負責與作業系統之間的互動及實際的記憶體申請。為降低記憶體申請的呼叫開銷,互動單位一般在2MB 左右。此層同時也有記憶體預取和預佔用的功能。

第三層實際上是非常重要的,主要因為:

  • 記憶體預取可以非常有效地降低記憶體分配開銷,提高吞吐量。
  • 與 NUMA 庫進行互動的效能成本非常高,如果直接放在互動層會對效能產生很大影響。

記憶體引擎對短期與長期的記憶體使用針對 NUMA 結構適配的角度也是不同的。短期使用,一般為事務或會話(session)本身,那麼此時一般需要在處理該會話的 CPU 核對應的 NUMA 節點上獲取本地記憶體,使得交易(transaction)本身的記憶體使用有著較小的開銷;而長期的記憶體使用,如表、索引、記錄的儲存,則需要用到 NUMA 概念中類似全域性分佈(interleaved)記憶體,並且要儘量將其平均分配在各個 NUMA 節點上,以防止單個 NUMA 節點記憶體消耗過多所帶來的效能下降。

短期的記憶體使用,也就是 NUMA 角度的本地記憶體,也有一個很重要的特性,就是這部分記憶體僅供本事務自身使用(比如複製的讀取資料及做出的更新資料),因此也就避免了這部分記憶體上的併發管控。

(五)記憶體引擎的持久化

記憶體引擎基於同步的 WAL機制以及檢查點來保證資料的持久化,並且此處透過相容openGauss的 WAL機制(即 Transaction log,事務日誌),在資料持久化的同時,也可以保證資料能夠在主備節點之間進行同步,從而提供 RPO=0的高可靠以及較小RTO 的高可用能力。

記憶體引擎的持久化機制如圖20所示。

在這裡插入圖片描述

圖20 記憶體引擎的持久化機制

可以看到,openGauss的 Xlog模組被記憶體引擎對應的管理器(manager)所呼叫,持久化日誌透過 WAL的寫執行緒(重新整理磁碟執行緒)寫至磁碟,同時被 wal_sender(事務日誌傳送執行緒)調起發往備機,並在備機 wal_receiver(事務日誌接收執行緒)處接收、落盤與恢復。

記憶體引擎的檢查點也是根據 openGauss自身的檢查點機制被調起。openGauss中的檢查點機制是透過在做檢查點時進行shared_buffer(共享緩衝區)中髒頁的刷盤,以及一條特殊檢查點日誌來實現的。記憶體引擎由於是全記憶體儲存,沒有髒頁的概念,因此實現了基於 CALC的檢查點機制。

這裡主要涉及一個部分多版本(partial multi-versioning)的概念:當一個檢查點指令被下發時,使用兩個版本來追蹤一個記錄:活躍(live)版本,也就是該記錄的最新版本;穩定(stable)版本,也就是在檢查點被下發且形成虛擬一致性點時此記錄對應的版本。在一致性點之前提交的事務需要更新活躍和穩定兩個版本,而在一致性點之後的事務僅更新活躍版本,保持穩定版本不變。在無檢查點狀態的時候,實際上穩定版本是空的,代表穩定與活躍版本在此時實際上其值是相同的;僅有在檢查點過程中,在一致性點後有事務對記錄進行更新時,才需要根據雙版本來保證檢查點與其他正常事務流程的並行運作。

CALC(CheckpointingAsynchronously using Logical Consistency,邏輯一致性非同步檢查點)的實現有下面5個階段:

  • 休息(rest)階段:這個階段內,沒有檢查點的流程,每個記錄僅儲存活躍版本。
  • 準備(prepare)階段:整個系統觸發檢查點後,會馬上進入這個階段。在這個階段中事務對讀寫的更改,也會更新活躍版本;但是在更新前,如果穩定版本不存在,那麼在更新活躍版本前,活躍版本的資料會被存入穩定版本。在此事務的更新結束,在放鎖前,會進行檢查:
    如果此時系統仍然處於準備階段,那麼剛剛生成的穩定版本可以被移除;反之,如果整個系統已經脫離準備階段進入下一階段,那麼穩定版本就會被保留下來。
  • 解析(resolve)階段:在進入準備階段前發生的所有事務都已提交或回滾後,系統就會進入解析階段,進入這個階段也就代表著一個虛擬一致性點已經產生,在此階段前提交的事務相關的改動都會被反映到此次檢查點中。
  • 捕獲(capture)階段:在準備階段所有事務都結束後,系統就會進入捕獲階段。此時後臺執行緒會開始將檢查點對應的版本(如果沒有穩定版本的記錄即則為活躍版本)寫入磁碟,並刪除穩定版本。
  • 完成(complete)階段:在檢查點寫入過程結束後,並且捕獲階段中進行的所有事務都結束後,系統進入完成階段,系統事務的寫操作的表現會恢復和休息階段相同的預設狀態。

CALC有著以下優點:

  • 低記憶體消耗:每個記錄至多在檢查點時形成兩份資料。在檢查點進行中如果該記錄穩定版本和活躍版本相同,或在沒有檢查點的情況下,記憶體中只會有資料自身的物理儲存。
  • 較低的實現代價:相對其他記憶體庫檢查點機制,對整個系統的影響較小。
  • 使用虛擬一致性點:不需要阻斷整個資料庫的業務以及處理流程來達到物理一致性點,而是透過部分多版本來達到一個虛擬一致性點。

小結

openGauss的整個系統設計是可插拔、自組裝的,openGauss透過支援多個儲存引擎來滿足不同場景的業務訴求,目前支援行儲存引擎、列儲存引擎和記憶體引擎。其中面向 OLTP不同的時延要求,需要的儲存引擎技術是不同的。例如在銀行的風控場景裡,對時延的要求是非常苛刻的,傳統的行儲存引擎的時 延很難滿足業務要求。openGauss除了支援傳統行儲存引擎外,還支援記憶體引擎。在 OLAP(聯機分析處理) 上openGauss提供了列儲存引擎,有極高的壓縮比和計算效率。另外一個事務裡可以同時包含三種引擎的 DML操作,且可以保證 ACID特性。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69997967/viewspace-2922535/,如需轉載,請註明出處,否則將追究法律責任。

相關文章