前言
本文基于rocketmq5.2.0,分析5.x新特性-分层存储 (Tiered Store)。
分层存储,简单来说是将原来存储在本地磁盘的数据文件,存储到其他外部设备中,后文简称原来的数据文件(commitlog、consumequeue、index等)为主存。
相关历史文章参考:
- rocketmq4.x普通消息发送(主存流程和格式,包括commitlog、consumequeue、index);
另外,写完这一篇,我可能要停笔一阵子了,找工作去了,看八股文想yue。
5.2版本新特性
1、分层存储-Index存储重构
见ISSUE-7545(RIP-65)。
5.2版本对分层存储的Index索引存储进行了重构,所以本文不基于5.1.1分析。
2、引入Rocksdb存储部分数据
见ISSUE-7064(RIP-66)。
问题:config目录下的配置,有一个变化就需要全量同步persist持久化。
举例,如更新topic配置,只变更一个topic ,就需要全量持久化整个topics.json(所有topic配置)。
如果topic数量巨多,内存和io压力就大。
5.2版本基于rocksdb存储可以改善这个问题。
不过默认broker配置还是采用老方式(default),如果要使用rocksdb,需要配置storeType=defaultRocksDB。
此外,rocksdb存储不仅仅针对config目录下的配置,还支持ConsumeQueue存储,不深入分析。
3、Controller采用sofa-jraft代替DLedger
见ISSUE-7300(RIP-67)。
之前分析过Controller的作用,就是在master-slave模式下支持自动提升slave为master,见RocketMQ5源码(二)controller模式。
controller依赖raft,在5.2中raft实现使用sofa-jraft代替了dledger。
sofa-jraft在nacos里也有用到,见Nacos源码(三)Sofa-JRaft和Nacos源码(四)1.4.1配置中心-Derby集群模式。
不过同样的,默认controller还是会采用controllerType=DLedger。
背景
要解决什么问题
在[RIP-57]中提到,RocketMQ所有消息都按照append-only的方式存储在本地磁盘的CommitLog。
虽然这种存储模式能带来很高的吞吐量,但是也面临一些问题:
- 消息仅支持固定保留时间(fileReservedTime),不支持Topic级别TTL;
- MessageStore仅支持同步读消息的api,当消息不在pagecache时(冷读),工作线程会阻塞读,然后堵住其他请求;
- topic分区与broker绑定,无法迁移,想在减少集群大小时不丢失历史数据;
总的来说,分层存储就是将消息数据按照一定条件同步到另一个地方去,
rocketmq提供接口定义(TieredFileSegment),业务方按照自己的需求实现存储接口,比如使用OSS存储。
Kafka分层存储
Kafka 高级版本也支持分层存储,只不过官方不建议用于生产环境 ,仅仅作为预览特性。
broker使用分层存储demo配置:
- remote.log.storage.system.enable:开启分层存储;
- remote.log.storage.manager.class.name:远程存储实现类;
- remote.log.storage.manager.class.path:远程存储实现类classpath;
- remote.log.metadata.manager.class.name:元数据存储实现类,内置demo实现TopicBasedRemoteLogMetadataManager;
- rsm.config.dir:远程存储相关配置,这里LocalTieredStorage将数据存储到当前文件系统/tmp/kafka-remote-storage;
ini
remote.log.storage.system.enable=true
remote.log.storage.manager.class.name=org.apache.kafka.server.log.remote.storage.LocalTieredStorage
remote.log.storage.manager.class.path=/PATH/TO/kafka-storage-x.x.x-test.jar
remote.log.metadata.manager.class.name=org.apache.kafka.server.log.remote.metadata.storage.TopicBasedRemoteLogMetadataManager
rsm.config.dir=/tmp/kafka-remote-storage
kafka开启分层存储,需要在topic纬度配置,具体参数解释见官网。
arduino
bin/kafka-topics.sh --create --topic tieredTopic \
--bootstrap-server localhost:9092 \
--config remote.storage.enable=true \
--config local.retention.ms=1000 --config retention.ms=3600000 \
--config segment.bytes=1048576 --config file.delete.delay.ms=1000
producer发送n条消息,触发segment.bytes阈值,/tmp/kafka-remote-storage中产生多个数据文件。
一、使用案例
1、Broker配置
使用分层存储,只需要更改broker配置。
ini
brokerClusterName=DefaultCluster
brokerName=broker-a
brokerId=0
deleteWhen=04
fileReservedTime=48
brokerRole=ASYNC_MASTER
flushDiskType=ASYNC_FLUSH
namesrvAddr=127.0.0.1:9876
# 主存
storePathRootDir=/data/rmq_data/broker-a
storePathCommitLog=/data/rmq_data/broker-a/commitlog
# 分层存储
messageStorePlugIn=org.apache.rocketmq.tieredstore.TieredMessageStore
tieredMetadataServiceProvider=org.apache.rocketmq.tieredstore.metadata.TieredMetadataManager
tieredBackendServiceProvider=org.apache.rocketmq.tieredstore.provider.posix.PosixFileSegment
tieredStoreFilePath=/data/rmq_data/broker-a-tieredstore
- messageStorePlugIn :开启MessageStore插件 ,TieredMessageStore是分层存储插件;(类比kafka的remote.log.storage.system.enable)
- tieredMetadataServiceProvider :分层存储-元数据存储 ,TieredMetadataStore 实现类,官方实现TieredMetadataManager ,默认将元数据存储在 {storePathRootDir}/config/tieredStoreMetadata.json;(类比kafka的remote.log.metadata.manager.class.name)
- tieredBackendServiceProvider :分层存储-消息数据存储 ,TieredFileSegment 实现类,官方实现demo -PosixFileSegment ,存储到当前文件系统的tieredStoreFilePath指定路径下;(类比kafka的remote.log.storage.manager.class.name)
使用producer发送消息,观察数据变化。
2、消息数据
消息数据如何组织在于TieredFileSegment 的实现 ,这里还是以PosixFileSegment实现为例。
在tieredStoreFilePath 路径下可以看到另一份消息数据 ,按照Cluster/Broker/Topic/queueId/数据类型/数据文件的方式存储。
数据类型有三种:
- COMMITLOG存储Message消息;
- CONSUME_QUEUE存储消费队列consumequeue;
- INDEX存储查询索引;
对于COMMIT_LOG,区别于主存:
主存将所有topic和queue的Message都混合存储在一个定长滚动的commitlog中,
而分层存储后和consumequeue一样,按照topic+queue纬度分片存储。
对于CONSUME_QUEUE,和主存目录结构类似:
对于INDEX,实现方式与前两者不同,所以在这里不能立即看到。
3、元数据
元数据 {storePathRootDir}/config/tieredStoreMetadata.json:
- xxxSegmentTable:数据文件的元数据;
- queueMetadataTable:MessageQueue的元数据;
- topicMetadataTable:Topic的元数据;
数据文件的元数据,包含比如路径、初始偏移量、各种时间、状态、大小等等。
MessageQueue的元数据,包含broker、topic、queueId、最大最小偏移量。
二、组件概览
1、MessageStore
MessageStore 是rocketmq-store模块的核心api,负责读写消息。
BrokerController#initializeMessageStore:broker启动创建MessageStore。
默认存储实现是DefaultMessageStore,但是支持MessageStoreFactory包装DefaultMessageStore。
MessageStoreFactory 支持根据messageStorePlugIn 配置创建扩展MessageStore实现(AbstractPluginMessageStore ),包装原始MessageStore。
MessageStore扩展实现都持有内层MessageStore(next) ,所以外层可以有选择性的调用底层DefaultMessageStore。
比如什么时候从底层DefaultMessageStore读消息,什么时候从当前MessageStore读消息。
2、TieredMessageStore(分层存储)
分层存储实现TieredMessageStore就是基于这种方式,包装了DefaultMessageStore。
TieredMessageStore有四个重要组件:
- TieredDispatcher :分层存储数据写入的入口;
- TieredMessageFetcher:负责处理读请求;
- TieredMetadataStore:实现元数据存储,默认实现TieredMetadataManager;
- TieredFlatFileManager :负责实际消息数据存储;
3、TieredDispatcher(写)
分层存储的写入,不是由文件过期清理触发,也不是由MessageStore写消息直接触发。
TieredDispatcher 实现CommitLogDispatcher。
CommitLogDispatcher 的作用是,在消息写入后,由DefaultMessageStore的reput线程触发回调。
(见DefaultMessageStore.ReputMessageService#doReput)
DefaultMessageStore在构造时会加入几个Dispatcher,
比如CommitLogDispatcherBuildConsumeQueue 用于构造consumequeue,只有消息写入consumequeue才对消费者可见。
broker消息接收线程,收到消息后,写commitlog(pagecache、刷盘、ha复制);
DefaultMessageStore#doDispatch:
reput线程(ReputMessageService),感知到commitlog的物理offset变大。
封装一个DispatchRequest,循环所有CommitLogDispatcher执行dispatch。
由于TieredMessageStore在构造时将TieredDispatcher注入了DefaultMessageStore,所以会收到消息写入回调。
4、TieredFlatFileManager(实际数据)
TieredFlatFileManager 管理所有实际消息数据。
分层存储基于queue纬度管理消息数据 ,每个MessageQueue 对应一个CompositeQueueFlatFile。
MessageQueue还是common包下的,broker+topic+queueId。
CompositeQueueFlatFile包含:队列元数据和三个数据服务。
IndexStoreService作为Index存储服务是全局单例,与commitlog和consumequeue不同。这个后面单独看。
无论是commitlog还是consumequeue,最终都会由一个TieredFlatFile管理实际存储。
TieredFlatFile 管理n个TieredFileSegment存储数据文件。
而TieredFileSegment正是用户需要实现的存储实现,比如demo的PosixFileSegment存储在本地文件系统。
所以分层存储的数据文件组织结构大概就是这样。
TieredFlatFile 类似于主存中的MappedFileQueue,用于管理n个定长文件,对上层屏蔽操作多个文件的细节。
TieredCommitLog 类似于主存中的CommitLog ,暴露commitlog相关api,用TieredFlatFile做底层存储。
5、TieredMessageFetcher(读)
TieredMessageFetcher负责处理读请求。
并非单纯调用底层存储TieredFlatFileManager ,还有一层所谓readAheadCache本地缓存。
6、TieredMetadataManager(元数据)
TieredMetadataManager是默认的元数据管理实现。
继承ConfigManager ,所以会将数据都写入主存 {storePathRootDir}/config/tieredStoreMetadata.json。
元数据主要用于描述两类数据:
- 分层存储需要用到的queue和topic的额外属性,比如topic级别的保留时间(虽然现在没有实现);
- TieredFileSegment的路径和元数据;
TieredFlatFileManager#recoverTieredFlatFile:
元数据的作用很多,以启动为例。
通过元数据服务能够迭代所有topic下的queue,重新恢复所有queue对应CompositeQueueFlatFile。
三、读-拉消息
1、Default or Tiered
TieredMessageStore#getMessageAsync:存储层,拉消息方法入口,决定是否走分层存储。
- 如果是系统topic走主存,比如:pop消费产生的reviveTopic消息、定时topic消息等等;
- fetchFromCurrentStore根据配置项tieredStorageLevel决定是否走主存;
- 查询分层存储,如果分层存储文件不存在 或查询异常 ,降级走主存;
TieredMessageStore#fetchFromCurrentStore:
如果tieredStorageLevel=FORCE,强制走分层,这可以方便debug;
如果tieredStorageLevel=DISABLE,强制走主存;
如果queue对应分层存储文件不存在,或查询offset超出分层consumequeue的写入进度(分层存储写入延迟) ,走主存。
在分层存储存在的前提下,根据level和主存的关系决定。
level=NOT_IN_DISK(默认),offset不在主存磁盘上,走分层存储;
level=NOT_IN_MEM,offset不在主存pagecache里(内存),走分层存储;
其他走主存。
2、读缓存
TieredMessageFetcher会用caffeine维护一份缓存:
- 写后10s过期;
- 基于内存考虑的容量限制,最大容量=0.3(readAheadCacheSizeThresholdRate)*最大堆内存;
- recordStats开启metrics统计;
缓存key,MessageCacheKey=分层存储文件(broker+topic+queueId)+offset。
缓存value,即查询出来的单条消息。
TieredMessageFetcher#getMessageAsync:根据队列定位到分层存储文件,进入分层存储查询逻辑。
TieredMessageFetcher读逻辑和Cache Aside类似。
只不过因为拉消息一般是连续有序 的,即基于offset递增,可以引入prefetch预读。
比如客户端基于queueOffset=100,maxCount=32拉消息,当缓存命中时可以继续向后再拉n条。
TieredMessageFetcher#getMessageFromCacheAsync:
Step1,优先从cache中获取n条消息。
TieredMessageFetcher#getMessageFromCacheAsync:
Step2,如果cache miss(list is empty) ,一条消息都没命中,且预读线程正在处理这个offset(future.isDone=false) 。
等预读结束后,再次发起getMessageFromCacheAsync流程。
TieredMessageFetcher#getMessageFromCacheAsync:
Step3,针对Step2预读完成(future.isDone)的情况,再次尝试从缓存中读取。
TieredMessageFetcher#getMessageFromCacheAsync:
Step4,针对缓存命中的情况(读到一条就算命中),组装结果返回。
此外,如果这批消息最后一条之后还有未读消息 (lastGetOffset < result.getMaxOffset),发起预读。
TieredMessageFetcher#getMessageFromCacheAsync:
Step5,针对缓存未命中的情况。
基于2倍(readAheadMinFactor)maxCount(客户端批量拉消息条数,比如32)数量,
调用底层分层存储查询消息,最终将数据放入cache。
因为基于2倍数量拉消息,也算一次小预读,所以放入一个future。
(见Step2,遇到future未完成,可以等future完成后再次读可以命中预读到的缓存)
3、读Tiered Store
TieredMessageFetcher#getMessageFromTieredStoreAsync:
- 读consumequeue n条;
- 根据consumequeue批量读commitlog n条;
- 结合consumequeue和commitlog,组装查询结果n条;
读consumequeue
再回顾一下consumequeue记录的格式。
每条consumequeue记录对应一条commitlog消息 ,定长20byte:
- 8byte代表commitlog的物理offset;
- 4byte代表消息长度;
- 8byte一般情况下是tag的hashcode,特殊情况如定时消息是目标投递时间;
通过查询逻辑offset 乘以定长20,可以轻松找到一条consumequeue记录。
CompositeFlatFile#getConsumeQueueAsync:
读取位置offset=逻辑offset乘以20,
length读取长度=数量乘以20。
TieredConsumeQueue#readAsync:consumequeue操作底层TieredFlatFile读数据。
这个后面看,是统一存储的api。
读commitlog
和传统拉消息的重要不同点在于。
传统主存 只有consumequeue是按照queue分片存储的,commitlog是所有topic和queue混合存储的一个逻辑文件,拉消息必须循环offset,一个一个从commitlog读消息。
分层存储的消息数据可以按照queue分片存储(和kafka那种一样,当然取决于用户Segement实现),即根据队列offset拉取n条消息,可以批量从commitlog读。
假设topic=A,queueId=1,queueOffset =100,batchSize=2拉取两条消息。
传统拉消息,读consumequeue可以读连续40byte,但是读commitlog只能分两次随机读。
分层存储拉消息,读commitlog可以批量读,因为commitlog也可以和consumequeue一样,在queue纬度分片存储。
所以读取commitlog的入参和读取consumequeue的入参逻辑一致,读取位置=第一个物理offset ,读取长度=所有消息长度和(即最后一个物理offset-第一个物理offset+最后一条消息长度) 。
TieredMessageFetcher#getMessageFromTieredStoreAsync:读commitlog。
计算逻辑和上面描述一致,除此之外,批量读commitlog还做了内存控制。
截断读取长度,不能超过readAheadMessageSizeThreshold(128M),防止oom,但至少读取一条消息。
CompositeFlatFile#getCommitLogAsync:和consumequeue完全一致,读底层TieredFlatFile。
组装结果
TieredMessageFetcher#getMessageFromTieredStoreAsync:
根据consumequeue和commitlog两段buffer,可以组装结果集,这个容易理解,忽略了。
4、读TieredFlatFile
和主存MappedFileQueue类似。
TieredFlatFile#readAsync:
- getSegmentIndexByOffset,根据offset找TieredFileSegment ,采用二分;
- TieredFileSegment#readAsync,传入offset 和读取长度;
比如offset=67109000,length=20。
Step1,根据offset=67109000,二分定位到segement[1],
Step2,TieredFileSegment#readAsync,传入offset=67109000-67108864=136,length=20。
TieredFileSegment#readAsync:确保offset和length不溢出,调用底层read0。
PosixFileSegment#read0:
官方案例PosixFileSegment,从本地文件读取position位置开始length长度buffer。
注意,这里没有用独立线程,所以处理线程任然是broker拉消息请求线程。
5、prefetch预读
TieredMessageFetcher#getMessageFromCacheAsync:
对于caffeine缓存命中的情况,会发起prefetch预读。
TieredMessageFetcher#prefetchMessage:触发预读的限制条件包括:
- 拉消息数量超过1条;
- readAheadFactor大于1,这个值在初始化为2后,会在CompositeQueueFlatFile纬度动态调整;
- inflightRequest isAllDone,代表消费组的上一次预读已经完成(或者预读offset->offset+size区间无重叠,或者未发生预读,具体见getInflightRequest,不赘述);
TieredMessageFetcher#prefetchMessage:
调整readAheadFactor,
如果缓存miss,readAheadFactor-1,减少预读数量,从当前请求offset开始,
如果缓存hit,readAheadFactor+1,增加预读数量,从上一次预读offset之后开始。
TieredMessageFetcher#prefetchMessage:
最终根据各种配置参数,决定分x批,每批拉y条消息,放入缓存。
TieredMessageFetcher#prefetchMessageThenPutToCache:和普通拉消息一致。
需要注意,预读线程从头到尾都会使用处理拉消息请求线程。
底层TieredFileSegment的read0必须切换为自己的io线程组,否则一旦进入分层存储,触发预读将导致拉消息延迟。
(案例中的PosixFileSegment的read0未切换线程不可取)
四、写
1、概览
TieredStore写大致由4组线程协作处理。
Step1,实时转储commitlog:
写消息请求处理线程将commitlog写入后,触发reput线程dispatch逻辑。
TieredDispatcher尝试根据offset读取主存commitlog并写入分层存储,写入成功后将DispatchRequest缓存到一个DispatchRequestWriteMap。
Step2,定时转储commitlog:
TieredDispatcher后台会定时扫描所有TieredFlatFile,逻辑同Step1。
Step3,转储ConsumeQueue和Index:
每隔1秒将writeMap倒入readMap,TieredDispatcher单线程消费readMap,构造consumequeue和index。
2、实时转储commitlog
主存commitlog、consumequeue、index处理完毕后,进入分层存储逻辑。
TieredDispatcher#dispatch:reput线程。
写commitlog也和读一样会过滤系统topic,忽略。
getOrCreateFlatFileIfAbsent ,reput往往是创建分层存储FlatFile的入口 ,同时底层也会将新创建的FlatFile元数据写入tieredStoreMetadata.json;
detectFallBehind ,如果readMap/writeMap堆积超过1w条 ,即consumequeue构建慢而发生堆积,直接返回,不构建commitlog,由TieredDispatchExecutor后台线程定时处理;
request.offset==file.dispatchOffset(commitlog写入进度) ,在上面detectFallBehind 这个case处理完毕后,分层存储构建追上消息发送速度 ,可以由reput线程准实时写入二级存储commitlog。
TieredDispatcher#dispatch:准实时写入commitlog。
在compositeFlatFileLock的保护下,
查询commitlog并写入分层存储(逻辑后面和consumequeue一起看),最后放入writeMap,供TieredDispatcher线程消费。
3、定时转储commitlog
readMap/writeMap来不及消费构建分层consumequeue,所以分层commitlog写再快也没意义,还会加重read/writeMap堆积占用内存。
reput线程为了不阻塞主流程(写主存consumequeue),选择不写分层commitlog,由后台线程定时扫描所有分层存储文件FlatFile,定时转储commitlog。
TieredDispatcher#initScheduleTask:
TieredDispatcher启动后,开启定时任务,每隔10s扫描所有FlatFile。
TieredDispatcher#dispatchFlatFileAsync:每个FlatFile提交一个任务到TieredDispatchExecutor线程组。
TieredDispatcher#dispatchFlatFile:同样,detectFallBehind如果分层consumequeue构建太慢,不做处理。
TieredDispatcher#dispatchFlatFile:
从dispatchOffset(commitlog写入进度)开始持续写入commitlog,一次最多处理2500条消息(tieredStoreGroupCommitCount)。
后续逻辑和实时写逻辑一致,不再赘述。
4、定时转储consumequeue
TieredDispatcher 还是个线程(ServiceThread),用于构建分层存储的consumequeue和index。
实时/定时分层commitlog写入成功后,对应FlatFile会生成新的DispatchRequest,放入writeMap。
TieredDispatcher#run:每隔1s跑一次。
TieredDispatcher#buildConsumeQueueAndIndexFile:
- swapDispatchRequestList:将writeMap灌入readMap,单线程消费;
- copySurvivorObject:消费完,剩余readMap再灌入writeMap,等下一批处理;
TieredDispatcher#buildConsumeQueueAndIndexFile:
遍历FlatFile,遍历DispatchRequest,写入consumequeue和index。
5、FlatFile写入
commitlog/consumequeue
无论是appendCommitLog还是appendConsumeQueue,其底层都是一样的,只是在commitlog和consumequeue模型上还有一些额外操作。
CompositeFlatFile#appendCommitLog:写commitlog会增加dispatchOffset。
TieredCommitLog#append:commitlog从buffer中可以读到message的写入时间。
CompositeFlatFile#appendConsumeQueue:写consumequeue,校验一下请求offset与offset写入进度一致。
TieredConsumeQueue#append:
将DispachRequest中的属性按照consumequeue格式(20byte)写入buffer,调用底层append。
写入主流程
从整体上来看,无论是commitlog还是consumequeue,都是先写内存buffer,由后台commit线程批量刷盘。
从每个queue的FlatFile纬度来看,要先找到最后一个写入的segment,append先追加写入segment的内存buffer。后台线程扫描这些待刷盘的buffer,执行批量刷盘。
TieredFileSegment
TieredFileSegment辅助用户实现Segment:
- 线程安全处理;
- segement元数据维护;
- 维护写buffer;
用户只需要实现read0、write0对接不同的三方api即可。
java
public abstract class TieredFileSegment implements Comparable<TieredFileSegment>, TieredStoreProvider {
// 文件路径 broker/topic/queue
protected final String filePath;
// 当前segment的起始offset
protected final long baseOffset;
// commitlog/consumequeue/index
protected final FileSegmentType fileType;
protected final TieredMessageStoreConfig storeConfig;
// segment最大容量
private final long maxSize;
private final ReentrantLock bufferLock = new ReentrantLock();
private final Semaphore commitLock = new Semaphore(1);
// segment是否写满
private volatile boolean full = false;
private volatile boolean closed = false;
// segment中最小的存储时间
private volatile long minTimestamp = Long.MAX_VALUE;
// segment中最大的存储时间
private volatile long maxTimestamp = Long.MAX_VALUE;
// 刷盘进度
private volatile long commitPosition = 0L;
// append进度
private volatile long appendPosition = 0L;
// commitlog写入物理offset对应逻辑offset
private volatile long dispatchCommitOffset = 0L;
// 用于commitlog,标识一个segment的结束
private ByteBuffer codaBuffer;
// 待刷盘buffer
private List<ByteBuffer> bufferList = new ArrayList<>();
private FileSegmentInputStream fileSegmentInputStream;
// 刷盘future
private CompletableFuture<Boolean> flightCommitRequest = CompletableFuture.completedFuture(false);
}
文件大小。
对于不同的文件类型,有不同的文件容量控制。默认commitlog-1g,consumequeue-100m,index-无限。
baseOffset。
一个FlatFile中有多个segement,baseOffset是当前segment在整个FlatFile中的物理偏移量。
position。
segment一般情况下不是同步刷盘(调用用户实现commit0),有两个position状态需要维护。
appendPosition是写入内存进度,commitPosition是刷盘进度。
bufferList。
写内存就是将buffer加入bufferList链表。
append
TieredFlatFile#append:
- getFileToWrite,找到写入segment;
- segment#append,写入buffer;
- 如果buffer满了(BUFFER_FULL),且需要commit(只有定时转储commitlog需要commit),同步等待一次commit,再次append;
- 如果文件写满了,getFileToWrite执行滚动,append到下一个segment;
找可写入segment
TieredFlatFile#getFileToWrite:找到可写入的segment。
Case1,返回最后一个segment。(通过元数据,启动阶段就可以加载所有segment)
Case2,最后一个segment写满了,需要同步commit刷盘并更新segment元数据,创建新segment;
Case3,segment不存在,即queue还没有消息,直接创建新的segment;
所有新创建的segment都加入待刷盘segment列表。
segment append
TieredFileSegment#append:
- 如果文件满了,即appendPosition+buffer大小超出maxSize,返回FILE_FULL,由外部FlatFile处理滚动;
- 如果buffer过大 (bufferList.size超过2500,待刷盘buffer超出32MB),即还未来得及定时刷盘,这里触发一次异步刷盘;
- bufferList.size超出1w,流控,不可append,返回BUFFER_FULL;
TieredFileSegment#append:正常情况,更新时间戳信息,增加append进度,复制buffer加入bufferList。
commit
TieredFlatFileManager#doScheduleTask:每隔60s,common线程组调度一次刷盘任务。
此时才能真正将数据从内存buffer写入外部存储,所以从写入数据到从外部存储看到数据至少要经过60s。
TieredFlatFileManager#doCommit:common线程组遍历所有FlatFile,每个FlatFile提交commitlog和consumequeue刷盘任务到commit线程组。
TieredFlatFile#commit:无论是commitlog还是consumequeue,底层都走FlatFile刷盘。
遍历所有待刷盘segment,执行commit,最后更新segment的元数据。
TieredFileSegment#commitAsync:
Step1,将bufferList转换为一个InputStream,对用户屏蔽多个buffer的处理。
TieredFileSegment#commitAsync:
Step2,调用用户实现commit0,传入InputStream,当前刷盘进度,buffer总长度;
Step3,处理用户commit0结果,如果刷盘成功,更新commit刷盘位点,如果刷盘失败,回滚InputStream;
Step4,commit0抛出异常,走异常处理;(比如调用oss服务异常怎么处理)。
PosixFileSegment#commit0:官方Segment案例,commit0写入本地磁盘,注意使用了独立线程池。
TieredFileSegment#handleCommitException:
如果commit0发生异常,需要确定本次commit到底最终是否成功,比如网络超时等情况,怎么处理。
本质上,这里的实现是根据当前segment文件大小来判断的。
TieredFileSegment#getCorrectFileSize:用户有两种方式告诉上层到底刷盘进度是多少。
如果commit0抛出TieredStoreException,从异常返回position获取文件大小;
否则走子类实现getSize获取文件大小。
TieredFileSegment#correctPosition:无论如何,最终刷盘进度都会设置为文件大小。
只不过,如果刷盘进度+stream中buffer长度=文件大小,认为刷盘成功,返回true,清空stream;
否则,认为刷盘失败,返回false,回滚stream;
总得来说,要确保刷盘位点正确,完全依赖于文件大小,其实未必可靠。
五、分层存储过期清理
1、定时清理
TieredFlatFileManager#doScheduleTask:
每隔30s,common线程组调度发起分层存储过期文件清理。
TieredFlatFileManager#doCleanExpiredFile:
保留时间默认72小时(tieredStoreFileReservedTime) ,
每个FlatFile提交到TieredCleanFileExecutor线程池处理。
TieredFlatFileManager#doCleanExpiredFile:
清理分为两步,cleanExpiredFile 先更新过期segment元数据,destroyExpiredFile删除过期segment。
CompositeFlatFile#cleanExpiredFile/destroyExpiredFile:
每步又分成两步,先处理commitlog再处理consumequeue。
2、元数据
commitlog和consumequeue更新元数据逻辑基本一致。
TieredFlatFile#cleanExpiredFile:
Step1,循环所有segment的元数据,根据segment中的最大存储时间来判断是否过期,将过期segment的baseOffset加入集合。
TieredFlatFile#cleanExpiredFile:
Step2,将所有过期segment的元数据标记为closed并持久化。
TieredFlatFile#recoverMetadata:
一旦元数据被标记为closed,即status=STATUS_DELETED(2),就无法在重启后恢复,即使未真实删除segment。
3、数据
TieredFlatFile#destroyExpiredFile:commitlog和consumequeue都一样,迭代所有被标记为DELETED的segment元数据,执行用户实现的destroyFile方法,如果删除成功,删除对应元数据。
TieredCommitLog#destroyExpiredFile:对于commitlog还有一个滚动逻辑。
如果当前写入的segment的最大写入时间超过24h (commitLogRollingInterval),且文件大小超过128MB(commitLogRollingMinimumSize),执行滚动生成新的segment。
所以commitlog在分层存储中不是定长1G文件。
六、Index
1、回顾主存index
Index提供根据时间、消息key、消息id查询消息的能力。
每个Index文件都是一个定长文件 ,以时间戳命名。
不会按照queue分片存储,所有topic-queue的数据存储在一个索引文件中,和commitlog一样。
Index文件分为三部分:
- Header:文件头,用于描述这个Index文件的元数据信息,包含:1-消息存储时间范围,2-消息commitlog的物理offset范围,3-slot数量,4-index数量;
- Slot区域:一个HashTable,每个节点占用4字节,存储索引头节点位置,可以找到一个IndexItem;
- Index区域 :实际索引数据,每个IndexItem包含20字节 ,hash-消息key的哈希 ,物理offset-定位commitlog消息 ,timeDiff-与当前索引文件第一个消息存储时间的距离,slotValue-指向下一个IndexItem;
索引写入,采用头插法,可顺序写(org.apache.rocketmq.store.index.IndexFile#putKey):
- hash(key)%500w定位Slot,得到Slot.slotValue=x;
- 基于当前Index区域写入位置,追加写入新IndexItem,新IndexItem.slotValue指向之前的头节点x;
- 更新Slot.slotValue=新IndexItem位置;
索引查询,需要随机读(DefaultMessageStore#queryMessage):
- hash(key)%500w定位Slot,得到Slot.slotValue=x;
- 根据slotValue找到索引IndexItem位置,即header大小+slot区域大小+slotValue乘以20;
- 根据IndexItem中的slotValue重复第二步(随机读);
- 根据2和3拿到所有的物理offset,查询消息;
ISSUE-7545,对于分层存储,在发生hash冲突的情况下(其实根据业务key查询,比如订单号,其实很容易发生冲突),随机读会引发大量io(比如调用oss),响应时间不能保证。
所以5.2分层存储会以新的格式存储Index文件 ,目的就是为了让同一个slot的索引项IndexItem连续存储,让一个slot的多次io变成一次io。
2、概览
IndexFile
对于分层存储,IndexFile有三种状态:
- UNSEALED :初始状态,类似主存索引文件格式(顺序写),存储在本地磁盘上,路径为{storePath}/tiered_index_file/{时间戳}:
- SEALED :本地Index文件写满,正在或已经生成压缩格式Index文件,还未上传到外部存储,路径为{storePath}/tiered_index_file/compacting/{时间戳}:
- UPLOAD :已经上传到外部存储,即用户实现的Segment;
总的来说,在分层存储中,Index文件会存在于3个地方,对应3种状态。
Index会在普通index文件写满之后,先压缩,再写入segment,这与commitlog和consumequeue都不同(这两个是近实时写入)。
IndexStoreFile是一个IndexFile的具体实现,在不同状态下维护了不同的文件形式。
重启恢复时(见IndexStoreService#recover):
tiered_index_file路径下的文件,都会以初始index文件方式加载,体现为一个MappedFile(主存的定长文件实现类);
tiered_index_file/compacting路径下的文件,会被删除;
segment外部存储,可以通过元数据恢复。
IndexStoreService
IndexStoreService管理多个索引文件。
TieredFlatFileManager#getTieredIndexService:IndexStoreService是个单例对象。
在构造时FlatFile的path固定为broker/rmq_sys_INDEX/0 ,即index在外部存储中不会按照topic-queue分片,不同的segment只有baseOffset的区别。
IndexItem
IndexItem索引项,追加写格式32byte(UNSEALED ),压缩格式28byte(SEALED 、UPLOAD)。
压缩格式比追加写格式少了itemIndex,即主存slotValue。
IndexItem除了主存索引项的4个属性之外(20byte),还额外包含了3个属性(12byte):
- topicId:在分层存储下,broker为每个topic被分配了一个唯一id ,顺序递增,存储在元数据文件中,topicMetadataTable;
- queueId:即topic下的queueId,也存储在元数据文件中,queueMetadataTable;
- size:消息长度;
3、写
TieredDispatcher#buildConsumeQueueAndIndexFile:
TieredDispatcher单线程消费writeMap,先写consumequeue,然后写index。
CompositeQueueFlatFile#appendIndexFile:解析DispatchRequest,调用IndexStoreService。
IndexStoreService#putKey:
调用当前IndexFile写入,如果文件写满,滚动到新的IndexFile。
IndexStoreFile#putKey:
当前写入IndexFile一定是处于UNSEALED状态,未压缩在本地磁盘。
如果索引数量超出最大容量(和主存配置一致2kw),状态变更为SEALED等待压缩上传,返回文件已满,让外部IndexStoreService创建新的UNSEALED状态IndexFile。
IndexStoreFile#putKey:循环每个key,写入索引。
逻辑和主存完全一致 ,只是index和header区域的模型有变化。
header:去除了offset区间,加了个魔数;
index:加了topicId、queueId和size;
4、过期清理/压缩/上传
IndexStoreService还是一个线程,用于处理Index文件的生命周期。
IndexStoreService#run:
- 处理过期segment,默认72h,和commitlog/consumequeue一致,先标记元数据过期,再调用用户segment#destroyFile;
- 找到下一个待压缩文件;
- 压缩上传;
- 更新压缩时间戳;
IndexStoreService#getNextSealedFile:
一般情况下,当本地磁盘tiered_index_file下存在两个及以上UNSEALED文件时,返回第一个文件执行压缩上传。
所以当IndexFile写满发生滚动,才会触发压缩上传。
每次压缩上传成功,compactTimestamp会更新为上一次处理的IndexFile。
重启后,tiered_index_file下的第一个文件时间戳-1会作为compactTimestamp重新加载到内存。
IndexStoreService#doCompactThenUploadFile:大体分为三块
- 压缩原始IndexFile;
- 调用用户segment#commit0将压缩buffer整体写入外部存储;
- 切换table中的IndexFile为已上传的IndexFile ,删除本地IndexFile;
IndexStoreFile#doCompaction:重点在于index文件压缩,压缩的格式决定了后期查询分层存储的io次数。
IndexStoreFile#compactToNewFile:循环处理所有slot,最后写入header。
压缩文件和原始文件相比:
- header未变化;
- 每个slot从原来的4byte扩大为8byte;
- index区域只保留实际索引数量,且索引项从32变为28byte。
IndexStoreFile#compactToNewFile:对于每个slot
- 迭代slot中所有IndexItem ,顺序写入buffer,丢弃每个Item中最后4byte不需要的slotValue;
- 写入slot的8byte,前4byte还是和原来一样是第一个IndexItem的位置,后4byte是连续IndexItem的长度;
可以想象,通过这种压缩存储方式,查询时就能批量读取一个slot中的连续IndexItem,从而能避免多次io。
5、读
TieredMessageStore#queryMessageAsync:判断是否走分层存储查询
- 在force级别下,强制走分层存储查询;
- 非force级别下,比较主存首条消息时间和查询区间,分别走主存和分层存储查询,合并后返回;
- 主存查询本质是同步api,分层存储查询可异步;
TieredMessageFetcher#queryMessageAsync:
Step1,根据查询条件得到所有索引项;
Step2,循环索引项,找到queue对应FlatFile;
Step3,根据索引项的offset和size,读commitlog的segment,得到消息;(如果底层segment#read0采用独立线程池,这边result.addMessage有线程安全问题,我提了ISSUE-7872应该可以修复)
IndexStoreService#queryAsync:根据时间区间范围找到所有IndexFile,执行每个IndexFile查询。
IndexStoreFile#queryAsync:每个IndexFile根据当前状态,选择从不同的文件中读数据。
UPLOAD ,已上传,走segment查询;
SEALED/UNSEALED ,未上传,都走未压缩的tiered_index_file下的文件查询,这个逻辑和主存index类似不用看,就是需要随机读;
IndexStoreFile#queryAsyncFromSegmentFile:已上传IndexFile,调用用户实现segment读取。
- 第一次io,hash(key)%500w定位slotPosition读取slotBuffer(8byte) ;
- 第二次io,根据slotBuffer读取连续的IndexBuffer,slotBuffer前4byte是IndexBuffer的开始位置,后4byte是IndexBuffer的长度;
- 最后反序列化IndexBuffer,匹配hash和时间,返回;
七、实现一个Segment
借助阿里云oss简单实现一个segment,只是作为补充案例,不要直接用于生产。
注:其实这里如何实现方式很多,根据topic使用不同实现也可以,只需要用户实现segment做一层代理即可。
xml
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.10.2</version>
</dependency>
1、实现要点
第一点,segment的读写要用独立线程池。
以拉消息为例,在5.xtiered store出现之后,冷读的一种处理方式是提供一套异步api,避免阻塞业务线程。
比如commitlog不在pagecache中,那就需要读本地磁盘产生io操作,这会阻塞业务线程。
如果segment在实现上使用独立线程池,那么pull消息线程就不会被冷读阻塞。
pull消息业务线程只做内存操作,比如各种校验计算、读本地缓存、读pagecache等等。
如果需要segment读数据,只要提交到segment线程即可,由segment线程调用存储得到buffer,再提交给netty的io线程。
除此之外,如果segment不采用异步读,prefetch预读触发就会阻塞本次拉消息请求,那预读也没意义了。
第二点,异常处理。
读发生异常,其实不影响,无非是拉不到消息。
而写发生异常,需要纠正segment元数据的size,即文件到底写到了什么position。
比如oss写超时,客户端设置position。
从底层实现上看,如果发生异常,根据当前segment文件大小决定position。
而发生异常时,获取文件大小有两种方式,上面提到了:
1、优先采用通过手动抛出TieredStoreException指定的position;
2、调用segment#getSize获取;
这里我没仔细处理。
2、构造Segment
AliyunFileSegment,继承TieredFileSegment,用于接入分层存储。
- fullPath,代表oss中的key(理解为文件存储路径)。这里我没有将不同类型的文件区分路径(见官方PosixFileSegment),而是用文件类型+offset作为文件名。这里的含义是,路径如何组织可以由实现者随便定义 ,segment元数据在启动后会重新调用TieredFileSegment构造,只要保证同样的元数据对应同一份文件即可;
- client,用于调用三方api,一般情况下都可以做单例;
scala
public class AliyunFileSegment extends TieredFileSegment {
private static final Logger LOGGER = LoggerFactory.getLogger(TieredStoreUtil.TIERED_STORE_LOGGER_NAME);
private final Client client;
private final String fullPath;
public AliyunFileSegment(TieredMessageStoreConfig storeConfig,
FileSegmentType fileType, String filePath, long baseOffset) {
super(storeConfig, fileType, filePath, baseOffset);
client = Client.getInstance(storeConfig); // 单例client
String dir = "rmq_data/" + storeConfig.getBrokerClusterName() + "/" + filePath;
String name = fileType.toString() + "_" + TieredStoreUtil.offset2FileName(baseOffset);
// rmq_data/cluster/broker/topic/queue/file_type_offset
this.fullPath = dir + "/" + name;
}
}
Client,调用阿里云oss的二次封装。
- 采用独立线程池asyncExecutor负责调用三方api;
- 读取TieredMessageStoreConfig中的配置项,构造三方sdk对象com.aliyun.oss.OSS;
arduino
private static class Client {
private static final Logger LOGGER = LoggerFactory.getLogger(TieredStoreUtil.TIERED_STORE_LOGGER_NAME);
private static volatile Client client = null;
private final OSS oss;
private final String bucket;
private final ExecutorService asyncExecutor = Executors.newFixedThreadPool(8);
private Client(TieredMessageStoreConfig config) {
this.oss = new OSSClientBuilder()
.build(config.getObjectStoreEndpoint(), config.getObjectStoreAccessKey(), config.getObjectStoreSecretKey());
this.bucket = config.getObjectStoreBucket();
}
private static Client getInstance(TieredMessageStoreConfig config) {
if (client == null) {
synchronized (Client.class) {
if (client == null) {
client = new Client(config);
}
}
}
return client;
}
}
3、实现api
因为rocketmq在框架层已经维护了元数据、内存buffer、批量commit等逻辑,
在实现TieredFileSegment的时候,只需要对接三方api即可。
AliyunFileSegment这里直接调用client。
typescript
@Override
public String getPath() {
return this.fullPath;
}
@Override
public long getSize() {
try {
ObjectMetadata metadata = client.getMetadata(this.fullPath);
return metadata.getContentLength();
} catch (Exception e) {
return 0;
}
}
@Override
public boolean exists() {
return true;
}
@Override
public void createFile() {
}
@Override
public void destroyFile() {
try {
client.delete(this.fullPath);
} catch (Exception e) {
}
}
@Override
public CompletableFuture<ByteBuffer> read0(long position, int length) {
return client.getAsync(this.fullPath, position, length);
}
@Override
public CompletableFuture<Boolean> commit0(FileSegmentInputStream inputStream, long position, int length,
boolean append) {
return client.appendAsync(fullPath, position, inputStream).thenApply(none -> true);
}
AliyunFileSegment.Client#appendAsync:写。
采用独立线程池asyncExecutor。
构造AppendObjectRequest,设置objectKey=path,传入InputStream(记得底层是TieredFileSegment的bufferList)。
arduino
public CompletableFuture<Void> appendAsync(String path, long position, InputStream stream) {
return CompletableFuture.runAsync(() -> {
try {
this.append(path, position, stream);
} catch (IOException e) {
throw new RuntimeException(e);
}
}, this.asyncExecutor);
}
private void append(String path, long position, InputStream inputStream) throws IOException {
AppendObjectRequest appendObjectRequest = new AppendObjectRequest(bucket, path, inputStream);
appendObjectRequest.setPosition(position);
AppendObjectResult result = oss.appendObject(appendObjectRequest);
}
AliyunFileSegment.Client#getAsync:读。
采用独立线程池asyncExecutor。
构造GetObjectRequest,设置objectKey=path,range=读buffer范围。
解析返回OSSObject,返回ByteBuffer。
csharp
public CompletableFuture<ByteBuffer> getAsync(String path, long position, int length) {
return CompletableFuture.supplyAsync(() -> {
try {
return this.getSync(path, position, length);
} catch (IOException e) {
throw new RuntimeException(e);
}
}, asyncExecutor);
}
private ByteBuffer getSync(String path, long position, int length) throws IOException {
OSSObject object = null;
try {
GetObjectRequest getObjectRequest = new GetObjectRequest(bucket, path);
getObjectRequest.setRange(position, position + length - 1);
object = oss.getObject(getObjectRequest);
byte[] byteArray = IOUtils.readStreamAsByteArray(object.getObjectContent());
ByteBuffer byteBuffer = ByteBuffer.allocate(length);
byteBuffer.put(byteArray);
byteBuffer.flip();
byteBuffer.limit(length);
return byteBuffer;
} finally {
if (object != null) {
try {
object.close();
} catch (Exception ignore) {
}
}
}
}
4、配置broker
配置segment实现类和oss客户端需要的参数信息。
ini
messageStorePlugIn=org.apache.rocketmq.tieredstore.TieredMessageStore
tieredMetadataServiceProvider=org.apache.rocketmq.tieredstore.metadata.TieredMetadataManager
tieredStoreFilePath=/data/rmq_data/broker-a-tieredstore
tieredBackendServiceProvider=org.apache.rocketmq.tieredstore.provider.aliyun.AliyunFileSegment
objectStoreAccessKey=x
objectStoreSecretKey=x
objectStoreBucket=x
objectStoreEndpoint=x
5、实现效果
一个queue纬度下是一个FlatFile。
一个FlatFile包含三类文件commitlog、consumequeue、index(需要写满本地indexfile才能上传,所以这里没看到)。
最好是将不同类型文件分目录存储,我这里是举例说明,也可以不按照这种方式实现。
元数据还是用的官方实现存储在本地config/tieredStoreMetadata.json。
在启动阶段,通过元数据可以找到oss上的文件(path+fileType+offset),
通过segment元数据中的size可以知道该segment的写入位置,后续可以继续追加写入。
总结
本文分析了rocketmq5.2的分层存储实现。
总的来说,目前rocketmq的分层存储还没有完全完善,现在也只是看着玩。
比如topic级别的ttl,实际上还没有api支持,只能手动维护元数据文件。
而且api还在不停的修改之中吧,比如5.2就对index部分做了重构。
存储概览
在分层存储出现之前,rocketmq的消息只能保存在本地磁盘,如果磁盘容量不足或到达保留时间(默认fileReservedTime=72小时),就会物理删除。
分层存储,就是将commitlog、consumequeue、index等三类文件,存储到外部设备上,比如各种OSS。
对于Tiered Store来说有两类数据。
第一类是元数据,元数据用于描述数据。
TieredMetadataStore接口描述了元数据存储需要的api,官方提供的模式实现类TieredMetadataManager将元数据都存储在config/tieredStoreMetadata.json中。
第二类是数据,包括commitlog、consumequeue、index。
TieredFlatFileManager 基于queue纬度管理n个CompositeQueueFlatFile。
每个CompositeQueueFlatFile包含commitlog、consumequeue和index。
commitlog和consumequeue 都有一个FlatFile 包含n个TieredFileSegment,一个Segment可以理解为一个外部存储中的文件。
index较为特殊,除了FlatFile之外,还可能包含两种本地index文件(tiered_index_file和compacting)。
Segment的生命周期都由rocketmq管理,默认情况下三类segment的保留时间都是72h(tieredStoreFileReservedTime)。
segment过时将调用segment#destroyFile,至于怎么删除由实现者决定。
但是在元数据上,segment会被标记为删除,重启后也不会再加载,即逻辑删除。
除此之外,commitlog还有一个滚动逻辑。
如果当前写入的segment的最大写入时间超过24h (commitLogRollingInterval),且文件大小超过128MB(commitLogRollingMinimumSize),执行滚动生成新的segment,所以commitlog在分层存储中不是一个定长文件。
(注:commitlog默认最大1G,consumequeue默认最大100MB,index没有最大限制,由本地tiered_index_file整体压缩后上传)
commitlog/consumequeue写
reput会触发分层存储写逻辑,过滤系统topic(如延迟topic、reviveTopic等),先写commitlog,再写consumequeue和index。
根据broker+topic+queue能找到queue纬度的CompositeQueueFlatFile。
无论是commitlog还是consumequeue,底层都是写FlatFile中的当前TieredFileSegment。
总体来说,segment写入分为两个阶段,先写内存buffer ,再写外部存储(调用用户实现segment#commit0)。
TieredFileSegment如果写满了(比如commitlog是1g),会滚动一个新的segment。
TieredFileSegment会将数据先写入内存bufferList。
bufferList到达阈值,,2500条buffer或buffer超过32MB,会直接触发异步刷盘。
bufferList未到达阈值,由后台线程定时刷盘(每隔60s),即外部存储能真正看见数据最少要延迟60s。
用户代码segment#commit0执行异常 ,比如发生网络超时,框架需要判断究竟写入外部存储是否成功。
目前rocketmq采用的方式是,根据segment的文件大小来判断异常之后的写入position。
如果异常类型是TieredStoreException,则取TieredStoreException.position;
其他情况,都会重新调用segment#getSize(用户实现)获取文件大小。
拉消息
拉消息会过滤系统toipic,默认(tieredStorageLevel=NOT_IN_DISK)情况下。
如果offset不在主存中,走分层存储 ,如果分层存储发生异常,再降级走主存。
分层存储拉消息逻辑和Cache Aside类似,先读本地缓存(caffeine),如果缓存未命中,再走segment#read0(用户逻辑)读。
只不过因为拉消息一般是连续有序 的,即基于offset递增,可以引入prefetch预读。
比如客户端基于queueOffset=100,maxCount=32拉消息。
当缓存命中,可以继续向后再拉n条。
当缓存未命中,放大2倍拉n条。
未命中缓存/预读,通过offset二分查找定位segment,调用segment#read0(用户实现)得到buffer。
index写
Index的写入方式与commitlog和consumequeue都不同。
分层存储consumequeue写入成功后,触发index写入。
Index文件会先以非压缩格式 ,写入本地文件系统storePath/tiered_index_file路径下。
UNSEALED,非压缩格式,索引项比主存多了topicId、queueId、size(消息大小)。
分层存储会为topic和queue记录元数据到tieredStoreMetadata.json,会为topic分配序号topicId,queueId则是本身的queueId。
按照非压缩格式存储的index文件的问题在于,假设根据业务key查询消息,业务key重复概率较高(比如同一个订单的消息)而导致哈希冲突,此时需要经过n次随机读才能完整读出一个slot下的所有索引项。
如果是在pagecache还好,但是对于外部存储来说,这种随机读就不能接受。
所以未压缩的本地index文件会在写满后,经过压缩,再上传至外部存储(segment)。
SEALED ,压缩格式,特点是同一个slot的所有索引项存放在一段连续区域中 ,而slot从原来的4byte变为8byte,指向一段连续buffer,从而能解决随机读问题。
SEALED 文件,属于一个临时文件 ,存放在tiered_index_file/compacting目录下。
UPLOAD文件,compacting本地压缩文件最终会上传至外部存储,调用用户实现的segment#commit0(整块压缩文件buffer),上传成功后会删除UNSEALED和SEALED文件。
index读
用户指定业务key+时间范围查询消息,需要先读index再读commitlog。
根据时间范围,在非强制走分层存储的情况下,可能需要读主存后再读分层存储,合并后返回。
分层存储下,会根据时间范围圈定查找的index文件。
如果index文件是SEALED或UNSEALED ,则走本地未压缩的文件查询;
如果index文件是UPLOAD ,则走外部存储查询(用户segment#read0)。
参考资料: