Apache Doris 学习笔记: Backend存储层字符串编码与解码
字符串编码/解码
字符串编码主要逻辑位于BinaryDictPageBuilder以及其上下游的ColumnWriter和BitshufflePageBuilder。
它们在backend的storage_engine中负责把内存中的字符串数据(CHAR/VARCHAR,在EncodingInfoResolver中确定)进行编码压缩,打包成page并写入文件。
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类型)。
ScalarColumnWriter::finish_current_page()
从page_builder获取finish出来经过编码的page数据,然后加入自己的vector
如果自身是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的字符串。
解码步骤 :
- 求出_offsets_pos(通过element_number计算offset部分的开头)
- 通过index求找出对应的offset,
- 通过offset找到str
- 通过对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返回。