OceanBase 儲存層程式碼解讀(二)微塊儲存格式

OceanBase開源社群發表於2021-10-13
作者:
公祺,一個專注於 OBKV 的程式設計師
海芊,一個致力於當網紅的 OceanBase 文件工程師。個人頻道: Amber loves OB

1. 微塊和巨集塊的關係

OceanBase 資料庫的儲存引擎採用了基於 LSM-Tree 的架構,把基線資料和增量資料分別儲存在磁碟(SSTable)和記憶體(MemTable)中,其中 SSTable 以巨集塊(Macro Block)為單位組織資料,每個巨集塊大小為 2MB。巨集塊內部又劃分出很多個大小為 16K(壓縮前的大小)微塊(Micro Block),每個微塊內則包含多個行(Row)。
OceanBase 資料庫內部 IO 的最小單元就是微塊,微塊資料可以進行壓縮,下圖是微塊在巨集塊中的儲存格式:

OceanBase 儲存層程式碼解讀(二)微塊儲存格式

其中:
1.macro block header:巨集塊的後設資料,包括:版本、巨集塊大小、微塊的個數、列的型別、行的個數、壓縮演算法等資訊。
2.micro block n:微塊的具體資料,具體資訊見下文微塊的儲存格式。
3.micro block index:微塊的索引資料,用來索引每個微塊,通常常駐記憶體。
每一個微塊包含多個行資料,行資料中有多個列的資料,如下是當前微塊的儲存格式:

OceanBase 儲存層程式碼解讀(二)微塊儲存格式

其中:
1.record header:包含微塊資料大小、頭部大小、version、checksum 等。
2.micro block header:包含微塊的後設資料,包括:列數、行索引的偏移量、行數等。
3.Row n:包含行資料,包括:row header、列資料、列索引。
(1)row header:行的後設資料,包括:列索引佔用的位元組數、刪除標記等。
(2)col n:列的值,包括:型別、長度、列的值。
(3)column index:列的索引資料,用來索引每一列,記錄了每列值的偏移和長度。
4.Row Index:行的索引資料,用來索引每一行,記錄了每行的偏移和長度。
接下來,讓我們根據 OceanBase 的開原始碼,介紹微塊的具體程式碼實現,後續的程式碼解讀都是基於此版本: v3.1.0_CE_BP1

2. 微塊中行資料的編碼

