【ClickHouse源码】通过CachedReadBufferFromRemoteFS了解Cache缓存机制

CachedReadBufferFromRemoteFS为支持本地Cache封装的一个ReadBuffer。

下面主要介绍CachedReadBufferFromRemoteFS的3个函数,来理解ClickHouse Cache的实现。

预先了解下3种读类型(ReadType):

  • CACHED:本地缓存中已存在
  • REMOTE_FS_READ_BYPASS_CACHE:在读远端文件时,如果本地缓存不存在,不做任何操作,直接绕过缓存读远端文件
  • REMOTE_FS_READ_AND_PUT_IN_CACHE:在读远端文件时,如果本地缓存不存在,读远端文件并写入本地缓存

initialize

熟悉ReadBufferFromS3的应该会比较好理解,每个ReadBufferFromS3都需要初始化一个数据源,供上层读取数据。ReadBufferFromS3的initialize就是通过S3Client携带必要的参数(如bucket、object_name、offset等)请求S3返回一个数据流。CachedReadBufferFromRemoteFS的initialize的作用与其类似,同样是初始化一个数据源,不同的是,它对应的是本地缓存文件(如果本地缓存文件不存在,会对应一个远端文件流)。并且根据丰富的控制参数来实现不同场景的读需求。

代码如下:

void CachedReadBufferFromRemoteFS::initialize(size_t offset, size_t size)
{
    if (settings.read_from_filesystem_cache_if_exists_otherwise_bypass_cache)
    {
        file_segments_holder.emplace(cache->get(cache_key, offset, size));
    }
    else
    {
        file_segments_holder.emplace(cache->getOrSet(cache_key, offset, size));
    }

    /**
     * Segments in returned list are ordered in ascending order and represent a full contiguous
     * interval (no holes). Each segment in returned list has state: DOWNLOADED, DOWNLOADING or EMPTY.
     */
    if (file_segments_holder->file_segments.empty())
        throw Exception(ErrorCodes::LOGICAL_ERROR, "List of file segments cannot be empty");

    LOG_TEST(log, "Having {} file segments to read", file_segments_holder->file_segments.size());
    current_file_segment_it = file_segments_holder->file_segments.begin();

    initialized = true;
}

file_segments_holder会持有file_segments列表,每个file_segment就是远端文件被拆分成的一个个文件快(有可能是一个或多个,不会为空),并且列表中所有的file_segment是按照升序排列的,也是为了对应真实远端文件的顺序。

首先介绍下file_segment的状态,一共有5种:

  • DOWNLOADED:已经被下载到本地;
  • EMPTY:file_segment第一次被创建时的状态,还没有开始下载数据;
  • DOWNLOADING:当file_segment被设置downloader时,file_segment会被设置为下载中状态;
  • PARTIALLY_DOWNLOADED_NO_CONTINUATION:file_segment只有部分下载成功,读取时会先读已下载的的部分,未下载的部分继续去远端读取文件,不会将未下载的部分补全;
  • PARTIALLY_DOWNLOADED:对于PARTIALLY_DOWNLOADED_NO_CONTINUATION,这种状态会被其他downloader将未下载的部分继续补全;
  • SKIP_CACHE:由于磁盘空间不足等问题,遇此状态的file_segment直接跳过缓存

在初始化时,设置read_from_filesystem_cache_if_exists_otherwise_bypass_cache(缓存存在就读,不存在就跳过)为true,则通过cache->get()来获取file_segments,如果获取不到,填充一个EMPTY的file_segment到file_segments_holder中,如果能够获取到file_segments,file_segments_holder会持有一个file_segments列表,此时列表中包含的每个file_segment就是已下载到本地的文件块。但此时并不能保证,所需的range中所有file_segment在本地都是存在的,势必会有孔洞存在,如图:

                  segment{1}     segment{2}        segment{k}
file_segments:  {[----------][________________]...[----______]}

