暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

ClickHouse In Memory Part 与 WAL 分析

ClickHouse开发者 2021-03-09
1148

相关 PR

https://github.com/ClickHouse/ClickHouse/pull/10697

使用

在 MergeTree 表引擎中添加 part 的 in memory format,将数据存储在内存中。part 在第一次合并时写入磁盘。如果 part 的行或字节大小小于阈值min_rows_for_compact_part
min_bytes_for_compact_part
,则将以内存格式创建 part。还提供可选的Write-Ahead-Log
支持,默认情况下启用此功能,并通过设置in_memory_parts_enable_wal
进行控制。

示例 SQL

CREATE TABLE default.mem (  a UInt32,  b UInt32,  c UInt32) ENGINE = MergeTree()PARTITION BY aORDER BY bSETTINGS min_rows_for_compact_part = 3;

当单次插入数据行数小于 3 时,就会以 in memory part 的形式插入,in memory part 的数据暂时不落盘,但会写日志到wal.bin
文件。

Write Ahead Log

相关实现主要在src/Storages/MergeTree/MergeTreeWriteAheadLog.h

当插入如下一行数据时,可以看到wal.bin
中的内容。

localhost :) insert into default.mem values(1,2,3);
$ hexdump -C wal.bin00000000  01 00 02 7b 7d 00 07 31  5f 31 5f 31 5f 30 03 01  |...{}..1_1_1_0..|00000010  01 61 06 55 49 6e 74 33  32 01 00 00 00 01 62 06  |.a.UInt32.....b.|00000020  55 49 6e 74 33 32 02 00  00 00 01 63 06 55 49 6e  |UInt32.....c.UIn|00000030  74 33 32 03 00 00 00                              |t32....|00000037

一个 part 对应wal.bin
一条记录,插入一个 part 会被序列化为:Version+metadata+ActionType+PartName+Block

具体逻辑在void MergeTreeWriteAheadLog::addPart(DataPartInMemoryPtr & part)

void MergeTreeWriteAheadLog::addPart(DataPartInMemoryPtr & part){    ...    writeIntBinary(WAL_VERSION, *out);    ActionMetadata metadata{};    metadata.part_uuid = part->uuid;    metadata.write(*out);    writeIntBinary(static_cast<UInt8>(ActionType::ADD_PART), *out);    writeStringBinary(part->name, *out);    block_out->write(part->block);    block_out->flush();    ...}
  • WAL_VERSION
    UInt8
    类型,1byte。

  • ActionMetadata
    ,包括min_compatible_version(1byte),ser_meta.length()(VarInt)

  • ActionType
    表示这次操作是插入还是删除。

  • PartName
    表示这一 part 的分区键,最小 block,最大 block,merge level。在序列化为字符串时还会在开始写入PartName
    长度。

  • Block
    NativeBlockOutputStream
    负责序列化,具体实现逻辑在src/DataStreams/NativeBlockOutputStream.h

删除 part 与插入的日志类似,只是不会插入block
内容。

从 WAL 中恢复数据

当重启 ClickHouse 时,需要从 WAL 中恢复 in memory part。

主要实现逻辑在MergeTreeData::MutableDataPartsVector MergeTreeWriteAheadLog::restore(const StorageMetadataPtr & metadata_snapshot)
,会在void MergeTreeData::loadDataParts(bool skip_sanity_checks)
中被调用。

MergeTreeData::MutableDataPartsVector MergeTreeWriteAheadLog::restore(const StorageMetadataPtr & metadata_snapshot){    ...    while (!in->eof())    {        ...        try        {            ...//读metadata            if (action_type == ActionType::DROP_PART)            {                dropped_parts.insert(part_name);            }            else if (action_type == ActionType::ADD_PART)            {                ...//从wal中读block内容            }            else            {                throw Exception("Unknown action type: " + toString(static_cast<UInt8>(action_type)), ErrorCodes::CORRUPTED_DATA);            }        }        catch (const Exception & e)        {           ...        }        if (action_type == ActionType::ADD_PART)        {            ...//在内存中重建in memory part        }    }    MergeTreeData::MutableDataPartsVector result;    std::copy_if(parts.begin(), parts.end(), std::back_inserter(result),        [&dropped_parts](const auto & part) { return dropped_parts.count(part->name) == 0; });    return result;}

in memory part 写入数据

实现主要在src/Storages/MergeTree/MergeTreeDataPartWriterInMemory.h

void MergeTreeDataPartWriterInMemory::write(    const Block & block, const IColumn::Permutation * permutation)

block
为写入的数据块,permutation
用来对block
的数据排序,若为nullptr
则不需要排序。

        ...    if (permutation)//排序    {        ...    }    else    {        for (const auto & col : columns_list)            result_block.insert(block.getByName(col.name));    }        ...    part_in_memory->block = std::move(result_block);    if (settings.rewrite_primary_key)        calculateAndSerializePrimaryIndex(primary_key_block);

主要逻辑包括排序,然后将插入的block
移动到 part 中的 block,然后计算主键。在 in memory part 中数据的存储格式仍然是列式。

void MergeTreeDataPartWriterInMemory::calculateAndSerializePrimaryIndex(const Block & primary_index_block){//计算主键    size_t rows = primary_index_block.rows();    if (!rows)        return;    size_t primary_columns_num = primary_index_block.columns();    index_columns.resize(primary_columns_num);    for (size_t i = 0; i < primary_columns_num; ++i)    {        const auto & primary_column = *primary_index_block.getByPosition(i).column;        index_columns[i] = primary_column.cloneEmpty();        index_columns[i]->insertFrom(primary_column, 0);        if (with_final_mark)            index_columns[i]->insertFrom(primary_column, rows - 1);    }}

在计算主键时,可以看出主键只会将第一行的主键值插入到index_columns
中,并会根据with_final_mark
来确定是否将block
的最后一行插入到主键中。这里猜测是因为 in memory part 的数据量一般比较小,所以作了比较简单的计算。

in memory part 读取数据

实现主要在src/Storages/MergeTree/MergeTreeDataPartReaderInMemory.h

主要实现逻辑在readRows
中,将 in memory part 中的数据读到res_columns
中并返回读取的行数。

如果读取的是整个 part 的行,则会把 part 中的列读到结果列中,如果只是部分行,则会创建新的列,并读取所需行。

size_t MergeTreeReaderInMemory::readRows(size_t from_mark, bool continue_reading, size_t max_rows_to_read, Columns & res_columns){    ...    auto column_it = columns.begin();    for (size_t i = 0; i < num_columns; ++i, ++column_it)    {        auto [name, type] = getColumnFromPart(*column_it);        /// Copy offsets, if array of Nested column is missing in part.        auto offsets_it = positions_for_offsets.find(name);        if (offsets_it != positions_for_offsets.end())        {            ...        }        else if (part_in_memory->block.has(name))        {            const auto & block_column = part_in_memory->block.getByName(name).column;            if (rows_to_read == part_rows)            {                res_columns[i] = block_column;            }            else            {                if (res_columns[i] == nullptr)                    res_columns[i] = type->createColumn();                auto mutable_column = res_columns[i]->assumeMutable();                mutable_column->insertRangeFrom(*block_column, total_rows_read, rows_to_read);                res_columns[i] = std::move(mutable_column);            }
        }
    }

    total_rows_read += rows_to_read;
    return rows_to_read;
}



关注 ClickHouse开发者 ,获取更多技术干货


文章转载自ClickHouse开发者,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论