天天看點

分布式存儲引擎OceanBase,UpdateServer 實作機制——存儲引擎1.記錄檔2.MemTable3.SSTable

UpdateServer存儲引擎如下圖所示。

分布式存儲引擎OceanBase,UpdateServer 實作機制——存儲引擎1.記錄檔2.MemTable3.SSTable
UpdateServer存儲引擎與Bigtable存儲引擎看起來很相似,不同點在于:

  • UpdateServer隻存儲了增量修改資料,基線資料以SSTable的形式存儲在Chunkserver上,而Bigtable存儲引擎同時包含某個子表的基線資料和增量資料;
  • UpdateServer内部所有表格共用MemTable以及SSTable,而Bigtable中每個子表的MemTable和SSTable分開存放;
  • Updateserver的SSTable存儲在SSD磁盤中,而Bigtable的 SSTable存儲在GFS中。

UpdateServer存儲引擎包含幾個部分:記錄檔、MemTable以及ssTable。更新操作首先記錄到記錄檔中,接着更新記憶體中活躍的MemTable(Active MemTable)活躍的MemTable到達一定大小後将被當機,稱為Frozen MemTable,同時建立新的Active MemTables Frozen MemTable将以SSTable檔案的形式轉儲到SSD磁盤中。

1.記錄檔

OceanBase中有一個專門的送出線程負責确定多個寫事務的順序(即事務id),将這些寫事務的操作追加到日志緩沖區,并将日志緩沖區的内容寫人日志檔案。為了防止寫記錄檔污染作業系統的緩存,寫記錄檔檔案采用Direct IO的方式實作:

class ObLogWriter
{
public:
    //write_1og高數清記錄檔存入日志統沖區
    int write_log(const LogComand cmd, const char* log_data, const int64_t data_len);
    //将日志緩沖區中的日志先同步到備機再寫入主機磁盤
    int flush_log(LogBuffer&  tlog_buffer, const bool sync_to_slave = true, const bool is_master = true);           

每條日志項由四部分組成:日志頭+日志序号+日志類型(LogCommand)+日志内容,其中,日志頭中記錄了每條日志的校驗和(checksum)。ObLogWriter中的write_log函數負責将記錄檔拷貝到日志緩沖區中,如果日志緩沖區已滿,則向調用者傳回緩沖區不足(OB_BUF_NOT_ENOUGH)錯誤碼。接着,調用者會通過flush_log将緩沖區中已有的日志内容同步到備機并寫入主機磁盤。如果主機磁盤的最後一個日志檔案超過指定大小(預設為64MB),還會調用switch_log函數切換日志檔案。為了提高寫性能,UpdateServer實作了成組送出(Group Commit)技術,即首先多次調用write_log函數将多個寫操作的日志拷貝到相同的日志緩沖區,接着再調用flush_log函數将日志緩沖區中的内容一次性寫入到日志檔案中。

2.MemTable

MemTable底層是一個高性能記憶體B樹。MemTable封裝了B樹,對外提供統一的讀寫接口。

B樹中的每個葉子節點對應MemTable中的一行資料,key為行主鍵,value為行操作連結清單的指針。每行的操作按照時間順序構成一個行操作連結清單。

如下圖所示,MemTable的記憶體結構包含兩部分:索引結構以及行操作連結清單,索引結構為B樹,支援插入、删除、更新、随機讀取以及範圍查詢操作。行操作連結清單儲存的是對某一行各個列(每個行和列确定一個單元,稱為Cell)的修改操作。

分布式存儲引擎OceanBase,UpdateServer 實作機制——存儲引擎1.記錄檔2.MemTable3.SSTable

【例】對主鍵為1的商品有3個修改操作,分别是:将商品購買人數修改為100,删除該商品,将商品名稱修改為“女鞋”,那麼,該商品的行操作鍊中将儲存三個Cell,分别為:

<update,購買人數,100> 、<delete,*> 以及 <update,商品名,“女鞋”>

MemTable中存儲的是對該商品的所有修改操作,而不是最終結果。另外,MemTable删除一行也隻是往行操作連結清單的末尾加入一個邏輯删除标記,即

<delete,*>

,而不是實際删除索引結構或者行操作連結清單中的行内容。

MemTable實作時做了很多優化,包括:

  • 哈希索引:針對主要操作為随機讀取的應用,MemTable不僅支援B樹索引,還支援哈希索引,UpdateServer内部會保證兩個索引之間的一緻性。
  • 記憶體優化:行操作連結清單中每個cell操作都需要存儲操作列的編号(column_id)、

操作類型(更新操作還是删除操作)、操作值以及指向下一個cell操作的指針,如果不做優化,記憶體膨脹會很大。為了減少記憶體占用,MemTable實作時會對整數值進行變長編碼,并将多個cell操作編碼後序列到同一塊緩沖區中,共用一個指向下一批cell操作緩沖區的指針:

struct ObCellMeta
{
  const static int64_t TP_INT8 = 1; //int8整數類型
  const static int64_t TP_INT16 = 2; //int16整數類型
  const static int64_t TP_INT32 = 3; //int32整數類型
  const static int64_t TP_INT64 = 4; //int64整數類型
  const static int64_t TP_VARCHAR = 6; //變長字元串類型
  const static int64_t TP_DOUBLE = 13; //雙精度浮點類型
  const static int64_t TP_ESCAPE = 0x1f; //擴充類型
  const static int64_t ES_DEL_ROW = 1; //删除行操作
};

class ObCompactCellwriter
{
 public:
    //寫入更新操作,存儲成壓縮格式
    int append(uint64_t column_id, const ObObj& value);
    //寫入删除操作,存儲成壓縮格式
    int row_delete();
};           

MemTable通過ObCompactCellWriter來将cell操作序列化到記憶體緩沖區中,如果為更新操作,調用append函數;如果為删除操作,調用row_delete函數。更新操作的存儲格式為:資料類型+值+列ID,TP_INT8/TP_INT16/TP_INT32/TP_INT64分别表示8位/16位/32位/64位整數類型,TP_VARCHAR表示變長字元串類型,TP_DOUBLE表示雙精度浮點類型。删除操作為擴充操作,其存儲格式為:TP_ESCAPE+ES_DEL_ROW。例9-3中的三個Cell;

<update,購買人數,100>、<delete,*>以及<update, 商品名,“女鞋”>

在記憶體緩沖區的存儲格式為:

1 2 3 4 5 6 7 8
TP_INT8 100 購買人數列ID TP_ESCAPE ES_DEL_ROW TP_VARCHAR 女鞋 商品名列ID

第1~3位元組考示第一個Cell,即

<update,購買人數,100>

;第4~5位元組表示第二個cell,即

<delete. *>

;第6~8位元組表示第三個Cel1,即

<update,商品名,“女鞋”>

MemTable的主要對外接口可以歸結如下:

//開啟一個事務
// @param [in] trans_type 事務類型,可能為讀事務或者寫事務
// @param [out] id 傳回的事務描述符
int start_transaction(const TETransType trans_type, MemTableTransDescriptor& td);
// 送出或者復原一個要新
// @param [in] td 事務描述符
// @param [in] rollback 是否復原,預設為false
int and transation(conat MemTableTransDescriptor td, bool rollback = false);
// 執行随機讀取操作,傳回一個選代器
// @param [in] td 事務描述符
// @param [in] table_id 表格編号
// @param [in] row key 待查詢的主鍵
// @param [out]iter 傳回的疊代器
int get(const MemTableTransDescriptor td, const uint64_t table_id,const ObRowkey& row key, MemTableIterator& iter);
// 執行範圍查詢操作,傳回一個選代器
// @param [in] td 事務描述符
// @param [in] range 查詢範周,包括起始行、結束行,開區間或者閉區間
// @param [out] iter 傳回的疊代器
int scan(const MemTableTransDescriptor td, const ObRange& range, MemTableIterator& iter);
// 開始執行一次修改操作
// @param [in] td 事務描述符
int atart_mutatlon(const MemTableTransDescriptor td);
// 送出或者復原一次修改操作
// @param [in] td 事務描述符
// @param [in] rollback 是否復原
int end _mutation(const MemTableTransDescriptor td, bool rollback);
//執行修改操作
// @param [in] td 事務描述符
// @param [in] mutator 修改操作,包含一個或者多個對多個表格的cell操作
int set(const MemTableTransDescriptor td, ObUpsMutator& mutator);           

對于讀事務,操作步驟如下:

  1. 調用start transaction開始一個讀事務,獲得事務描述符;
  2. 執行随機讀取或者掃描操作,傳回一個疊代器;接着可以從疊代器不斷疊代資料;
  3. 調用end transaction送出或者復原一個事務。
class MemTableIterator
{
public:
  //疊代器移動到下一個cell
  int next cell();
  //擷取目前cell的内容
  //@param [out] cell_info 目前cell的内容,包括表名(table_id),行主健(row_key),列編号(column_id)以及列值(column_value)
  int get_cell(obcellInfo** cell_info);
  //擷取目前cell的内容
  //@param [out] cell_info 目前cell的内容
  //@param is_row_changed 是否疊代到下一行
  int get_cell(obcellInfo** cell_info, bool* is_row_changed);
};           

讀事務傳回一個疊代器Mem Tablelterator,通過它可以不斷地擷取下一個讀到的cell。在【例】中,讀取編号為1的商品可以得到一個疊代器,從這個疊代器中可以讀出行操作鍊中儲存的3個Cell,依次為:

<update,購買人數,100>,<delete, *>,<update, 商品名, “女鞋”>

寫事務總是批量執行,步驟如下:

  1. 調用start transaction開始一批寫事務,獲得事務描述符;
  2. 調用start mutation開始一次寫操作;
  3. 執行寫操作,将資料寫入到MemTable中;
  4. 調用end_mutation送出或者復原一次寫操作;如果還有寫事務,轉到步驟2;
  5. 調用end transaction送出寫事務。

3.SSTable

當活躍的MemTable超過一定大小或者管理者主動發起當機指令時,活躍的MemTable将被當機,生成當機的MemTable,并同時以SSTable的形式轉儲到SSD磁盤中。

SSTable的詳細格式請參考

ChunkServer實作機制

,與ChunkServer中的SSTable不同的是,UpdateServer中所有的表格共用一個SSTable,且SSTable為稀疏格式,也就是說,每一行資料的每一列可能存在,也可能不存在修改操作。

另外,OceanBase設計時也盡量避免讀取UpdateServer中的SSTable,隻要記憶體足夠,當機的MemTable會保留在記憶體中,系統會盡快将當機的資料通過定期合并或者資料分發的方式轉移到ChunkServer中去,以後不再需要通路UpdateServer中的SSTable資料。

當然,如果記憶體不夠需要丢棄當機MemTable,大量請求隻能讀取SSD磁盤,UpdateServer性能将大幅下降。是以,希望能夠在丢棄當機MemTable之前将SSTable的緩存預熱。

UpdateServer的緩存預熱機制實作如下:在丢棄當機MemTable之前的一段時間(比如10分鐘),每隔一段時間(比如30秒),将一定比率(比如5%)的請求發給SSTable,而不是當機MemTable。這樣,SSTable上的讀請求将從5%到10%,再到15%,依次類推,直到100%,很自然地實作了緩存預熱。