所以在获取到file_segments列表后,这些孔洞会同样会被EMPTY的file_segment进行站位填充,保证返回一个无孔洞的file_segments列表。

如果read_from_filesystem_cache_if_exists_otherwise_bypass_cache为false,则通过cache->getOrSet()来获取file_segments,不同于cache->get()的是,cache->getOrSet()会创建cell(可以简单理解为file_segment的一个wrapper),cell会根据file_offset和total_size所包含的range请求远端文件,并将range进行segment切分,在本地cache路径(cache_base_path)下创建相应cache路径,这里有个大小限制,最大的segment为max_file_segment_size,所以segment的大小以total_size和max_file_segment_size的最小只为准。

注意,这两部分其实都是先构造出需要的file_segment,并没有触发download。

nextImplStep

nextImplStep是CachedReadBufferFromRemoteFS中nextImpl的实现,是填充read_buffer的方法。在CachedReadBufferFromRemoteFS中implementation_buffer是私有的buffer,所有的buffer操作都是针对implementation_buffer来做的,但外部使用的是CachedReadBufferFromRemoteFS,也就是其基类BufferBase中的working_buffer和internal_buffer,所以在操作implementation_buffer的前后都需要做swap操作,将implementation_buffer和base_buffer对齐,外部使用才会正确。

代码中可以看到在implementation_buffer完成构造后和填充数据后都会执行:

swap(*implementation_buffer);

所以该函数作用就是如何构造和填充implementation_buffer。

该函数主要分下面几个过程:

  • 初始化file_segments
  • 构造implementation_buffer
  • 预下载
  • implementation_buffer读数据
  • 更细file_offset

下面逐点进行分析:

初始化file_segments

if (!initialized)
    initialize(file_offset_of_buffer_end, getTotalSizeToRead());

如果CachedReadBufferFromRemoteFS没有初始化,会进行初始化,由于key是CachedReadBufferFromRemoteFS构造时传入的,初始化不需要传参key,只需要offset和size,getTotalSizeToRead是对各种情况做了简单封装,正常会返回read_until_position - file_offset_of_buffer_end,也就是从offset到file_offset+total_size,其余如果有越界之类的情况,getTotalSizeToRead会抛出异常。

构造implementation_buffer

if (implementation_buffer)
{
    bool can_read_further = updateImplementationBufferIfNeeded();
    if (!can_read_further)
        return false;
}
else
{
    implementation_buffer = getImplementationBuffer(*current_file_segment_it);

    if (read_type == ReadType::CACHED)
        (*current_file_segment_it)->incrementHitsCount();
}

assert(!internal_buffer.empty());
swap(*implementation_buffer);
implementation_buffer已被构造

再次进入nextImpl函数,则需要updateImplementationBufferIfNeeded来进行更新,比如上次nextImpl已经将第一个file_segment读完了,新的一轮需要读下一个file_segment,或者上次nextImpl并没有读完当前file_segment,新的一轮可以继续读。上一个file_segment读完,会通过completeFileSegmentAndGetNext函数更换到下一个file_segment,如果上次的nextImpl没读完,updateImplementationBufferIfNeeded函数会根据不同的ReadType,来进行更新。

首先介绍下file_segment,file_segment有5种状态:

  • DOWNLOADED:已经被下载到本地;
  • EMPTY:file_segment第一次被创建时的状态,还没有开始下载数据;
  • DOWNLOADING:当调用getOrSetDownloader时,file_segment会被设置为下载中状态;
  • PARTIALLY_DOWNLOADED_NO_CONTINUATION:file_segment只有部分下载成功,读取时会先读已下载的的部分,为下载的部分继续去远端读取文件,不会将为下载的部分补全;
  • PARTIALLY_DOWNLOADED:对于PARTIALLY_DOWNLOADED_NO_CONTINUATION,这种状态会被其他downloader将为下载的部分补全;
  • SKIP_CACHE:由于磁盘空间不足等问题,遇此状态的file_segment直接跳过缓存

