【ClickHouse源码】ReadIndirectBufferFromRemoteFS介绍

ReadIndirectBufferFromRemoteFS直译过来就是针对远程文件系统创建的间接的ReadBuffer,这是由于远程文件系统并不能像本地文件系统一样直接操作文件,所以通过ReadBuffer抽象出了必要的接口,让各种ReadBuffer去实现,ReadIndirectBufferFromRemoteFS就是其中之一。

头文件如下:

class ReadIndirectBufferFromRemoteFS : public ReadBufferFromFileBase
{

public:
    explicit ReadIndirectBufferFromRemoteFS(std::shared_ptr<ReadBufferFromRemoteFSGather> impl_);

    off_t seek(off_t offset_, int whence) override;

    off_t getPosition() override;

    String getFileName() const override;

    void setReadUntilPosition(size_t position) override;

    void setReadUntilEnd() override;

private:
    bool nextImpl() override;

    std::shared_ptr<ReadBufferFromRemoteFSGather> impl;

    size_t file_offset_of_buffer_end = 0;
};

其中标有override的函数就是需要重写实现的函数。

对于这类的包装的ReadBuffer都会包含一个对应能够读远程文件的ReadBuffer,就比如它的成员变量impl,类型为ReadBufferFromRemoteFSGather。看一下最主要的nextImpl函数:

bool ReadIndirectBufferFromRemoteFS::nextImpl()
{
    /// Transfer current position and working_buffer to actual ReadBuffer
    swap(*impl);

    assert(!impl->hasPendingData());
    /// Position and working_buffer will be updated in next() call
    auto result = impl->next();
    /// and assigned to current buffer.
    swap(*impl);

    if (result)
    {
        file_offset_of_buffer_end += available();
        BufferBase::set(working_buffer.begin() + offset(), available(), 0);
    }

    assert(file_offset_of_buffer_end == impl->file_offset_of_buffer_end);

    return result;
}

首先就是通过swap来将Impl的buffer真正的交换给ReadIndirectBufferFromRemoteFS,保证ReadIndirectBufferFromRemoteFS的buffer是Impl buffer的当前状态。然后通过impl->next()将数据读入Impl buffer,读入完成后再将Impl buffer的状态交换给ReadIndirectBufferFromRemoteFS的buffer,并更新file_offset_of_buffer_end,完成一次next读。其实一般的next流程大体都是这样。

ReadIndirectBufferFromRemoteFS的next实际是调用了Impl的next,ReadBufferFromRemoteFSGather是对远程文件的一层代理封装,因为ClickHouse的一个数据文件有可能非常大,对于远程文件系统而言,这一个文件有可能对应多个远程文件,所以ReadBufferFromRemoteFSGather对读一个ClickHouse数据文件进行了封装,屏蔽实际的读取细节,让上层觉得就是在读一个远程文件。

ReadBufferFromRemoteFSGather

ReadBufferFromRemoteFSGather目的就是要逐个的读取所对应的远程文件。

构造函数

ReadBufferFromRemoteFSGather(
    const std::string & common_path_prefix_,
    const BlobsPathToSize & blobs_to_read_,
    const ReadSettings & settings_);

看其构造函数,入参包含common_path_prefix_、blobs_to_read_和settings_。common_path_prefix_是对应远程文件系统的路径前缀,如果对于S3来说,那common_path_prefix_就是{bucket}/{path_pre}/。BlobsPathToSize是个vector,存储这所有的远程文件以及文件的大小。这个vector也是有序的,如果再用S3类比,那这里就是多个object和object_size。

createImplementationBufferImpl

这个是ReadBufferFromRemoteFSGather中提供的虚函数,因为远程文件系统并不是只有S3,还有HDFS、BLOB等,所以ImplementationBuffer也就是真正直接和远程文件系统交互等buffer需要有不同的实现,例如S3就有其实现类ReadBufferFromS3Gather。具体看下代码:

SeekableReadBufferPtr ReadBufferFromS3Gather::createImplementationBufferImpl(const String & path, size_t file_size)
{
    auto remote_path = fs::path(common_path_prefix) / path;
    auto remote_file_reader_creator = [=, this]()
    {
        return std::make_unique<ReadBufferFromS3>(
            client_ptr, bucket, remote_path, version_id, max_single_read_retries,
            settings, /* use_external_buffer */true, /* offset */ 0, read_until_position, /* restricted_seek */true);
    };

    if (with_cache)
    {
        return std::make_shared<CachedReadBufferFromRemoteFS>(
            remote_path, settings.remote_fs_cache, remote_file_reader_creator, settings, query_id, read_until_position ? read_until_position : file_size);
    }

    return remote_file_reader_creator();
}

主要有两部分逻辑:

