【ClickHouse源码】各类存储引擎的实现原理

注册各类存储引擎

ClickHouseServer端启动时会注册很多内容,包括存储引擎、函数、表函数等等,源码Server.cpp,如下:

int Server::main(const std::vector<std::string> & /*args*/)
{
    ......
    registerFunctions();
    registerAggregateFunctions();
    registerTableFunctions();
    registerStorages();
    registerDictionaries();
    ......
}

其中,registerStorages()就是进行存储引擎的注册,源码registerStorages.cpp,如下:

void registerStorages()
{
    auto & factory = StorageFactory::instance();

    registerStorageLog(factory);
    registerStorageTinyLog(factory);
    registerStorageStripeLog(factory);
    registerStorageMergeTree(factory);
    registerStorageNull(factory);
    registerStorageMerge(factory);
    registerStorageBuffer(factory);
    registerStorageDistributed(factory);
    registerStorageMemory(factory);
    registerStorageFile(factory);
    registerStorageURL(factory);
    registerStorageS3(factory);
    registerStorageDictionary(factory);
    registerStorageSet(factory);
    registerStorageJoin(factory);
    registerStorageView(factory);
    registerStorageMaterializedView(factory);
    registerStorageLiveView(factory);

    #if USE_HDFS
    registerStorageHDFS(factory);
    #endif

    #if USE_POCO_SQLODBC || USE_POCO_DATAODBC
    registerStorageODBC(factory);
    #endif
    registerStorageJDBC(factory);


    #if USE_MYSQL
    registerStorageMySQL(factory);
    #endif

    #if USE_RDKAFKA
    registerStorageKafka(factory);
    #endif
}

可以看出方法上半部分会注册LogMergeTreeS3等存储引擎,下半部分会根据宏定义决定是否会注册,各个宏值为:

#define USE_ICU 0
#define USE_MYSQL 1
#define USE_RDKAFKA 1
#define USE_EMBEDDED_COMPILER 0
#define USE_POCO_SQLODBC 0
#define USE_POCO_DATAODBC 1
#define USE_POCO_MONGODB 1
#define USE_POCO_REDIS 1
#define USE_INTERNAL_LLVM_LIBRARY 0
#define USE_SSL 1

所以MySQL和Kafka存储引擎默认是会注册的。

取其中一个registerStorageMergeTree(factory)方法细看一下,源码registerStorageMergeTree.cpp,如下:

void registerStorageMergeTree(StorageFactory & factory)
{
    factory.registerStorage("MergeTree", create);
    factory.registerStorage("CollapsingMergeTree", create);
    factory.registerStorage("ReplacingMergeTree", create);
    factory.registerStorage("AggregatingMergeTree", create);
    factory.registerStorage("SummingMergeTree", create);
    factory.registerStorage("GraphiteMergeTree", create);
    factory.registerStorage("VersionedCollapsingMergeTree", create);

    factory.registerStorage("ReplicatedMergeTree", create);
    factory.registerStorage("ReplicatedCollapsingMergeTree", create);
    factory.registerStorage("ReplicatedReplacingMergeTree", create);
    factory.registerStorage("ReplicatedAggregatingMergeTree", create);
    factory.registerStorage("ReplicatedSummingMergeTree", create);
    factory.registerStorage("ReplicatedGraphiteMergeTree", create);
    factory.registerStorage("ReplicatedVersionedCollapsingMergeTree", create);
}

该方法负责注册各种MergeTreeReplicatedMergeTree。既然所有引擎都注册到了工厂方法中,那在创建存储引擎时就会通过该factory来创建,实现了标准化封装。

创建各类存储引擎

创建存储引擎是在StorageFactory类中统一实现的,get方法在StorageFactory.h,如下:

StoragePtr get(
        ASTCreateQuery & query,
        const String & data_path,
        const String & table_name,
        const String & database_name,
        Context & local_context,
        Context & context,
        const ColumnsDescription & columns,
        const ConstraintsDescription & constraints,
        bool attach,
        bool has_force_restore_data_flag) const;