行資料寫入的主要邏輯在 ObMacroBlockWriter 模組,見如下的程式碼:
// src/storage/blocksstable/ob_macro_block_writer.cpp
// 該函式主要邏輯為:
//   1. 將行資料寫入到微塊中,並更新 bloomfilter 和 checksum
//   2. 在當前微塊寫滿的情況下:構建當前的微塊,並切換微塊寫入器
int ObMacroBlockWriter::append_row(const ObStoreRow& row, const int64_t split_size, const bool ignore_lob)
{
  int ret = OB_SUCCESS;
  const ObStoreRow* row_to_append = &row;
  // ... 省略引數檢查的相關程式碼if (OB_SUCC(ret)) {// 向當前的微塊中寫入一行資料if (OB_FAIL(micro_writer_->append_row(*row_to_append))) { if (OB_BUF_NOT_ENOUGH == ret) {
        if (0 == micro_writer_->get_row_count()) {
          // 不支援超過 16KB 的行
          ret = OB_NOT_SUPPORTED;
          STORAGE_LOG(ERROR, "The single row is too large, ", K(ret), K(row));
        } else if (OB_FAIL(build_micro_block(false))) { // 當前微塊寫滿後,需要構建該微塊:編碼、壓縮等// for compatibilitySTORAGE_LOG(WARN, "Fail to build micro block, ", K(ret));
        } else if (OB_FAIL(micro_writer_->append_row(*row_to_append))) { // 繼續寫當前行資料STORAGE_LOG(ERROR, "Fail to append row to micro block, ", K(ret), K(row));
        } else if (data_store_desc_->need_calc_column_checksum_ 
                   && OB_FAIL(add_row_checksum(row_to_append->row_val_))) { // 計算 checksumSTORAGE_LOG(WARN, "fail to add column checksum", K(ret));
        }
        if (OB_SUCC(ret) && data_store_desc_->need_prebuild_bloomfilter_) {
          // 根據 rowkey 構建 bloomfilter
          const ObStoreRowkey rowkey(row_to_append->row_val_.cells_, data_store_desc_->bloomfilter_rowkey_prefix_);
          if (OB_FAIL(micro_rowkey_hashs_.push_back(static_cast<uint32_t>(rowkey.murmurhash(0))))) {
            STORAGE_LOG(WARN, "Fail to put rowkey hash to array ", K(ret), K(rowkey));
            micro_rowkey_hashs_.reuse();
            ret = OB_SUCCESS;
          }
        }
      } else {
        STORAGE_LOG(WARN, "Fail to append row to micro block, ", K(ret), K(row));
      }
    } else {
      if (data_store_desc_->need_prebuild_bloomfilter_) {
        // 根據 rowkey 構建 bloomfilter
        const ObStoreRowkey rowkey(row_to_append->row_val_.cells_, data_store_desc_->bloomfilter_rowkey_prefix_);
        if (OB_FAIL(micro_rowkey_hashs_.push_back(static_cast<uint32_t>(rowkey.murmurhash(0))))) {
          STORAGE_LOG(WARN, "Fail to put rowkey hash to array ", K(ret), K(rowkey));
          micro_rowkey_hashs_.reuse();
          ret = OB_SUCCESS;
        }
      }
      // 計算 checksumif (data_store_desc_->need_calc_column_checksum_ && OB_FAIL(add_row_checksum(row_to_append->row_val_))) {STORAGE_LOG(WARN, "fail to add column checksum", K(ret));
      } else if (micro_writer_->get_block_size() >= split_size) {
        // 如果當前微塊已經滿 16KB,則構建微塊if (OB_FAIL(build_micro_block())) {STORAGE_LOG(WARN, "Fail to build micro block, ", K(ret));
        }
      }
    }
  }
  return ret;
}
行資料通過呼叫 ObMicroBlockWriter::append_row -> ObRowWriter::write 將資料寫到微塊中,從而進行行資料編碼。編碼分為兩種,程式碼如下:
// src/storage/blocksstable/ob_row_writer.cpp
int ObRowWriter::write(const int64_t rowkey_column_count, const ObStoreRow& row, char* buf, 
                       const int64_t buf_size, int64_t& pos, int64_t& rowkey_start_pos, 
                       int64_t& rowkey_length, const bool only_row_key)
{
  int ret = OB_SUCCESS;
  if (row.is_sparse_row_) { // 使用稀疏行儲存格式寫入行資料if (OB_FAIL(write_sparse_row(
            rowkey_column_count, row, buf, buf_size, pos, rowkey_start_pos, rowkey_length, only_row_key))) {
      STORAGE_LOG(WARN, "write sparse row failed", K(ret), K(row), K(OB_P(buf)), K(buf_size));
    }
  } else if (OB_FAIL(write_flat_row(rowkey_column_count,
                 row,
                 buf,
                 buf_size,
                 pos,
                 rowkey_start_pos,
                 rowkey_length,
                 only_row_key))) { // 使用稠密行儲存格式寫入行資料
    STORAGE_LOG(WARN, "write flat row failed", K(ret), K(row), K(OB_P(buf)), K(buf_size));
  }
  return ret;
}
行資料的儲存格式有兩種:
1.Sparse Format:稀疏格式,行資料的每列都要儲存資料,即使該列沒有值也要儲存 NOP;
2.Dense Format:稠密格式,行資料只儲存有值的列,沒有值的列不儲存。
這兩種格式差不多,只是對沒有值的列的處理略有不同,一般常用稠密格式儲存行資料,下面是相關的程式碼:
// src/storage/blocksstable/ob_row_writer.cpp
// 使用稠密行儲存格式寫入行資料
int ObRowWriter::write_flat_row(const int64_t rowkey_column_count, const ObStoreRow& row, char* buf,
    const int64_t buf_size, int64_t& pos, int64_t& rowkey_start_pos, int64_t& rowkey_length, const bool only_row_key)
{
  int ret = OB_SUCCESS;
  int64_t tmp_rowkey_start_pos = 0;
  int64_t tmp_rowkey_length = 0;
  if (!row.is_valid() || rowkey_column_count <= 0 || rowkey_column_count > row.row_val_.count_) {
    ret = OB_INVALID_ARGUMENT;
    STORAGE_LOG(ERROR, "invalid input argument.", K(buf), K(buf_size), K(row), K(rowkey_column_count), K(ret));
  } else if (OB_FAIL(init_common(buf, buf_size, pos))) {  // buf 引數初始化
    STORAGE_LOG(WARN, "row writer fail to init common.", K(ret), K(row), K(OB_P(buf)), K(buf_size), K(pos));
  } else if (OB_FAIL(init_store_row(row, rowkey_column_count))) { // 做一些引數檢查
    STORAGE_LOG(WARN, "row writer fail to init store row.", K(ret), K(rowkey_column_count));
  } else if (OB_FAIL(append_row_header(row))) { // 填充 row header,不包括:列索引佔用的位元組數if (OB_BUF_NOT_ENOUGH != ret) {
      STORAGE_LOG(WARN, "row writer fail to append row header.", K(ret));
    }
  } else if (OB_FAIL(
                 append_store_row(rowkey_column_count, row, only_row_key, tmp_rowkey_start_pos, tmp_rowkey_length))) { // 填充行資料,具體程式碼見下面的程式碼:ObRowWriter::append_store_rowif (OB_BUF_NOT_ENOUGH != ret) {
      STORAGE_LOG(WARN, "row writer fail to append store row.", K(ret), K(rowkey_column_count));
    }
  } else if (OB_FAIL(append_column_index())) { // 填充列索引資料以及 row_header_.column_index_bytes_if (OB_BUF_NOT_ENOUGH != ret) {
      STORAGE_LOG(WARN, "row writer fail to append column index.", K(ret));
    }
  } else {
    pos = pos_;
    rowkey_start_pos = tmp_rowkey_start_pos;
    rowkey_length = tmp_rowkey_length;
  }
  return ret;
}
// 填充行資料,包括每一列的值
int ObRowWriter::append_store_row(const int64_t rowkey_column_count, const ObStoreRow& row, const bool only_row_key,
    int64_t& rowkey_start_pos, int64_t& rowkey_length)
{
  int ret = OB_SUCCESS;
  const int64_t end_index = only_row_key ? rowkey_column_count : row.row_val_.count_;
  rowkey_start_pos = pos_;
  if (!row.is_valid() || rowkey_column_count <= 0 || rowkey_column_count > row.row_val_.count_) {
    ret = OB_INVALID_ARGUMENT;
    STORAGE_LOG(WARN, "invalid row input argument.", K(row), K(rowkey_column_count), K(ret));
  } else {
    for (int64_t i = 0; OB_SUCC(ret) && i < end_index; ++i) {
      // 記錄列索引,當前還沒有確定列索引佔用的位元組數// 所以記錄 3 種型別的列索引陣列,在 append_column_index(...) 時,// 會最終確認列索引佔用的位元組數,以及使用哪個陣列const int64_t column_index = pos_ - start_pos_;
      column_indexs_8_[i] = static_cast<int8_t>(column_index);
      column_indexs_16_[i] = static_cast<int16_t>(column_index);
      column_indexs_32_[i] = static_cast<int32_t>(column_index);
      // 填充列資料,包括:列型別、列值長度、列值if (OB_FAIL(append_column(row.row_val_.cells_[i]))) {if (OB_BUF_NOT_ENOUGH != ret) {
          // 行資料太長了
          STORAGE_LOG(WARN, "row writer fail to append column.", K(ret), K(i), K(row.row_val_.cells_[i]));
        }
        break;
      }
    }
  }
  if (OB_SUCC(ret)) {
    column_index_count_ += end_index;
    if (rowkey_column_count < end_index) {
      rowkey_length = column_indexs_32_[rowkey_column_count] + start_pos_ - rowkey_start_pos;
    } else {
      rowkey_length = pos_ - rowkey_start_pos;
    }
  }
  return ret;
}
以上是單個行資料的編碼,寫入多行資料,直到微塊寫滿時,就需要構建微塊了。

