• Apache Doris 学习笔记: Backend存储层字符串编码与解码


    字符串编码/解码

    字符串编码主要逻辑位于BinaryDictPageBuilder以及其上下游的ColumnWriter和BitshufflePageBuilder。

    它们在backend的storage_engine中负责把内存中的字符串数据(CHAR/VARCHAR,在EncodingInfoResolver中确定)进行编码压缩,打包成page并写入文件。

    graph LR A[BetaRowsetWriter] -->B[SegmentWriter] B --> C[ColumnWriter] C -->D[BinaryDictPageBuilder] D -->E[BitshufflePageBuilder]
    BetaRowsetWriter::flush_single_memtable(MemTable* memtable, int64_t* flush_size) [MemTable::Iterator->ContiguousRow(多个)]
    
    · memtable: 用于把数据缓存在内存中,并利用类似跳表的结构有序地维护数据。在写满后异步生成一个segement。
    
    · ContiguousRow: 利用MemTable::Iterator来取得memtable中的一行数据。含有schema和void*形式的数据。
    
    Status SegmentWriter::append_row(const RowType& row) [ContiguousRow->RowCursorCell(每列一个)]
    
    · RowCursorCell: 内部持有一个void类型的指针, void*的第一位是bool型的is_null,随后跟着数据(slice里面的uint8*和length)。 
    
    ColumnWriter::append(const CellType& cell) [RowCursorCell -> void*]
    
    BinaryDictPageBuilder::add(const uint8_t* vals, size_t* count) [void* -> Slice]
    
    · Slice: 对uint8指针的封装,含有指针和长度(byte数),同时还含有一些功能性的函数。它主要来存储各种数据。
    
    · OwnedSlice: 对Slice的封装,它将赋值运算的实现改为swap,以此来实现类似move的高效传递数据。
    
    BitshufflePageBuilder::add(const uint8_t* vals, size_t* count) [UINT32]
    
    · faststring: 和slice一样含有一个uint8和length,类似std::string的字符串类,通过预先申请32byte空间来提高一些操作的速度。支持转换到slice。
    

    ScalarColumnWriter

    一个page由若干slice组成body,然后和footer、next指针组成。

    ScalarColumnWriter的写入过程中会产生一个page构成的链表,最后再统一write,如果是dict编码则在最后再写入一个DICTIONARY_PAGE(PLAIN_ENCODING类型)。

    graph LR A[DataPage1] -->B[DataPage2] B --> C[DataPage3] C -->D[DataPage...] D -->E[DictionaryPage]

    ScalarColumnWriter::finish_current_page()

    从page_builder获取finish出来经过编码的page数据,然后加入自己的vector body。
    如果自身是nullable的,则再往body里加入一个_null_bitmap_builder产生的page数据。

    然后造一个当前page的footer,footer格式如下:

    • type: 表示page的类型,这里设置为DATA_PAGE,表示是存储数据的page。

    • uncompressed_size: page的大小,是body里所有slice的size求和。

    • first_ordinal: 该page第一行数据的row_id,每个ColumnWriter独立计数。

    • num_values: 元素个数。

    • nullmap_size: nullmap的size。

    ScalarColumnWriter::write_data()

    把当前的page都写入文件。

    对于每个data_page,通过PageIO::write_page把body和footer写入writeable_block,然后返回一个page_pointer(page在文件中的偏移量和长度),最后把page_pointer加入ordinal_index。

    在写完data_page后写入dictionary_page(字典编码时)。

    把BinaryDictPageBuilder的hash_map通过PLAIN_ENCODING做成一个page,然后同样做好footer。

    经过PageIO::compress_and_write_page压缩并写入wblock。(LZ4F)

    然后把page_pointer写入ColumnWriterOptions.ColumnMetaPB.dict_page。


    BinaryDictPageBuilder

    对字符串做字典编码并传递给下层的bitshuffle_page_builder。

    输入 :

    通过ColumnWriter的append_data调用BinaryDictPageBuilder的add来传入uint8_t* ptr形式的数据(slice)。

    输出 :

    通过ColumnWriter的finish_current_page调用BinaryDictPageBuilder的finish来获取一个OwnedSlice格式的编码完的page数据。

    示例

    原始数据:

    aaaaa,
    aaaab,
    bbbbb,
    aaaaa,
    bbbbb,
    aaaaa
    

    字典编码:

    value_code dict
    0,1,2,0,2,0 aaaaa->0,aaaab->1,bbbbb->2

    bitshuffle:

    源码中value_code为UINT32,有32位,这里为了简单起见改为4位。

    0000,
    0001,
    0010,
    0000,
    0010,
    0000
    

    对数据按列重新排列:

    000000,000000,001010,010000

    lz4压缩(以扫描窗口大小为4举例):

    0000(4,4)(8,4)00101(5,4)000

    持有根据_encoding_type的值分为两种类型的PageBuilder,它们会产生不同编码类型的page。

    DICT_ENCODING负责产生data_page,字典编码+bitshuffle+lz4压缩。

    PLAIN_ENCODING负责产生dictionary_page,lz4f压缩(在write时)。

    DICT_ENCODING

    add(const uint8_t* vals, size_t* count) 添加一个字符串
    这里会做一次字典编码,然后追加到BitshufflePageBuilder的_data(faststring)末尾。

    详细过程 :

    先进行一些合法性检查(例如是否已经finish过了,是否添加了空串)。

    然后将输入的指针用reinterpret_cast强制转换为Slice指针(由void*转换为slice)。

    如果是page的第一行数据,则要拷贝存入_first_value。

    然后对Slice指针的的每个Slice进行遍历,在这里进行字典编码,获得每个Slice的value_code。

    字典编码采用The Parallel Hashmap中的flat_hash_map。

    相比普通的hash_map,flat_hash_map扩容的时候迭代器会失效,但是它有着更小的内存占用和在小数据规模下更快的性能。

    phmap::flat_hash_map<Slice, uint32_t, HashOfSlice> _dictionary;

    在x86架构下HashOfSlice内部默认采用CityHash64作为具体实现。

    然后将value_code添加进BitshufflePageBuilder(第二层编码)。

    在过程中同时在新增value_code时把string追加到一个vector,这样顺序存储value_code对应的string,之后用于生成dictionary_page。

    在有许多重复输入的字符串时,这里的字典编码由一定压缩效果。

    理论上这里可以根据value_code最大值去缩小存储类型的长度,从32位改为更小的位数。这样就能有效缩小存储空间,也能提高编码/解码效率。

    不过之后还有一层编码压缩,所以这里的冗余部分在之后会有更高的压缩率。

    DICT_ENCODING退化机制

    当dictionary_page大于option_->dict_page_size(1024*1024)时,会触发退化机制。

    此时会立刻finish当前page,在这之后的data_page的编码模式都会被reset成PLAIN_ENCODING(之前的page保持不变)。

    finish()

    返回一个自身page数据的OwnedSlice,会先经过BitshufflePageBuilder的finish(里面通过bitshuffle::compress_lz4进行一次lz4压缩)。

    然后把这个slice转换成faststring,并在header写入编码类型(_encoding_type)。

    在finish之后想要继续添加数据必须先执行reset重置自身。

    get_dictionary_page()

    返回字典,用于解码。
    会在ColumnWriter执行write_data的时候被调用。多个page公用一个dictionary_page。

    BinaryDictPageDecoder

    与BinaryDictPageBuilder对应的解码器。

    同样在_encoding_type=DICT_ENCODING时会持有BitShufflePageDecoder类型的_data_page_decoder,和PLAIN_ENCODING编码类型的_dict_decoder。

    _data_page_decoder用来解码被Bitshuffle编码压缩的数据body,_dict_decoder用来解码字典。

    然后通过_dict_decoder->string_at_index来把value_code转换回字符串slice并传递给作为输出的ColumnBlock。

    最后通过ColumnBlock持有的Mempool申请一块连续内存,按顺序分配给每个Slice分配一个向上取到8的整倍数((mem_size + 7) & ~7)内存空间来做到内存对齐。

    PLAIN_ENCODING

    通过BinaryPlainPageBuilder生产BinaryPlainPage。直接append,不进行任何编码。

    具体存储格式为|str_1|str_2|str_3....|str_n|offset_1|offset_2|offset_3....|offset_n|element_number|

    str为字符串(不定长),offset和element_number为uint32(固定占4位)。

    BinaryPlainPageDecoder

    string_at_index(size_t idx)

    根据id直接计算出偏移位置,然后获取对应value_code的字符串。

    解码步骤

    1. 求出_offsets_pos(通过element_number计算offset部分的开头)
    2. 通过index求找出对应的offset,
    3. 通过offset找到str
    4. 通过对offset序列进行一次差分得到str的length序列。

    BitshufflePageBuilder

    生产BitShufflePage(以Slice的形式)。
    BitShufflePage内部数据由Header和Element data两部分组成。

    Header(16bytes 4个uint32_t):
    
    num_elements,page内的元素个数
    
    compressed_size,page压缩后的大小(Header和压缩后的Element data)
    
    padded_num_elements,填充空元素的个数(BitShuffle库需要输入元素个数是8的倍数,所以需要在序列末尾添加空元素)
    
    elem_size_bytes,每个元素占的空间大小。
    
    Element data:
    
    经过字典编码+BitShuffle编码和lz4压缩后的数据。
    
    DictEncodingDataPage
    _encoding_type
    num_elements
    compressed_size
    padded_num_elements
    elem_size_bytes
    ElementData

    add(const uint8_t* vals, size_t* count)

    直接给slice追加数据,做了一些容量限制(这里写满一个page会返回到上层执行finish进行落盘)。

    一个BitshufflePageBuilder可以写16384个value_code(page_size/sizeof(uint32),64×1024/4)。

    finish()

    首先记录first_value和last_value,之后在这部分进行bitshuffle排列和lz4压缩。

    首先做了一些resize调整以满足bitshuffle条件,然后调用bitshuffle的compress_lz4把data数据压缩存储到faststring类型的buffer。

    Bitshuffle本身没有压缩效果,只是把元素按位从高到低以列的顺序重新排列。它的作用是通过编码提高其他压缩算法的压缩率(特别是LZF和LZ4)。

    最后更新buffer的header然后通过build返回OwnedSlice数据。

    在进行之前的字典编码后再进行Bitshuffle很可能会让数据出现较长的全0前缀。

    BitShufflePageDecoder

    BitShufflePageDecoder在init(二段构造)时进行一系列检查数据合法性的操作,检查完后在_decode()进行lz4解码。

    具体流程 :

    首先检测init是否已经被调用过,防止重复解码。
    
    然后检测输入数据长度是否小于header的长度,header长度是固定的,如果小于这个长度显然数据不合法。
    
    然后解析出header部分的数据,通过header中的数据去做数据合法性检测:		
    
    1、校验data size是否和header中的_compressed_size一致。
    
    2、检测header中_num_element_after_padding是否和通过_num_elements重新计算出的实际数值一致。
    
    3、检测_size_of_element符合要求,除了UNSIGNED_INT类型外的数据元素size必须和decoder的SIZE_OF_TYPE相同。UNSIGNED_INT类型则允许从数据定义的size比decoder定义的size小(允许类型提升到UNSIGNED_INT)。
    
    然后执行_decode()开始解码,把解码的数据存储到faststring类型的_decoded。
    

    next_batch(size_t* n, ColumnBlockView* dst)

    读取一批数据,因为在二段构造后解码实际已经完成,所以可以直接取出数据到ColumnBlockView。

    跟其他层的next_batch或者类似功能的函数一样,输入需要提取元素的个数n,然后对n和剩余元素取min,最后将成功读取的元素个数更新回n返回。

  • 相关阅读:
    php静态调用非静态方法
    phalcon 框架3.0更新时报错
    centos7.5更换docker-ce镜像源
    腾讯云更换镜像源遇到的坑
    php cli模式下调试
    审查php.ini自动分析程序
    docker WARNING: IPv4 forwarding is disabled. Networking will not work.
    git常用命令,制作缩写命令
    学习GRPC(一) 简单实现
    mac与linux服务器之间使用ssh互通有无
  • 原文地址:https://www.cnblogs.com/bitetheddddt/p/15210062.html
Copyright © 2020-2023  润新知