get方法通过ASTCreateQuery来决定创建什么存储引擎,并返回Storage指针后续使用。

这里的StoragePtr就是IStorage的指针,所以所有的存储引擎都是继承IStorage来实现不同的功能

各类存储引擎的实现

首先看一下实现了该基类的各个存储引擎类:

StorageBufferStorageDictionaryStorageDistributedStorageDistributedFakeStorageFileStorageFromMergeTreeDataPartStorageHDFSStorageInputStorageJoinStorageKafkaStorageLiveViewStorageLogStorageMaterializedViewStorageMemoryStorageMergeStorageMergeTreeStorageMySQLStorageNullStorageReplicatedMergeTreeStorageS3StorageSetStorageSetOrJoinBaseStorageStripeLogStorageSystemColumnsStorageSystemDetachedPartsStorageSystemDisksStorageSystemNumbersStorageSystemOneStorageSystemPartsStorageSystemPartsBaseStorageSystemPartsColumnsStorageSystemReplicasStorageSystemStoragePoliciesStorageSystemTablesStorageTinyLogStorageURLStorageValuesStorageViewStorageXDBC

可以看到标粗的(StorageReplicatedMergeTreeStorageDistributedStorageS3)就是经常会涉及到的存储引擎,这些引擎都是实现IStorage中的虚函数来实现不同引擎的存取的(即多态)。

基类IStorage的实现

先看下IStorage中到底有都有什么方法,源码IStorage.h,如下:

class IStorage : public std::enable_shared_from_this<IStorage>, public TypePromotion<IStorage>
{
public:
    ......
    virtual std::string getName() const = 0;
    virtual std::string getTableName() const = 0;
    virtual std::string getDatabaseName() const { return {}; }
    ......

public: 
    ......
    Block getSampleBlock() const; 
    Block getSampleBlockWithVirtuals() const; 
    Block getSampleBlockNonMaterialized() const; 
   
    void check(const Names & column_names, bool include_virtuals = false) const;
    void check(const NamesAndTypesList & columns) const;
    void check(const NamesAndTypesList & columns, const Names & column_names) const;
    void check(const Block & block, bool need_all = false) const;
    ......

public:
    virtual BlockInputStreams watch(
        const Names & /*column_names*/,
        const SelectQueryInfo & /*query_info*/,
        const Context & /*context*/,
        QueryProcessingStage::Enum & /*processed_stage*/,
        size_t /*max_block_size*/,
        unsigned /*num_streams*/)
    {
        throw Exception("Method watch is not supported by storage " + getName(), ErrorCodes::NOT_IMPLEMENTED);
    }

    virtual BlockInputStreams read(
        const Names & /*column_names*/,
        const SelectQueryInfo & /*query_info*/,
        const Context & /*context*/,
        QueryProcessingStage::Enum /*processed_stage*/,
        size_t /*max_block_size*/,
        unsigned /*num_streams*/);

    virtual BlockOutputStreamPtr write(
        const ASTPtr & /*query*/,
        const Context & /*context*/)
    {
        throw Exception("Method write is not supported by storage " + getName(), ErrorCodes::NOT_IMPLEMENTED);
    }

    virtual void startup() {}
    virtual void shutdown() {}

private:
    mutable RWLock alter_intention_lock = RWLockImpl::create();
    mutable RWLock new_data_structure_lock = RWLockImpl::create();
    mutable RWLock structure_lock = RWLockImpl::create();
}

这个类的虚函数比较多,这里保留了写比较核心的方法,下面做下简要说明:

  • 第一个public中主要是包含回去该存储引擎本身信息的一些方法;
  • 第二个public中主要是获取Block和验证Block的方法;
  • 第三个public中主要是readwritewatch方法及startupshutdown方法(用于在引擎创建启动和删除后的一些额外操作,比如启动/删除ReplicatedMergeTree的监听zk线程、自动merge线程等,类似统一hook);
  • 最后一个private中是流程中涉及到的锁;

