- 为了实现写入的高吞吐,设计类4层内存上的存储。队列缓存、瞬时存储、PageCache、磁盘
- 为了查询的效率。对写入到commitedLog文件的消息进行异步重放构建CommitLogQueue队列索引信息和属性队列信息
- 先将消息存储写入PageCache、再将消息flush到磁盘
1 写入内存
- 接受发送的消息 (SendMessageProcessor#sendMessage)
- CommitLog接受异步发送的消息,对消息体进行编码,并设置对应的信息到buffer的位上,构建内部的消息(CommitLog#asyncPutMessage)
- 获取commitedLog队列中最后一个映射文件,如果绑定的瞬时存储buffer不为空则返回写buffer ,否则直接获取对应文件的映射
- 将消息写入对应的映射文件buffer中(DefaultAppendMessageCallback#doAppend)
需要先堆消息buffer进行编码
编码的消息体,在指定的位置填充特定的信息(MessageExtEncoder#encode))
public PutMessageResult encode(MessageExtBrokerInner msgInner) {
this.byteBuf.clear();
/**
* Serialize message
*/
final byte[] propertiesData =
msgInner.getPropertiesString() == null ? null : msgInner.getPropertiesString().getBytes(MessageDecoder.CHARSET_UTF8);
final int propertiesLength = propertiesData == null ? 0 : propertiesData.length;
if (propertiesLength > Short.MAX_VALUE) {
log.warn("putMessage message properties length too long. length={}", propertiesData.length);
return new PutMessageResult(PutMessageStatus.PROPERTIES_SIZE_EXCEEDED, null);
}
final byte[] topicData = msgInner.getTopic().getBytes(MessageDecoder.CHARSET_UTF8);
final int topicLength = topicData.length;
final int bodyLength = msgInner.getBody() == null ? 0 : msgInner.getBody().length;
final int msgLen = calMsgLength(
msgInner.getVersion(), msgInner.getSysFlag(), bodyLength, topicLength, propertiesLength);
// Exceeds the maximum message body
if (bodyLength > this.maxMessageBodySize) {
CommitLog.log.warn("message body size exceeded, msg total size: " + msgLen + ", msg body size: " + bodyLength
+ ", maxMessageSize: " + this.maxMessageBodySize);
return new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, null);
}
final long queueOffset = msgInner.getQueueOffset();
// Exceeds the maximum message
if (msgLen > this.maxMessageSize) {
CommitLog.log.warn("message size exceeded, msg total size: " + msgLen + ", msg body size: " + bodyLength
+ ", maxMessageSize: " + this.maxMessageSize);
return new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, null);
}
// 1 TOTALSIZE
this.byteBuf.writeInt(msgLen);
// 2 MAGICCODE
this.byteBuf.writeInt(msgInner.getVersion().getMagicCode());
// 3 BODYCRC
this.byteBuf.writeInt(msgInner.getBodyCRC());
// 4 QUEUEID
this.byteBuf.writeInt(msgInner.getQueueId());
// 5 FLAG TODO 到该处已经占用啦前20个字节数
this.byteBuf.writeInt(msgInner.getFlag());
// 6 QUEUEOFFSET TODO 队列偏移量 8个字节
this.byteBuf.writeLong(queueOffset);
// 7 PHYSICALOFFSET, need update later TODO 消息物理偏移量 8字节 值之后进行更新
this.byteBuf.writeLong(0);
// 8 SYSFLAG TODO 系统标识 4个字节
this.byteBuf.writeInt(msgInner.getSysFlag());
// 9 BORNTIMESTAMP TODO 到达Broker的时间 占用 8字节
this.byteBuf.writeLong(msgInner.getBornTimestamp());
// 10 BORNHOST TODO broker的host地址 长度按照之后的长度进行更新
ByteBuffer bornHostBytes = msgInner.getBornHostBytes();
this.byteBuf.writeBytes(bornHostBytes.array());
// 11 STORETIMESTAMP // TODO 保存时间 占用8字节
this.byteBuf.writeLong(msgInner.getStoreTimestamp());
// 12 STOREHOSTADDRESS // TODO 存储保存的host地址进行保存
ByteBuffer storeHostBytes = msgInner.getStoreHostBytes();
this.byteBuf.writeBytes(storeHostBytes.array());
// 13 RECONSUMETIMES
this.byteBuf.writeInt(msgInner.getReconsumeTimes());
// 14 Prepared Transaction Offset
this.byteBuf.writeLong(msgInner.getPreparedTransactionOffset());
// 15 BODY
this.byteBuf.writeInt(bodyLength);
if (bodyLength > 0)
this.byteBuf.writeBytes(msgInner.getBody());
// 16 TOPIC
if (MessageVersion.MESSAGE_VERSION_V2.equals(msgInner.getVersion())) {
this.byteBuf.writeShort((short) topicLength);
} else {
this.byteBuf.writeByte((byte) topicLength);
}
this.byteBuf.writeBytes(topicData);
// 17 PROPERTIES properties
this.byteBuf.writeShort((short) propertiesLength);
if (propertiesLength > 0)
this.byteBuf.writeBytes(propertiesData);
return null;
}
获取内部真正的映射文件,是瞬时存储中的buffer或者直接对文件建立映射的buffer
然后对映射文件进行拼接(在映射内存中的操作)
对消息buffer的某些信息进行啦重写修改,然后提交到文件映射的内存中
写入内存,调整啦 DefaultMappedFile#wrotePosition的写入游标
2 内存 写入到PageCache
commit线程开启一个while循环(CommitLog.CommitRealTimeService#run)
从配置中获取commit的时间间隔、最少commit的页数等配置
MappedFileQueue#commit
-
依据commited的位置获取需要commit的映射内存MappedFile
-
commit内存中的消息数据到 PageCache
-
计算最新的commitedWhere 游标 并设置
DefaultMappedFile#commit
-
针对没有开启瞬时存储的文件,无需进行commit,仅仅跟进wrotePosition和committedPosition一直就可以,将wrotePosition当作commitedPosistion进行返回
- 判断是否需要commit,需要的话进行commit
- 如果开启啦瞬时存储,则需要在commit之后重置buffer,归还buffer到瞬时存储池
判断是否需要commit
- 映射文件的内存写满啦需要commit
- 需要提交的commit页数大于配置的阀值需要commit
- 在需要配置的阀值小于等于0,的时候wirte>commit就需要进行commit
将瞬时缓存刷新到Pagecache中
- 重制瞬时存储buffer
- 瞬时缓存中的内容写入Pagecache
- 设置commitedPosition游标
该过程调整啦 committedPosition
3 PageCache刷新到磁盘
CommitLog.FlushRealTimeService 线程开启异步线程中while循环中轮训刷新到磁盘
MappedFileQueue#flush 进行刷新有点类似commit的线程逻辑
- 获取MappedFile
- 刷新到磁盘
- 设置flushPosition游标
DefaultMappedFile#flush
-
对比游标判断是否需要刷新到磁盘
- 有瞬时存储的时候对fileChannel进行force 强制刷新到磁盘
- 没有瞬时存储的时候对mappedByteBuffer读buffer 强制刷新到磁盘
- 强制刷新flushedPosistion的游标
该过程调整啦 flushedPosition
4 异步构建队列索引文件和属性索引文件
文件存储设计的几点:
- 文件的命名按照文件的物理偏移量进行命名,每个问价的大些为1G,所以第二个文件的命名就是第一个文件名+1G的大小。这种设计可以在给出一个物理偏移量的值就可以通过二分发确定所在的文件位置
- commiteQueue的组织方式是/topic/queue,每个队列存在多个文件,文件内容的设计是每条消息对应一个条目,每个条目由8字节的消息物理偏移量、4字节消息长度、8字节的消息hashcode组成。所以根据某个主题某个队列中的消费逻辑偏移可以定位出条目的位置,进而确定出消息的物理偏移、确定commitlog的文件. concumeItem = logicOffset*20,(消息对应的队列索条目的物理位置)
- Index文件的设计:
前40字节的文件头+500w*4字节哈希槽+2000w*20字节的Index条目
其存储结构类似HashMap,通过对消息属性值的hash取模,将构建的Index条目的物理位置存入对应hash槽。对于hash冲突的进行链表处理。在查询的时候先查询通过数结构进行查询hash槽获取对应的Index条目。而Index条目存者消息的物理位置,从而确定commitLog文件中消息的位置。
文件头的40字节的内容
- 开始时间(8 byte)存储前索引文件内,所有消息的最小时间
- 结束时间(8 byte)存储前索引文件内,所有消息的最大时间,因为根据key查询的时候 ,时间是必填选项 ,开始与结束时间用来快速定位消息是否命中
- 最小物理偏移量(8 byte)存储前索引文件内,所有消息的最小物理偏移量
- 最大物理偏移量(8 byte)存储前索引文件内,所有消息的最大物理偏移量;框定最小、最大物理偏移量,是为了给通过物理地址查询时快速索引
- 有效hash slot数量(4 byte)因为存储hash冲突的情况,所以最坏情况是,hash slot只有1个,最理想情况是有500万个
- index索引数量(4 byte)如果当前索引文件已经构建完毕,那么该值是固定值2000万
4.1 异步构建Queue 和其他属性文件的流程
调用分发列表以及处理重放的消息
4.2 Index消息的构建
创建Index文件
在IndexFile的构造函数中创建文件建立内存文件映射
4.3 主题队列索引文件的构建
FlushConsumeQueueService 线程中异步线程
调用的底层MappedFileQueue的flush方法刷新磁盘,和commitLog的flush方法一样,上面已经分析参考上面分析的流程
至此消息接受落盘、构建队列索引和属性索引文件的流程已经分析完。