3. 微塊的整體構建

微塊是有多個行組成的,微塊寫滿後就需要對整個微塊進行序列化、壓縮、填充後設資料等,微塊的構建程式碼見 ObMacroBlockWriter::build_micro_block,如下:
// src/storage/blocksstable/ob_macro_block_writer.cpp
int ObMacroBlockWriter::build_micro_block(const bool force_split)
{
  int ret = OB_SUCCESS;
  char* block_buffer = NULL;
  int64_t block_size = 0;
  bool mark_deletion = false;
  ObMicroBlockDesc micro_block_desc;
  if (micro_writer_->get_row_count() <= 0) {
    ret = OB_INNER_STAT_ERROR;
    STORAGE_LOG(WARN, "micro_block_writer is empty", K(ret));
  } else if (OB_FAIL(micro_writer_->build_block(block_buffer, block_size))) { // 序列化
    STORAGE_LOG(WARN, "Fail to build block, ", K(ret));
  } else if (OB_FAIL(
                 compressor_.compress(block_buffer, block_size, micro_block_desc.buf_, micro_block_desc.buf_size_))) { // 使用壓縮演算法,對微塊序列化好的 buffer 進行壓縮
    STORAGE_LOG(WARN, "macro block writer fail to compress.", K(ret), K(OB_P(block_buffer)), K(block_size));
  } else if (MICRO_BLOCK_MERGE_VERIFY_LEVEL::NONE != micro_writer_->get_micro_block_merge_verify_level() &&
             OB_FAIL(check_micro_block( // 檢查壓縮是否正確
                 micro_block_desc.buf_, micro_block_desc.buf_size_, block_buffer, block_size, micro_writer_))) {
    STORAGE_LOG(WARN, "failed to check micro block", K(ret));
  } else if (OB_FAIL(can_mark_deletion(pre_micro_last_key_, 
                                       last_key_, mark_deletion))) { // 設定巨集塊後設資料:微塊 delete mark
    STORAGE_LOG(WARN, "fail to run can mark deletion", K(ret));
  } else if (OB_FAIL(save_pre_micro_last_key(last_key_))) { // 設定巨集塊的後設資料:微塊的 endkey
    STORAGE_LOG(WARN, "Fail to save pre micro last key, ", K(ret), K_(last_key));
  } else {
    micro_block_desc.last_rowkey_ = micro_writer_->get_last_rowkey();
    micro_block_desc.data_size_ = micro_writer_->get_data_size();
    micro_block_desc.row_count_ = micro_writer_->get_row_count();
    micro_block_desc.column_count_ = micro_writer_->get_column_count();
    micro_block_desc.row_count_delta_ = micro_writer_->get_row_count_delta();
    micro_block_desc.can_mark_deletion_ = mark_deletion;
    micro_block_desc.column_checksums_ =
        data_store_desc_->need_calc_column_checksum_ ? curr_micro_column_checksum_ : NULL;
    if (data_store_desc_->is_multi_version_minor_sstable()) {
      micro_block_desc.max_merged_trans_version_ = micro_writer_->get_max_merged_trans_version();
      micro_block_desc.contain_uncommitted_row_ = micro_writer_->is_contain_uncommitted_row();
    }
    // 將該微塊寫入巨集塊,包括://   1. 將該微塊放入到巨集塊的微塊陣列中//   2. 更新巨集塊的 bloomfilterif (OB_FAIL(write_micro_block(micro_block_desc, force_split))) {
      STORAGE_LOG(WARN, "build_micro_block failed", K(micro_block_desc), K(force_split), K(ret));
    } else {
      // 重置微塊的寫入器,後續可以複用micro_writer_->reuse();if (data_store_desc_->need_prebuild_bloomfilter_ && micro_rowkey_hashs_.count() > 0) {
        micro_rowkey_hashs_.reuse();
      }
    }
  }
  return ret;
}
後面,再看一下微塊的壓縮的程式碼實現。