StorageReplicatedMergeTree的实现

该类就是继承了IStorage的部分方法实现的,类在StorageReplicatedMergeTree.h,这里不再详细列举,主要是实现一些核心方法及StorageReplicatedMergeTree相关taskqueue_updating_taskmutations_updating_taskqueue_task_handlemove_parts_task_handlemerge_selecting_taskmutations_finalizing_task)的初始化。

下面看下ReplicatedMergeTree如何写入到磁盘:

       BlockOutputStreamPtr StorageReplicatedMergeTree::write()
                                 ↓ 
           void ReplicatedMergeTreeBlockOutputStream::write()
                                 ↓
                    storage.writer.writeTempPart()
                                 ↓
            MergedBlockOutputStrea out.writeWithPermutation()
                                 ↓
     std::pair<size_t, size_t> IMergedBlockOutputStream::writeColumn()
                                 ↓  
         size_t IMergedBlockOutputStream::writeSingleGranule()
                                 ↓
   void DataTypeLowCardinality::serializeBinaryBulkWithMultipleStreams()
                                 ↓
                 DataTypeString::serializeBinaryBulk()
                                 ↓
                       WriteBuffer ostr.write()

主要流程:

  1. 调用StorageReplicatedMergeTree::write()方法时会创建OutputStream,最终由OutputStreamwrite方法实现写逻辑;
  2. 写数据时先写临时part文件,其中的writeWithPermutation()方法是为了节省RAM
  3. ck每个数据文件都是一个列,writeColumn()来实现接下来的数据落盘,并且根据Granule间隔(默认8192)一块一块操作;
  4. 接下来就是序列化过程,对于StringArrayTuple等都是有不同的实现方式;
  5. 最后写入WriteBuffer,并按照数据量阈值或时间阈值实现真正的数据落盘;

StorageS3的实现

S3的实现就比较简单,只是实现几个核心方法,源码StorageS3.h

写盘流程如下:

               BlockOutputStreamPtr StorageS3::write()
                                 ↓ 
              void StorageS3BlockOutputStream::write()
                                 ↓
                  void WriteBufferFromS3::writePart()

StorageHDFS的实现

与S3的实现类似,也是实现几个核心方法,源码StorageHDFS.h

写盘流程如下:

           BlockOutputStreamPtr StorageHDFS::write()
                             ↓ 
             void HDFSBlockOutputStream::write()
                             ↓
                     tSize hdfsWrite()

操作hdfs时有专门的实现类库libhdfs3,源码hdfs.h,包含了hdfsWritehdfsReadhdfsOpenFile等。

总结

通过分析ck中的核心的ReplicatedMergeTree以及扩展的S3HDFSStorage实现,可以发现所有的实现都是继承IStorage基类的;

ReplicatedMergeTree由于是直接与标准文件系统接口交互,会有许多复杂的逻辑实现,如序列化/反序列化、Buffer刷盘等,还有*MergeTreeReplicated*的一些特性,所以实现会相对复杂,而S3HDFS有各自独特的一套接口协议,使用了不同的接口实现了IStorage中的必要方法;

如果要实现一种表引擎,ck的模式是通过实现IStorage基类的方法作为标准化规范,再实现一套对应的数据流读写方法,这一层也同样要通过实现IBlockInputStream/IBlockOutputStream中的一些列标准方法,最后是有一套新存储系统的client标准类库作为支撑;

WriteBuffer的实现

如果想要知道clickhouse如何和Linux文件系统交互的,可以继续从StorageReplicatedMergeTree 中的WriteBuffer ostr.write()继续深入分析。在clickhouse中的buffer有很多实现,但都是基于WriteBuffer实现的。源码WriteBuffer.h