继续看implementation_buffer是如何更新的,这里分为两种情况:

ReadType::CACHED

如果是ReadType::CACHED类型,file_segment的状态为DOWNLOADED,那继续读就好,因为本地已经缓存好了;状态不是DOWNLOADED,那就需要判断已经读到的file_offset_of_buffer_end和download_offset的大小关系来判断,其中download_offset是指file_segment已经下载到的file_offset,如图:

case 1:
                       segment{k}
cache:           [______|___________]
                        ^
                        download_offset
requested_range:    [__________]
                    ^
                    file_offset_of_buffer_end
downloaded:         |___|
need_to_download:       |___________|                   
case 2:
                       segment{k}
cache:           [______|___________]
                        ^
                        download_offset
requested_range:        [__________]
                        ^
                        file_offset_of_buffer_end
downloaded:             |
need_to_download:       |___________|    

如果file_offset_of_buffer_end < download_offset,说明有一部分数据(downloaded)已经下载好了可以继续读。如果file_offset_of_buffer_end == download_offset,说明这个正在下载的file_segment中已下载的部分(downloaded)已经读完,未下载的部分还处在下载中,所以没办法继续读,需要构造新的implementation_buffer。

ReadType::REMOTE_FS_READ_AND_PUT_IN_CACHE

如果是ReadType::REMOTE_FS_READ_AND_PUT_IN_CACHE类型,说明并没有cache可供读取,需要构造新的implementation_buffer。

implementation_buffer未被构造

通过getImplementationBuffer函数直接构造,构造都是通过getImplementationBuffer函数来完成的,后面再详细说明。

最后要通过swap来使implementation_buffer和base_buffer保持一致。

这里多处涉及到了构造新的implementation_buffer,所以先穿插介绍下用于构造implementation_buffer的函数。

getReadBufferForFileSegment

上面说到过,file_segments_holder会持有file_segments列表,需要的缓存数据都在一个个file_segment中,所以要构造file_segment的read_buffer来获取file_segment中的数据。

在该函数中首先会根据参数read_from_filesystem_cache_if_exists_otherwise_bypass_cache进行快速判断,如果为true,说明缓存存在就读不存在就跳过去直接读远端文件,不会出发下载操作;如果为false,则会继续更复杂的逻辑。