4. 微塊資料的壓縮

微塊的壓縮的管理類為“ObCompressorPool”,支援多種壓縮演算法:
1.NONE_COMPRESSOR:不壓縮;
2.LZ4_COMPRESSOR:lz4_1.0
3.LZ4_191_COMPRESSOR:lz4_1.9.1
4.SNAPPY_COMPRESSOR:snappy_1.0
5.ZLIB_COMPRESSOR:zlib_1.0
6.ZSTD_COMPRESSOR:zstd_1.0
7.ZSTD_1_3_8_COMPRESSOR:zstd_1.3.8
8.STREAM_LZ4_COMPRESSOR:stream_lz4_1.0
9.STREAM_ZSTD_COMPRESSOR:stream_zstd_1.0
10.STREAM_ZSTD_1_3_8_COMPRESSOR:stream_zstd_1.3.8
目前微塊的壓縮沒有使用流式壓縮(流式壓縮主要用在 RPC 中),微塊使用的壓縮演算法的實現主要是繼承 ObCompressor 介面類:
// deps/oblib/src/lib/compress/ob_compressor.h
class ObCompressor {
public:
  static const char* none_compressor_name;
public:
  ObCompressor()
  {}
  virtual ~ObCompressor()
  {}
  
  // 壓縮介面virtual int compress(const char* src_buffer, const int64_t src_data_size, char* dst_buffer,
      const int64_t dst_buffer_size, int64_t& dst_data_size) = 0;
  
  // 解壓介面virtual int decompress(const char* src_buffer, const int64_t src_data_size, char* dst_buffer,
      const int64_t dst_buffer_size, int64_t& dst_data_size) = 0;
  
  // 獲取 max(壓縮後的資料大小 - 原始資料大小)// 主要用來確定 dst_buffer_size: src_data_size + max_overflow_size// 用來預分配壓縮後的記憶體 buffervirtual int get_max_overflow_size(const int64_t src_data_size, int64_t& max_overflow_size) const = 0;
  // 獲取壓縮演算法名稱介面virtual const char* get_compressor_name() const = 0;
};
具體的演算法實現可以參見具體的程式碼實現類:ObNoneCompressor、ObLZ4Compressor、ObLZ4Compressor191、ObSnappyCompressor、ObZlibCompressor、ObZstdCompressor、ObZstdCompressor_1_3_8 等。
微塊的壓縮邏輯比較簡單,包裝在 ObMicroBlockCompressor 中:
// src/storage/blocksstable/ob_macro_block.h
class ObMicroBlockCompressor {
public:
  ObMicroBlockCompressor();
  virtual ~ObMicroBlockCompressor();
  void reset();
  