class WriteBuffer : public BufferBase
{
public:
    WriteBuffer(Position ptr, size_t size) : BufferBase(ptr, size, 0) {}
    void set(Position ptr, size_t size) { BufferBase::set(ptr, size, 0); }

    inline void next()
    {
        if (!offset() && available())
            return;
        bytes += offset();

        try
        {
            nextImpl();
        }
        catch (...)
        {
            pos = working_buffer.begin();
            throw;
        }

        pos = working_buffer.begin();
    }

    virtual ~WriteBuffer() {}

    inline void nextIfAtEnd()
    {
        if (!hasPendingData())
            next();
    }


    void write(const char * from, size_t n)
    {
        size_t bytes_copied = 0;

        while (bytes_copied < n)
        {
            nextIfAtEnd();
            size_t bytes_to_copy = std::min(static_cast<size_t>(working_buffer.end() - pos), n - bytes_copied);
            memcpy(pos, from + bytes_copied, bytes_to_copy);
            pos += bytes_to_copy;
            bytes_copied += bytes_to_copy;
        }
    }


    inline void write(char x)
    {
        nextIfAtEnd();
        *pos = x;
        ++pos;
    }

private:
    virtual void nextImpl() { throw Exception("Cannot write after end of buffer.", ErrorCodes::CANNOT_WRITE_AFTER_END_OF_BUFFER); }
}

主要的核心方法有next()write()set()等。

这里先看一下set(),是由BufferBase类的set实现的,那BufferBase是做什么用的呢?看一下描述,源码BufferBase.h

  • ReadBufferWriteBuffer分别类似于istreamostream
  • 使用iostream不可能有效地实现某些操作,例如使用istream,无法从制表符分隔的文件中快速读取字符串值;
  • ReadBufferWriteBuffer提供了对内部缓冲区的直接访问;
  • ReadBufferWriteBuffer可以拥有或不拥有自己的内存;

可知,BufferBase是为了实现一种高效的buffer,提供给clickhouse更快的读写效率。

着重看一下next(),其中主要就是调用了nextImpl()nextImpl()由不同的buffer做了不同的实现,以WriteBufferFromOStreamWriteBufferFromFileDescriptor为例看一下具体实现。源码WriteBufferFromOStream.cppWriteBufferFromFileDescriptor.cpp

WriteBufferFromOStream::nextImpl()

void WriteBufferFromOStream::nextImpl()
{
    if (!offset())
        return;

    ostr->write(working_buffer.begin(), offset());
    ostr->flush();

    if (!ostr->good())
        throw Exception("Cannot write to ostream at offset " + std::to_string(count()),
            ErrorCodes::CANNOT_WRITE_TO_OSTREAM);
}

OStream中的write()就类似std::ostreamwrite(),会将数据以流的形式写入文件,并执行flush().

WriteBufferFromFileDescriptor::nextImpl()

void WriteBufferFromFileDescriptor::nextImpl()
{
    if (!offset())
        return;

    Stopwatch watch;

    size_t bytes_written = 0;
    while (bytes_written != offset())
    {
        ProfileEvents::increment(ProfileEvents::WriteBufferFromFileDescriptorWrite);

        ssize_t res = 0;
        {
            CurrentMetrics::Increment metric_increment{CurrentMetrics::Write};
            res = ::write(fd, working_buffer.begin() + bytes_written, offset() - bytes_written);
        }

        ......

        if (res > 0)
            bytes_written += res;
    }

    ......
}

FileDescriptor中的::write()就是unistd.hwrite()方法,通过fd操作数据写入文件。

总结

clickhouse根据自身需要,通过BufferBase构建了特殊的buffer,最终通过各个实现类不同的nextImpl()方法实现不同的功能的差异化;

Stream中的write()就类似std::ostreamwrite(),这里是隐式调用了open()等方法,直接的文件操作就是是直接的带fd参数的write()

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一只努力的微服务

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值