下面的逻辑就和file_segment的状态强相关了:

  • FileSegment::State::SKIP_CACHE

    如果是SKIP_CACHE,则会将read_type设置为REMOTE_FS_READ_BYPASS_CACHE,并返回一个RemoteFSReadBuffer,直接读远端文件

  • FileSegment::State::EMPTY

    如果是EMPTY,首先会去尝试获取或设置downloader,这里还有个caller_id的概念,caller_id是request_id+thread_id构成的,downloader_id就是file_segment在没有downloader_id的情况下首次被设置的caller_id。如果该线程是file_segment的downloader,则会将read_type设置为REMOTE_FS_READ_AND_PUT_IN_CACHE。这里有个小细节,如果file_segment的download_offset和file_offset_of_buffer_end相等,说明刚好要从file_segment的开头开始创建buffer,所以直接返回RemoteFSReadBuffer即可,如果download_offset < file_offset_of_buffer_end,如下图所:

                                 segment{k}
    cache:                  [_________________
                            ^
                            download_offset
    requested_range:            [__________]
                                ^
                                file_offset_of_buffer_end
    bytes_to_predownload:   |___| 
    

    bytes_to_predownload也就是要预下载的部分,然后再返回RemoteFSReadBuffer。

  • FileSegment::State::DOWNLOADING

    如果是DOWNLOADING,说明file_segment已经在下载中,如果download_offset > file_offset_of_buffer_end,则说明已下载的部分已经满足buffer需求,从已经下载的部分中返回一个CacheReadBuffer即可。

  • FileSegment::State::DOWNLOADED

    如果是DOWNLOADED,说明file_segment已经完整的存在在本地磁盘中,直接返回CacheReadBuffer读取即可。

  • FileSegment::State::PARTIALLY_DOWNLOADED

    如果是PARTIALLY_DOWNLOADED,说明file_segment已经没有downloader了,需要重新设置一个新的downloader来继续下载。如果是部分下载,且download_offset > file_offset_of_buffer_end,同样说明已下载的部分已经满足buffer需求,不需要继续进行下载了,并且将downloader置空。如果download_offset < file_offset_of_buffer_end,说明已下载的部分不足以满足buffer的需求,同样需要预下载bytes_to_predownload部分,再返回RemoteFSReadBuffer。

  • FileSegment::State::PARTIALLY_DOWNLOADED_NO_CONTINUATION

    如果是PARTIALLY_DOWNLOADED_NO_CONTINUATION,说明缓存中有就读,没有直接读远端文件,不存在下载等操作。

最后要注意,这些状态是在while循环里不停切换等,通过上层的next来触发不断的读取。

预下载

如果bytes_to_predownload不为0,则需要预下载,这里的预下载可以理解为是一种lazy seek,不过这里的predownload会将file_segment的前面不需要的部分缓存到本地,然后从需要的位置开始构造implementation_buffer并继续使用。如图:

                             segment{k}
cache:                  [--------____________
                                 ^
                                 download_offset
requested_range:                     [__________]
                                     ^
                                     file_offset_of_buffer_end
bytes_to_predownload:            |___| 

虽然在本地查询时bytes_to_predownload并未使用,但是其他查询很有可能就会用到,与其使用lazy seek忽略掉部分数据,不如将其缓存下来供后面查询使用,也可以理解为一种预读。

所以file_segment无论处于什么状态,要么是空,要么就是头部有部分数据,要么就是全部被填满数据,不会出现头部空中间有部分数据的情况。

implementation_buffer读数据

一般的read_buffer在读数据时正常将数据读出到buffer里即可,CachedReadBufferFromRemoteFS会多一个过程,如果该线程是当前file_segment的downloader时,需要额外将buffer中读到的数据写入本地缓存文件块中。

读取数据和写入本地缓存文件块完成后,还要进行一次swap,将implementation_buffer同步到base_buffer。

最后还要将file_segment的downloader置空,downloader的生命周期只在一次next中有效,这样就可以灵活的处理下载和切换downloader。

更新file_offset

将file_offset累加已读取的size,记录read_buffer读到了哪里。

seek

seek也是read_buffer中必须要实现的函数,就是设置buffer的开始读取位置,一般包含SEEK_SET、SEEK_CUR和SEEK_END这3种类型。但是对于CachedReadBufferFromRemoteFS只支持SEEK_SET,并且在CachedReadBufferFromRemoteFS还未初始化时才可以调用。代码如下:

off_t CachedReadBufferFromRemoteFS::seek(off_t offset, int whence)
{
    if (initialized)
        throw Exception(ErrorCodes::CANNOT_SEEK_THROUGH_FILE, "Seek is allowed only before first read attempt from the buffer");

    if (whence != SEEK_SET)
        throw Exception(ErrorCodes::CANNOT_SEEK_THROUGH_FILE, "Only SEEK_SET allowed");

    first_offset = offset;
    file_offset_of_buffer_end = offset;
    size_t size = getTotalSizeToRead();
    initialize(offset, size);

    return offset;
}

该函数主要就是设置file_offset_of_buffer_end,并调用initialize初始化CachedReadBufferFromRemoteFS。

到此CachedReadBufferFromRemoteFS的主要函数就介绍完了,通过这3个方法就可以整体了解了cache的具体执行流程。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

一只努力的微服务

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

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

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

打赏作者

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

抵扣说明:

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

余额充值