如果启用cache,那么会使用CachedReadBufferFromRemoteFS;如果不使用cache,那么直接使用裸的ReadBufferFromS3。即便这里使用了两种实现,但是在使用CachedReadBufferFromRemoteFS时,如果本地没有cache,同样还是会使用ReadBufferFromS3,去请求远程文件数据。

其中ReadBufferFromS3的nextImpl实现还是有一定复杂性的,这里先只需简单了解下,ReadBufferFromS3就是通过initailize来初始化一个S3对象的流,一个next调用,就会获取该流的数据填充到buffer中,并且每次最多读取buffer_size大小的数据供上层使用。

nextImpl

再看ReadBufferFromRemoteFSGather的nextImpl函数,上面提到的ReadIndirectBufferFromRemoteFS的next实际就是调用的这个nextImpl。在nextImpl中为了方便又封装了一层readImpl,代码如下:

bool ReadBufferFromRemoteFSGather::readImpl()
{
    // step 1
    swap(*current_buf);

    bool result = false;

    /**
     * Lazy seek is performed here.
     * In asynchronous buffer when seeking to offset in range [pos, pos + min_bytes_for_seek]
     * we save how many bytes need to be ignored (new_offset - position() bytes).
     */
    // step 2
    if (bytes_to_ignore)
    {
        total_bytes_read_from_current_file += bytes_to_ignore;
        current_buf->ignore(bytes_to_ignore);
        result = current_buf->hasPendingData();
        file_offset_of_buffer_end += bytes_to_ignore;
        bytes_to_ignore = 0;
    }

    // step 3
    if (!result)
        result = current_buf->next();

    if (blobs_to_read.size() == 1)
    {
        file_offset_of_buffer_end = current_buf->getFileOffsetOfBufferEnd();
    }
    else
    {
        /// For log family engines there are multiple s3 files for the same clickhouse file
        file_offset_of_buffer_end += current_buf->available();
    }

    // step 4
    swap(*current_buf);

    /// Required for non-async reads.
  
    // step 5
    if (result)
    {
        assert(available());
        nextimpl_working_buffer_offset = offset();
        total_bytes_read_from_current_file += available();
    }

    return result;
}

Step1

这里以S3为例来说明整个流程,所以current_buf就是ReadBufferFromS3Gather,是通过调用initialize来完成初始化的。initialize会根据file_offset_of_buffer_end来遍历所有远程文件的大小(因为在BlobsPathToSize中存储了大小,这个逻辑并不需要访问远程文件系统),知道定位到要从哪个文件开始读,如图:

clickhouse file:  [-------------------------------------------------]
                      file1          file2        file3       file4
remote files:    {
    
    [------------][------------][-----------][--------]}

need_to_read:                       [_________________________]
                                    ^
                                    file_offset_of_buffer_end
need_to_seek                     [__]

如上图所示,一个bin文件中会包含多个s3的object,每个object都记录了path和size,当给定file_offset_of_buffer_end时,通过for循环从第一个object开始遍历,去一个临时变量offset = file_offset_of_buffer_end,如果第一个object的size大于offset,说明需要从offset处开始读第一个object获取数据,如果第一个object的size小于offset,说明需要跳过第一个object去判断第二个object是否能包含所指定的offset,这时跳过了第一个object,那么offset自然也要减去第一个object的size,才是第二个object的相对offset。当遍历到file2时,已经找到要读的起始点,选择file2,并seek need_to_seek个字节,构造current_buf并返回。

Step2

继续看bytes_to_ignore,这里使用了lazy seek,因为ClickHouse支持prefetch预读能力,预读会使用额外一个外部buffer进行读取,当在这里调用next时,如果需要的数据已被预读完成,那就可以直接拿过来使用,但是外部buffer预读并不能准确的按照所需的file_offset进行预读,所以需要bytes_to_ignore来调整下file_offset_of_buffer_end的位置,所以这里使用了lazy seek。

Step3

调用current_buf->next()来获取远程文件数据了。具体的实现根据是不是用cache分别对应CachedReadBufferFromRemoteFS或者ReadBufferFromS3,详细的上面已经介绍了。

Step4

做swap。

Step5

更新file_offset_of_buffer_end和total_bytes_read_from_current_file。

其实到这里会发现readImpl只是实现一个远程文件的读取,因为在整个函数里并没有构造新远程文件buffer的地方,这其实就是为什么要单独封装一个readImpl的作用。在nextImpl中如果调用readImpl读取完了发现还有文件需要继续读,会调用moveToNextBuffer,这个函数会构造新的current_buf继续读,直到所有文件都读取完成。

到此可以知道current_buf被构造出来使用,也知道S3的current_buf是通过ReadBufferFromS3Gather::createImplementationBufferImpl函数构造的,具体是怎么构造的呢?代码如下:

到此ReadIndirectBufferFromRemoteFS的主要流程就介绍完了。

猜你喜欢

转载自blog.csdn.net/weixin_39992480/article/details/124871572