  // 根據微塊大小、壓縮演算法,初始化壓縮器int init(const int64_t micro_block_size, const char* comp_name);
  
  // 壓縮介面,通過呼叫 ObCompressor 的實現類的 compress 介面int compress(const char* in, const int64_t in_size, const char*& out, int64_t& out_size);
  
  // 解壓介面,通過呼叫 ObCompressor 的實現類的 decompress 介面int decompress(const char* in, const int64_t in_size, const int64_t uncomp_size, const char*& out, int64_t& out_size);
private:
  bool is_none_;
  int64_t micro_block_size_;          // 微塊大小
  common::ObCompressor* compressor_;  // 指向壓縮演算法的實現類
  ObSelfBufferWriter comp_buf_;       // 壓縮 buffer
  ObSelfBufferWriter decomp_buf_;     // 解壓 buffer// ObSelfBufferWriter 是可以自動擴充套件的記憶體 buffer 類
};
一般 LZ4、ZSTD 是比較常用的壓縮演算法,前者壓縮速度快,後者壓縮率高。
微塊使用何種壓縮演算法,是和建表的 schema 有關係的,比如可以通過以下建表 SQL 指定使用 ZSTD 的壓縮:
CREATE TABLE `t` (
  `id` int(11) NOT NULL,
  `k` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) DEFAULT CHARSET = utf8mb4 ROW_FORMAT = DYNAMIC COMPRESSION = 'zstd_1.0' REPLICA_NUM = 1 BLOCK_SIZE = 16384 USE_BLOOM_FILTER = FALSE TABLET_SIZE = 134217728 PCTFREE = 0

