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的主要流程就介绍完了。