5. 微塊儲存的一些思考

基於列存的編碼演算法
OceanBase 資料庫的三層儲存結構:SSTable、MacroBlock、MicroBlock。其中微塊是讀 IO 的最小單元,直接影響著業務請求的延時,微塊的壓縮對降低 IO 延時都有比較好的改善效果。編碼方面,基於行儲存的編碼效果並不明顯,可以考慮基於列式儲存的編碼,比如:字典、字首等。
Parquet 列式儲存編碼方式支援的就比較多,可以參考 Parquet Encodings,ORC 也基本實現了這些編碼演算法 ORC v2,可見列式儲存編碼演算法的豐富性。
OceanBase 資料庫中微塊如果採用合適的列式編碼演算法,可以非常有效地降低 IO 延時以及儲存空間的成本,並且列式儲存對 AP 場景也有很好的效能表現。
計算量的優化方向
眾所周知,資料庫不僅是 CPU 密集性應用,也是 IO 密集性應用,但是隨著業界 CPU 效能提升放緩,Nvme SSD 效能大幅提升的情況下,CPU 慢慢成為資料庫的瓶頸,OceanBase 資料庫也不例外,加上在微塊編碼、壓縮等方面的 CPU 開銷,對 CPU 效能要求會越來越高。
針對目前的 CPU 瓶頸,業界普遍採用硬體加速的方式來降低機器 CPU 的使用率:
1.FPGA:將計算量解除安裝到硬體中,降低機器 CPU 的使用率;
2.SIMD:單指令流、多資料流,例如 Intel 的 MMXSSE,以及 AMD 的 3D Now! 指令集;
3.其他。

6. 微塊儲存格式的 demo

可以通過下面的真實的一個微塊 demo,更好的理解微塊的儲存格式:

OceanBase 儲存層程式碼解讀(二)微塊儲存格式

通過 record header 可以知道微塊的大小,是否啟用壓縮(data_length、data_zlength 相等表示沒有壓縮),以及微塊資料的 checksum。
在 micro block header 中,主要記錄了行數、列數,以及的行索引偏移量,行索引記錄在微塊的最後。
Row 0 表示第一行資料,ObRowHeader 主要記錄每列索引佔用的位元組數(column_index_bytes),列索引的偏移量是通過行索引得出第一行的偏移和長度往前推8位元組(column_count * column_index_bytes)得出來的。
每一列中都一個列後設資料和列值,列後設資料中記錄了列值的型別、長度,列值中才真正記錄了列的資料。

總結

以上通過走讀程式碼的方式,梳理了微塊的儲存格式,後面會繼續走讀 OceanBase 資料庫的儲存層的程式碼:
1.巨集塊的儲存格式;
2.合併的主要邏輯;
3.巨集塊的 GC 實現;
4.其他。

最後的最後:

如果大家有任何疑問,可以通過以下方式與我們進行交流: 測試遇到問題?

企業使用者想享受技術顧問的免費一對一諮詢服務?

快加入 OB 創計劃→

釘釘群:33254054

OceanBase 儲存層程式碼解讀(二)微塊儲存格式

今日之星,明日之星都不如你留下的星星(⭐️️)

我們想讓 Github 上優質的開源專案被更多人看到。

文件都是我們精心整理。如果有幫助的話 求個star(◕ᴗ◕✿),鼓勵鼓勵我們喲! 也歡迎大家給我們 issue,請點選 這裡。運營小姐姐在此跪謝️️ ❥(^_-)

歡迎大家一起參與 社群貢獻,指南請參考看 這裡

社群答疑:請點選 這裡


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

相關文章