前言

面试官:你了解RocketMQ是如何存储消息的吗?
我:额,,,你等下,我看下这篇文字, (逃

由于这部分内容优点多,所以请哥哥姐姐们自备茶水,欢迎留言!

问题:

1. RocketMQ存储的文件是什么样子

2. RocketMQ为什么存储的性能高?

3. 事务的prepareMsg、rollback消息如何对消费者不可见

4. ConsumeQueue、Index文件是什么时候生成的,如何生成的

RocketMQ存储设计是高可用和高性能的保证, 利用磁盘存储来满足海量堆积能力。Kafka单机在topic数量在100+的时候,性能会下降很多,而RocketMQ能够在多个topic存在时,依然保持高性能

下面主要从存储结构、存储流程、存储优化的技术来形成文字

基于的版本是RocketMQ4.5.2

存储架构图

  1. 要发送的消息,会按顺序写入commitlog中,这里所有topic和queue共享一个文件
  2. 存入commitlog后,由于消息会按照topic纬度来消费,会异步构建consumeQueue(逻辑队列)和index(索引文件),consumeQueue存储消息的commitlogOffset/messageSize/tagHashCode, 方便定位commitlog中的消息实体。每个 Topic下的每个Message Queue都有一个对应的ConsumeQueue文件。索引文件(Index)提供消息检索的能力,主要在问题排查和数据统计等场景应用
  3. 消费者会从consumeQueue取到msgOffset,方便快速取出消息

好处

  1. CommitLog 顺序写 ,可以大大提高写人效率,提高堆积能力
  2. 虽然是随机读,但是利用操作系统的pagecache机制,可以批量地从磁盘读取,作为cache存到内存中,加速后续的读取速度
  3. 在实际情况中,大部分的 ConsumeQueue能够被全部读人内存,所以这个中间结构的操作速度很快, 可以认为是内存读取的速度

消息文件存储的结构设计

存储的文件主要分为:

  • commitlog: 存储消息实体
  • consumequeue: 按Topic和队列存储消息的offset
  • index: index按key、tag、时间等存储

commitlog(物理队列)

文件地址:${user.home} \store${commitlog}${fileName}

commitlog特点:

  • 存放该broke所有topic的消息
  • 默认1G大小
  • 以偏移量为文件名,当一个文件写满时则创建新文件,这样的设计主要是方便根据消息的物理偏移量,快速定位到消息所在的物理文件
  • 一个消息存储单元是不定长的
  • 顺序写但是随机读

消息单元的存储结构

下面的表格说明了,每个消息体不是定长的,会存储消息的哪些内容,包括物理偏移量、consumeQueue的偏移量、消息体等信息

顺序 字段名 说明
1 totalSize(4Byte) 消息大小
2 magicCode(4) 设置为daa320a7 (这个不太明白)
3 bodyCRC(4) 当broker重启recover时会校验
4 queueId(4) 消息对应的consumeQueueId
5 flag(4) rocketmq不做处理,只存储后透传
6 queueOffset(8) 消息在consumeQueue中的偏移量
7 physicalOffset(8) 消息在commitlog中的偏移量
8 sysFlg(4) 事务标示,NOT_TYPE/PREPARED_TYPE/COMMIT_TYPE/ROLLBACK_TYPE
9 bronTimestamp(8) 消息产生端(producer)的时间戳
10 bronHost(8) 消息产生端(producer)地址(address:port)
11 storeTimestamp(8) 消息在broker存储时间
12 storeHostAddress(8) 消息存储到broker的地址(address:port)
13 reconsumeTimes(4) 消息重试次数
14 preparedTransactionOffset(8) 事务消息的物理偏移量
15 bodyLength(4) 消息长度,最长不超过4MB
16 body(body length Bytes) 消息体内容
17 topicLength(1) 主题长度,最长不超过255Byte
18 topic(topic length Bytes) 主题内容
19 propertiesLength(2) 消息属性长度,最长不超过65535Bytes
20 properties(properties length Bytes) 消息属性内容

consumequeue文件(逻辑队列)

文件地址:${user.home}\store\consumeQueue${topic}${queueId}${fileName}

consumequeue文件特点:

  • 按topic和queueId纬度分别存储消息commitLogOffset、size、tagHashCode
  • 以偏移量为文件名
  • 一个存储单元是20个字节的定长的
  • 顺序读顺序写
  • 每个ConsumeQueue文件大小约5.72M

每个Topic下的每个MessageQueue都有一个对应的ConsumeQueue文件
该结构对应于消费者逻辑队列,为什么要将一个topic抽象出很多的queue呢?这样的话,对集群模式更有好处,可以使多个消费者共同消费,而不用上锁;

消息单元的存储结构

顺序 字段名 说明
1 offset(8) commitlog的偏移量
2 size(4) commitlog消息大小
3 tagHashCode tag的哈希值

index索引文件

文件地址:${user.home}\store\index${fileName}

index文件特点:

  • 以时间作为文件名
  • 一个存储单元是20个字节定长的

索引文件(Index)提供消息检索的能力,主要在问题排查和数据统计等场景应用

存储单元的结构

顺序 字段名 说明
1 keyHash(4) key的结构是
2 phyOffset(8) commitLog真实的物理位移
3 timeOffset(4) 时间偏移量
4 slotValue(4) 下一个记录的slot值

消息存储流程

RocketMQ文件存储模型层次结构

层次从上到下依次为:

  1. 业务层

    • QueueMessageProcessor类
    • PullMessageProcessor类
    • SendMessageProcessor类
    • DefaultMessageStore类
  2. 存储逻辑层
    • IndexService类
    • ConsumeQueue类
    • CommitLog类
    • IndexFile类
    • MappedFileQueue类
  3. 磁盘交互IO层
    • MappedFile类
    • MappedByteBuffer类
业务层 QueueMessageProcessor PullMessageProcessor
SendMessageProcessor
DefaultMessageStore
存储逻辑层 IndexService ConsumeQueue CommitLog
IndexFile MappedFileQueue
磁盘交互IO层 MappedFile
MappedByteBuffer
Disk

写commoitlog流程

1. DefaultMessageStore,入口方法是putMessage方法

RocketMQ 的存储核心类为 DefaultMessageStore,入口方法是putMessage方法

  1. // DefaultMessageStore#putMessage
  2. public PutMessageResult putMessage(MessageExtBrokerInner msg) {
  3. // 判断该服务是否shutdown,不可用直接返回【代码省略】
  4. // 判断broke的角色,如果是从节点直接返回【代码省略】
  5. // 判断runningFlags是否是可写状态,不可写直接返回,可写把printTimes设为0【代码省略】
  6. // 判断topic名字是否大于byte字节127, 大于则直接返回【代码省略】
  7. // 判断msg中properties属性长度是否大于short最大长度32767,大于则直接返回【代码省略】
  8.  
  9. if (this.isOSPageCacheBusy()) { // 判断操作系统页写入是否繁忙
  10. return new PutMessageResult(PutMessageStatus.OS_PAGECACHE_BUSY, null);
  11. }
  12.  
  13. long beginTime = this.getSystemClock().now();
  14. PutMessageResult result = this.commitLog.putMessage(msg); // $2 查看下方代码,写msg核心
  15.  
  16. long elapsedTime = this.getSystemClock().now() - beginTime;
  17. if (elapsedTime > 500) {
  18. log.warn("putMessage not in lock elapsed time(ms)={}, bodyLength={}", elapsedTime, msg.getBody().length);
  19. }
  20. // 记录写commitlog时间,大于最大时间则设置为这个最新的时间
  21. this.storeStatsService.setPutMessageEntireTimeMax(elapsedTime);
  22.  
  23. if (null == result || !result.isOk()) {
  24. // 记录写commitlog 失败次数
  25. this.storeStatsService.getPutMessageFailedTimes().incrementAndGet();
  26. }
  27.  
  28. return result;
  29. }

$2 CommitLog#putMessage 将日志写入CommitLog 文件

  1. public PutMessageResult putMessage(final MessageExtBrokerInner msg) {
  2. // Set the storage time
  3. msg.setStoreTimestamp(System.currentTimeMillis());
  4. // Set the message body BODY CRC (consider the most appropriate setting
  5. // on the client)
  6. msg.setBodyCRC(UtilAll.crc32(msg.getBody()));
  7. // Back to Results
  8. AppendMessageResult result = null;
  9.  
  10. StoreStatsService storeStatsService = this.defaultMessageStore.getStoreStatsService();
  11.  
  12. String topic = msg.getTopic();
  13. int queueId = msg.getQueueId();
  14.  
  15. final int tranType = MessageSysFlag.getTransactionValue(msg.getSysFlag()); // $1
  16. if (tranType == MessageSysFlag.TRANSACTION_NOT_TYPE
  17. || tranType == MessageSysFlag.TRANSACTION_COMMIT_TYPE) { // $2
  18. // Delay Delivery
  19. if (msg.getDelayTimeLevel() > 0) {
  20. if (msg.getDelayTimeLevel() > this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel()) {
  21. msg.setDelayTimeLevel(this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel());
  22. }
  23.  
  24. topic = ScheduleMessageService.SCHEDULE_TOPIC;
  25. queueId = ScheduleMessageService.delayLevel2QueueId(msg.getDelayTimeLevel());
  26.  
  27. // Backup real topic, queueId
  28. MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_TOPIC, msg.getTopic());
  29. MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_QUEUE_ID, String.valueOf(msg.getQueueId()));
  30. msg.setPropertiesString(MessageDecoder.messageProperties2String(msg.getProperties()));
  31.  
  32. msg.setTopic(topic);
  33. msg.setQueueId(queueId);
  34. }
  35. }
  36.  
  37. long elapsedTimeInLock = 0;
  38. MappedFile unlockMappedFile = null;
  39. MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile(); // $3
  40.  
  41. putMessageLock.lock(); //spin or ReentrantLock ,depending on store config // $4
  42. try {
  43. long beginLockTimestamp = this.defaultMessageStore.getSystemClock().now();
  44. this.beginTimeInLock = beginLockTimestamp;
  45.  
  46. // Here settings are stored timestamp, in order to ensure an orderly
  47. // global
  48. msg.setStoreTimestamp(beginLockTimestamp);
  49.  
  50. if (null == mappedFile || mappedFile.isFull()) { // $5
  51. mappedFile = this.mappedFileQueue.getLastMappedFile(0); // Mark: NewFile may be cause noise
  52. }
  53. if (null == mappedFile) {
  54. log.error("create mapped file1 error, topic: " + msg.getTopic() + " clientAddr: " + msg.getBornHostString());
  55. beginTimeInLock = 0;
  56. return new PutMessageResult(PutMessageStatus.CREATE_MAPEDFILE_FAILED, null);
  57. }
  58.  
  59. result = mappedFile.appendMessage(msg, this.appendMessageCallback); // $6
  60. switch (result.getStatus()) { // $7
  61. case PUT_OK:
  62. break;
  63. case END_OF_FILE:
  64. unlockMappedFile = mappedFile;
  65. // Create a new file, re-write the message
  66. mappedFile = this.mappedFileQueue.getLastMappedFile(0);
  67. if (null == mappedFile) {
  68. // XXX: warn and notify me
  69. log.error("create mapped file2 error, topic: " + msg.getTopic() + " clientAddr: " + msg.getBornHostString());
  70. beginTimeInLock = 0;
  71. return new PutMessageResult(PutMessageStatus.CREATE_MAPEDFILE_FAILED, result);
  72. }
  73. result = mappedFile.appendMessage(msg, this.appendMessageCallback);
  74. break;
  75. case MESSAGE_SIZE_EXCEEDED:
  76. case PROPERTIES_SIZE_EXCEEDED:
  77. beginTimeInLock = 0;
  78. return new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, result);
  79. case UNKNOWN_ERROR:
  80. beginTimeInLock = 0;
  81. return new PutMessageResult(PutMessageStatus.UNKNOWN_ERROR, result);
  82. default:
  83. beginTimeInLock = 0;
  84. return new PutMessageResult(PutMessageStatus.UNKNOWN_ERROR, result);
  85. }
  86.  
  87. elapsedTimeInLock = this.defaultMessageStore.getSystemClock().now() - beginLockTimestamp;
  88. beginTimeInLock = 0;
  89. } finally {
  90. putMessageLock.unlock();
  91. }
  92.  
  93. if (elapsedTimeInLock > 500) {
  94. log.warn("[NOTIFYME]putMessage in lock cost time(ms)={}, bodyLength={} AppendMessageResult={}", elapsedTimeInLock, msg.getBody().length, result);
  95. }
  96.  
  97. if (null != unlockMappedFile && this.defaultMessageStore.getMessageStoreConfig().isWarmMapedFileEnable()) {
  98. this.defaultMessageStore.unlockMappedFile(unlockMappedFile);
  99. }
  100.  
  101. PutMessageResult putMessageResult = new PutMessageResult(PutMessageStatus.PUT_OK, result);
  102.  
  103. // Statistics
  104. storeStatsService.getSinglePutMessageTopicTimesTotal(msg.getTopic()).incrementAndGet();
  105. storeStatsService.getSinglePutMessageTopicSizeTotal(topic).addAndGet(result.getWroteBytes());
  106.  
  107. handleDiskFlush(result, putMessageResult, msg); // $8
  108. handleHA(result, putMessageResult, msg); // $9
  109.  
  110. return putMessageResult;
  111. }
  1. $1 获取消息的事务类型
  2. $2 对于事务消息中UNKNOW、COMMIT消息,处理topic和queueId, 同时备份real_topic,real_queueId
  3. $3 获取最新的mappedFile文件,有可能为空
  4. $4 给写mappedFile加锁(默认自旋锁)
  5. $5 mappedFile为空时创建mappedFile文件, 创建的mappedFile文件offset为0
  6. $6 在mappedFile中append消息,下面具体说明
  7. $7 根据mappedFile写消息的结果
    • ok, 直接break
    • 文件剩下的空间不够写了,重新创建一个mappedFile文件, 重新写消息
    • msg大小,properties大小,未知错误,返回错误类型
  8. $8 执行刷盘
  9. $9 执行主从同步

3. $6 在mappedFile中append消息

mappedFile.appendMessage方法会调用this.appendMessagesInner方法

  1. public AppendMessageResult appendMessagesInner(final MessageExt messageExt, final AppendMessageCallback cb) {
  2. assert messageExt != null;
  3. assert cb != null;
  4.  
  5. int currentPos = this.wrotePosition.get(); // $1
  6.  
  7. if (currentPos < this.fileSize) {
  8. ByteBuffer byteBuffer = writeBuffer != null ? writeBuffer.slice() : this.mappedByteBuffer.slice(); // $2
  9. byteBuffer.position(currentPos);
  10. AppendMessageResult result;
  11. if (messageExt instanceof MessageExtBrokerInner) { // $3
  12. result = cb.doAppend(this.getFileFromOffset(), byteBuffer, this.fileSize - currentPos, (MessageExtBrokerInner) messageExt); // $4
  13. } else if (messageExt instanceof MessageExtBatch) {
  14. result = cb.doAppend(this.getFileFromOffset(), byteBuffer, this.fileSize - currentPos, (MessageExtBatch) messageExt);
  15. } else {
  16. return new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR);
  17. }
  18. this.wrotePosition.addAndGet(result.getWroteBytes()); // $5
  19. this.storeTimestamp = result.getStoreTimestamp();
  20. return result;
  21. }
  22. log.error("MappedFile.appendMessage return null, wrotePosition: {} fileSize: {}", currentPos, this.fileSize);
  23. return new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR);
  24. }
  1. $1 获取当前写入位置
  2. $2 创建写缓存,放入文件的写入位置
  3. $3 判断是单条消息还是批量消息
  4. $4 同步写消息, fileSize-currentPos即为该文件还剩下的空白大小
  5. $5 写完消息,累加文件当前位置

4. $4 同步写消息

代码在CommitLog内部类 DefaultAppendMessageCallback中

  1. // CommitLog$DefaultAppendMessageCallback#doAppend
  2. public AppendMessageResult doAppend(final long fileFromOffset, final ByteBuffer byteBuffer, final int maxBlank,
  3. final MessageExtBrokerInner msgInner) {
  4. // STORETIMESTAMP + STOREHOSTADDRESS + OFFSET <br>
  5.  
  6. long wroteOffset = fileFromOffset + byteBuffer.position(); // $1
  7. this.resetByteBuffer(hostHolder, 8); // $2
  8. String msgId = MessageDecoder.createMessageId(this.msgIdMemory, msgInner.getStoreHostBytes(hostHolder), wroteOffset);
  9.  
  10. // Record ConsumeQueue information
  11. keyBuilder.setLength(0);
  12. keyBuilder.append(msgInner.getTopic());
  13. keyBuilder.append('-');
  14. keyBuilder.append(msgInner.getQueueId());
  15. String key = keyBuilder.toString();
  16. Long queueOffset = CommitLog.this.topicQueueTable.get(key); // $3
  17. if (null == queueOffset) {
  18. queueOffset = 0L;
  19. CommitLog.this.topicQueueTable.put(key, queueOffset);
  20. }
  21.  
  22. // Transaction messages that require special handling
  23. final int tranType = MessageSysFlag.getTransactionValue(msgInner.getSysFlag());
  24. switch (tranType) {
  25. // Prepared and Rollback message is not consumed, will not enter the
  26. // consumer queuec
  27. case MessageSysFlag.TRANSACTION_PREPARED_TYPE:
  28. case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE: // $4
  29. queueOffset = 0L;
  30. break;
  31. case MessageSysFlag.TRANSACTION_NOT_TYPE:
  32. case MessageSysFlag.TRANSACTION_COMMIT_TYPE:
  33. default:
  34. break;
  35. }
  36.  
  37. // Serialize message // $5
  38. final byte[] propertiesData =
  39. msgInner.getPropertiesString() == null ? null : msgInner.getPropertiesString().getBytes(MessageDecoder.CHARSET_UTF8);
  40. final int propertiesLength = propertiesData == null ? 0 : propertiesData.length;
  41. if (propertiesLength > Short.MAX_VALUE) {
  42. log.warn("putMessage message properties length too long. length={}", propertiesData.length);
  43. return new AppendMessageResult(AppendMessageStatus.PROPERTIES_SIZE_EXCEEDED);
  44. }
  45.  
  46. final byte[] topicData = msgInner.getTopic().getBytes(MessageDecoder.CHARSET_UTF8);
  47. final int topicLength = topicData.length;
  48. final int bodyLength = msgInner.getBody() == null ? 0 : msgInner.getBody().length;
  49. final int msgLen = calMsgLength(bodyLength, topicLength, propertiesLength);
  50.  
  51. // Exceeds the maximum message
  52. if (msgLen > this.maxMessageSize) {
  53. CommitLog.log.warn("message size exceeded, msg total size: " + msgLen + ", msg body size: " + bodyLength
  54. + ", maxMessageSize: " + this.maxMessageSize);
  55. return new AppendMessageResult(AppendMessageStatus.MESSAGE_SIZE_EXCEEDED);
  56. }
  57.  
  58. // Determines whether there is sufficient free space
  59. if ((msgLen + END_FILE_MIN_BLANK_LENGTH) > maxBlank) { // $6
  60. this.resetByteBuffer(this.msgStoreItemMemory, maxBlank);
  61.  
  62. this.msgStoreItemMemory.putInt(maxBlank); // 1 TOTALSIZE
  63. this.msgStoreItemMemory.putInt(CommitLog.BLANK_MAGIC_CODE); // 2 MAGICCODE
  64. // 3 The remaining space may be any value
  65. // Here the length of the specially set maxBlank
  66. final long beginTimeMills = CommitLog.this.defaultMessageStore.now();
  67. byteBuffer.put(this.msgStoreItemMemory.array(), 0, maxBlank);
  68. return new AppendMessageResult(AppendMessageStatus.END_OF_FILE, wroteOffset, maxBlank, msgId, msgInner.getStoreTimestamp(),
  69. queueOffset, CommitLog.this.defaultMessageStore.now() - beginTimeMills);
  70. }
  71.  
  72. // $7 【代码省略】
  73.  
  74. if (propertiesLength > 0) this.msgStoreItemMemory.put(propertiesData);
  75. final long beginTimeMills = CommitLog.this.defaultMessageStore.now();
  76. // Write messages to the queue buffer
  77. byteBuffer.put(this.msgStoreItemMemory.array(), 0, msgLen); // $8
  78.  
  79. AppendMessageResult result = new AppendMessageResult(AppendMessageStatus.PUT_OK, wroteOffset, msgLen, msgId, // $9
  80. msgInner.getStoreTimestamp(), queueOffset, CommitLog.this.defaultMessageStore.now() - beginTimeMills);
  81.  
  82. switch (tranType) {
  83. case MessageSysFlag.TRANSACTION_PREPARED_TYPE:
  84. case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE:
  85. break;
  86. case MessageSysFlag.TRANSACTION_NOT_TYPE:
  87. case MessageSysFlag.TRANSACTION_COMMIT_TYPE:
  88. // The next update ConsumeQueue information
  89. CommitLog.this.topicQueueTable.put(key, ++queueOffset);
  90. break;
  91. default:
  92. break;
  93. }
  94. return result;
  95. }
  1. $1 计算消息的物理偏移量=文件初始偏移量+byteBuffer开始的偏移量,文件初始偏移量跟commitlog文件名相同
  2. $2 在读buffer之前,调用flip方法翻转buffer(设置position为0,limit设置为8)
  3. $3 在topicQueueTable中缓存msg对应的offset
  4. $4 针对事务消息的prepare、rollback消息,由于这个消息不需要对消费这可见,所以queueOffset=0,不记到consumerQueue
  5. $5 序列化properties,topic,计算消息最大值
  6. $6 如果消息长度+8大于MapperFile剩余文件空间,则返回END_OF_FILE, 抛给上层,由CommitLog#putMessage这层重新创建文件,重新写消息
  7. $7 根据commitlog的数据结构,构建commitlog数据,如TOTALSIZE,MAGICCODE 。。等等
  8. $8 把构建的this.msgStoreItemMemory写到byteBuffer中(内存中)
  9. $9 生成返回值
  10. $10 针对提交事务消息,重新放入topicQueueTable ???

异步构建ConsumeQueue和Index文件流程

  1. ConsumeQueue和IndexFile什么时候建立的呢?
    – 在Broker启动的时候,会启动一个ReputMessageService线程服务, 会去设置consumeQueueTable内存中最大的偏移量
  1. long maxPhysicalPosInLogicQueue = commitLog.getMinOffset();
  2. for (ConcurrentMap<Integer, ConsumeQueue> maps : this.consumeQueueTable.values()) {
  3. for (ConsumeQueue logic : maps.values()) {
  4. if (logic.getMaxPhysicOffset() > maxPhysicalPosInLogicQueue) {
  5. maxPhysicalPosInLogicQueue = logic.getMaxPhysicOffset();
  6. }
  7. }
  8. }
  9. if (maxPhysicalPosInLogicQueue < 0) {
  10. maxPhysicalPosInLogicQueue = 0;
  11. }
  12. if (maxPhysicalPosInLogicQueue < this.commitLog.getMinOffset()) {
  13. maxPhysicalPosInLogicQueue = this.commitLog.getMinOffset();
  14. log.warn("[TooSmallCqOffset] maxPhysicalPosInLogicQueue={} clMinOffset={}", maxPhysicalPosInLogicQueue, this.commitLog.getMinOffset());
  15. }
  16. this.reputMessageService.start();
  1. ReputMessageService线程每隔1ms执行doReput操作->根据CommitLog最新追加到的消息不断生成:
  • 消息的offset到CommitQueue
  • 消息索引到IndexFile
  1. 下面查看下doReput方法具体执行
  1. private void doReput() {
  2. if (this.reputFromOffset < DefaultMessageStore.this.commitLog.getMinOffset()) { // $1
  3. log.warn("The reputFromOffset={} is smaller than minPyOffset={}, this usually indicate that the dispatch behind too much and the commitlog has expired.",
  4. this.reputFromOffset, DefaultMessageStore.this.commitLog.getMinOffset());
  5. this.reputFromOffset = DefaultMessageStore.this.commitLog.getMinOffset();
  6. }
  7. for (boolean doNext = true; this.isCommitLogAvailable() && doNext; ) { // $2
  8. if (DefaultMessageStore.this.getMessageStoreConfig().isDuplicationEnable()
  9. && this.reputFromOffset >= DefaultMessageStore.this.getConfirmOffset()) {
  10. break;
  11. }
  12. SelectMappedBufferResult result = DefaultMessageStore.this.commitLog.getData(reputFromOffset); // $3
  13. if (result != null) {
  14. try {
  15. this.reputFromOffset = result.getStartOffset(); // $4
  16.  
  17. for (int readSize = 0; readSize < result.getSize() && doNext; ) {
  18. DispatchRequest dispatchRequest =
  19. DefaultMessageStore.this.commitLog.checkMessageAndReturnSize(result.getByteBuffer(), false, false); // $5 构建dispatchRequest
  20. int size = dispatchRequest.getBufferSize() == -1 ? dispatchRequest.getMsgSize() : dispatchRequest.getBufferSize();
  21.  
  22. if (dispatchRequest.isSuccess()) {
  23. if (size > 0) {
  24. DefaultMessageStore.this.doDispatch(dispatchRequest); // $6
  25.  
  26. if (BrokerRole.SLAVE != DefaultMessageStore.this.getMessageStoreConfig().getBrokerRole() // 如果该broker是主broker,可以推送消息到达conusmerQueue的消息,这里用户也客户自定定推送的监听
  27. && DefaultMessageStore.this.brokerConfig.isLongPollingEnable()) {
  28. DefaultMessageStore.this.messageArrivingListener.arriving(dispatchRequest.getTopic(),
  29. dispatchRequest.getQueueId(), dispatchRequest.getConsumeQueueOffset() + 1,
  30. dispatchRequest.getTagsCode(), dispatchRequest.getStoreTimestamp(),
  31. dispatchRequest.getBitMap(), dispatchRequest.getPropertiesMap());
  32. }
  33.  
  34. this.reputFromOffset += size; // $7
  35. readSize += size;
  36. if (DefaultMessageStore.this.getMessageStoreConfig().getBrokerRole() == BrokerRole.SLAVE) {
  37. DefaultMessageStore.this.storeStatsService
  38. .getSinglePutMessageTopicTimesTotal(dispatchRequest.getTopic()).incrementAndGet();
  39. DefaultMessageStore.this.storeStatsService
  40. .getSinglePutMessageTopicSizeTotal(dispatchRequest.getTopic())
  41. .addAndGet(dispatchRequest.getMsgSize());
  42. }
  43. } else if (size == 0) {
  44. this.reputFromOffset = DefaultMessageStore.this.commitLog.rollNextFile(this.reputFromOffset);
  45. readSize = result.getSize();
  46. }
  47. } else if (!dispatchRequest.isSuccess()) {
  48.  
  49. if (size > 0) { // &8
  50. log.error("[BUG]read total count not equals msg total size. reputFromOffset={}", reputFromOffset);
  51. this.reputFromOffset += size;
  52. } else {
  53. doNext = false;
  54. // If user open the dledger pattern or the broker is master node,
  55. // it will not ignore the exception and fix the reputFromOffset variable
  56. if (DefaultMessageStore.this.getMessageStoreConfig().isEnableDLegerCommitLog() ||
  57. DefaultMessageStore.this.brokerConfig.getBrokerId() == MixAll.MASTER_ID) {
  58. log.error("[BUG]dispatch message to consume queue error, COMMITLOG OFFSET: {}",
  59. this.reputFromOffset);
  60. this.reputFromOffset += result.getSize() - readSize;
  61. }
  62. }
  63. }
  64. }
  65. } finally {
  66. result.release();
  67. }
  68. } else {
  69. doNext = false;
  70. }
  71. }
  72. }
  • doReput流程:
    1. $1 如果reputFromOffset小于文件起始偏移量,则把reputFromOffset设置为文件起始偏移量,出现的可能原因:磁盘损坏,认为人为了文件等
    2. $2 因为reputFromOffset是consumeQueue中的偏移量,所以只要reputFromOffset小于commitlog最大偏移量,就会不断的循环
    3. $3 根据offset获取byteBuffer
    4. $4 更新reputFromOffset成byteBuffer中的offset
    5. $5 构建dispatchRequest
    6. $6 分别调用CommitLogDispatcherBuildConsumeQueue(构建消息消费队列)和CommitLogDispatcherBuildIndex(构建索引文件)
    7. $7 读完这条消息,更新reputFromOffset+=size,更新readSize+=size
    8. $8 不成功,如果这个消息的size不为0,尝试下一条
  1. 根据消息更新ConsumeQueue
    在doReput方法中$6中会更新consumeQueue, 消息消费队列转发的任务实现类为:CommitLogDispatcherBuildConsumeQueue,内部实际调用的是putMessagePositionInfo方法

  Step1: 根据topicId和queueId获取ConsumeQueue
  Step2: 将消息偏移量、消息size、tagHashCode(查看ConsumeQueue的数据结构)),把消息追加到ConsumeQueue的内存映射文件(mappedFile)中(不刷盘),consumeQueue默认异步刷盘

  1. return mappedFile.appendMessage(this.byteBufferIndex.array());
  1. 根据消息更新Index索引文件
    Hash索引文件转发任务实现类:CommitLogDispatcherBuildIndex

    如果messageIndexEnable设置为true, 则转发此任务,否则不转发
    step1: 获取indexFile, 如果indexFileList的内存中没有indexFile,则根据路径重新构建indexFile
    step2: 如果消息的唯一键不存在,则条件到放到indexFile中

说说存储的类与文件

DefaultMessageStore类核心属性

上面说到DefaultMessageStore是存储的业务层,putMessage是入口方法

  • messageStoreConfig

    • 存储相关的配置,例如存储路径、commitLog文件大小,刷盘频次等等。
  • CommitLog commitLog
    • comitLog 的核心处理类,消息存储在 commitlog 文件中。
  • ConcurrentMap<String/* topic /, ConcurrentMap<Integer/ queueId */, ConsumeQueue>> consumeQueueTable
    • topic 的队列信息。
  • FlushConsumeQueueService flushConsumeQueueService
    • ConsumeQueue 刷盘服务线程。
  • CleanCommitLogService cleanCommitLogService
    commitLog 过期文件删除线程。
  • CleanConsumeQueueService cleanConsumeQueueService
    • consumeQueue 过期文件删除线程。、
  • IndexService indexService
    • 索引服务。
  • AllocateMappedFileService allocateMappedFileService
    • MappedFile 分配线程,RocketMQ 使用内存映射处理 commitlog、consumeQueue文件。
  • ReputMessageService reputMessageService
    • reput 转发线程(负责 Commitlog 转发到 Consumequeue、Index文件)。
  • HAService haService
    • 主从同步实现服务。
  • ScheduleMessageService scheduleMessageService
    • 定时任务调度器,执行定时任务。
  • StoreStatsService storeStatsService
    • 存储统计服务。
  • TransientStorePool transientStorePool
    • ByteBuffer 池
  • RunningFlags runningFlags
    • 存储服务状态。
  • BrokerStatsManager brokerStatsManager
    • Broker 统计服务。
  • MessageArrivingListener messageArrivingListener
    • 消息达到监听器。
  • StoreCheckpoint storeCheckpoint
    • 刷盘检测点。
  • LinkedList dispatcherList
    • 转发 comitlog 日志,主要是从 commitlog 转发到 consumeQueue、index 文件。

从上面的属性可以观察到有几类属性:

  • 服务类:如刷盘服务线程、删除文件线程、索引服务、mappedFile分配线程、reput转发线程、主从同步线程、定时任务服务、broker统计服务
  • 配置类:存储设置类
  • 存储信息类:commitLog、consumeQueueTable topic队列信息、transientStorePool ByteBuffer池、刷盘检测点、dispatcherList
  • 监听器:消息达到监听器

刷盘

这里会另起一篇文字来说明

执行主从同步

这里会另起一篇文字来说明

PageCache(页缓存)与Mmap内存映射

pageCache定义

Page cache 也叫页缓冲或文件缓冲,是由好几个磁盘块构成,大小通常为4k,在64位系统上为8k,构成的几个磁盘块在物理磁盘上不一定连续,文件的组织单位为一页, 也就是一个page cache大小,文件读取是由外存上不连续的几个磁盘块,到buffer cache,然后组成page cache,然后供给应用程序。

pageCache加载

操作系统操作I/O时,会先在pageCache中查找,如果未命中,则启动磁盘I/O,并把磁盘文件中的数据加载到pageCache的一个空闲快中,然后在copy到用户缓冲区

pageCache预读

对于每个文件的第一个读请求操作,系统在读入所请求页面的同时会顺序读入后面少数几个页面

pageCache与RocketMQ的关联

MQ读取消息依赖系统PageCache,PageCache命中率越高,读性能越高

ConsumeQueue逻辑消费队列是顺序读取,在pageCache机制的预读取作用下,ConsumeQueue的读性能会比较高近乎内存,即使在有消息堆积情况下也不会影响性能。

Mmap内存映射技术—MappedByteBuffer

另外,RocketMQ主要通过MappedByteBuffer对文件进行读写操作。其中,利用了NIO中的FileChannel模型直接将磁盘上的物理文件直接映射到用户态的内存地址中(这种Mmap的方式减少了传统IO将磁盘文件数据在操作系统内核地址空间的缓冲区和用户应用程序地址空间的缓冲区之间来回进行拷贝的性能开销),将对文件的操作转化为直接对内存地址进行操作,从而极大地提高了文件的读写效率

使用mmap内存映射的限制

  • 每次只能映射1.5左右的文件至用户态的虚拟内存,这也是为何RocketMQ默认设置单个CommitLog日志数据文件为1G的原因
  • MMAP 使用的是虚拟内存,和 PageCache 一样是由操作系统来控制刷盘的,虽然可以通过 force() 来手动控制,但这个时间把握不好,在小内存场景下会很令人头疼。
  • 会存在内存占用率较高和文件关闭不确定性的问题

结语

参考:

欢迎关注我的公众号

【RocketMQ源码学习】- 5. 消息存储机制的更多相关文章

  1. RocketMQ 源码学习笔记————Producer 是怎么将消息发送至 Broker 的?

    目录 RocketMQ 源码学习笔记----Producer 是怎么将消息发送至 Broker 的? 前言 项目结构 rocketmq-client 模块 DefaultMQProducerTest ...

  2. RocketMQ 源码学习笔记 Producer 是怎么将消息发送至 Broker 的?

    目录 RocketMQ 源码学习笔记 Producer 是怎么将消息发送至 Broker 的? 前言 项目结构 rocketmq-client 模块 DefaultMQProducerTest Roc ...

  3. (转)RocketMQ源码学习--消息存储篇

    http://www.tuicool.com/articles/umQfMzA 1.序言 今天来和大家探讨一下RocketMQ在消息存储方面所作出的努力,在介绍RocketMQ的存储模型之前,可以先探 ...

  4. RocketMQ源码学习--消息存储篇

    转载. https://blog.csdn.net/mr253727942/article/details/55805876 1.序言 今天来和大家探讨一下RocketMQ在消息存储方面所作出的努力, ...

  5. 菜鸟学习Fabric源码学习 — kafka共识机制

    Fabric 1.4源码分析 kafka共识机制 本文档主要介绍kafka共识机制流程.在查看文档之前可以先阅览raft共识流程以及orderer服务启动流程. 1. kafka 简介 Kafka是最 ...

  6. 【RocketMQ源码学习】- 1. 入门

    为什么读RocketMQ 消息队列在互联网应用中使用较为广泛,学习她可以让我门更加了解使用技术的工作原理 透过学习她的源码,拓宽认知 RocketMQ经历了阿里双十一 有哪些名词 Producer 消 ...

  7. 【RocketMQ源码学习】- 4. Client 事务消息源码解析

    介绍 > 基于4.5.2版本的源码 1. RocketMQ是从4.3.0版本开始支持事务消息的. 2. RocketMQ的消息队列能够保证生产端,执行数据和发送MQ消息事务一致性,而消费端的事务 ...

  8. 【RocketMQ源码学习】- 3. Client 发送同步消息

    本文较长,代码后面给了方法简图,希望给你帮助 发送的方式 同步发送 异步发送 消息的类型 普通消息 顺序消息 事务消息 发送同步消息的时序图 为了防止读者朋友嫌烦,可以看下时序图,后面我也会给出方法的 ...

  9. RocketMq源码学习(一) nameService

    public class NamesrvStartup { public static Properties properties = null; public static CommandLine ...

随机推荐

  1. Java基础(三十一)JDBC(1)常用类和接口

    1.Driver接口 每种数据库的驱动程序都应该提供一个实现java.sql.Driver接口的类.在加载某一驱动程序的Driver类时,它应该创建自己的实例并向java.sql.DriverMana ...

  2. python的多线程和多进程(一)

    在进入主题之前,我们先学习一下并发和并行的概念: --并发:在操作系统中,并发是指一个时间段中有几个程序都处于启动到运行完毕之间,且这几个程序都是在同一个处理机上运行.但任一时刻点上只有一个程序在处理 ...

  3. QQ聊天记录分析

    今天我们用R语言来处理一下.我们会用到一下技术:. (1)正则表达式 (2)词频统计 (3)文本可视化 (4)ggplot2绘图 (5)中文分词 一.数据处理 首先我们要讲QQ聊天记录导出成txt文件 ...

  4. CentOS7.5模板机配置

    CentOS7.5模板机配置 标签(空格分隔): linux学习知识整理 Mr.Wei's notes! 人一定要有梦想,没有梦想那根咸鱼有什么区别: 即便自己成为了一条咸鱼,也要成为咸鱼里最咸的那一 ...

  5. linux文本编辑器教学

    linux常见服务 一. 文本编辑器 vi vim是vi增强版 vim需要安装 sudo apt-get -y install vim 1 vim的三种工作模式 1 编辑模式 命令模式=>编辑模 ...

  6. 基于Mustache实现sql拼接

    目录 一.前言 二.Mustache语法 三.Mustache拼接sql 一.前言 Mustache语法是一种模板语法,它可以帮我们拼接我们想要的东西.入职新公司,而项目里的sql语句就是用Musta ...

  7. html获得当前日期

    <html> <head> <title> </title> </head> <body> <!-- 获得当前日期(年月日 ...

  8. Java自动化测试框架-09 - TestNG之依赖注入篇 (详细教程)

    1.-依赖注入 TestNG支持两种不同类型的依赖项注入:本机(由TestNG本身执行)和外部(由诸如Guice的依赖项注入框架执行). 1.1-本机依赖项注入 TestNG允许您在方法中声明其他参数 ...

  9. Cookie、Session、Token那点事儿

    1.什么是Cookie? Cookie 技术产生源于 HTTP 协议在互联网上的急速发展.随着互联网时代的策马奔腾,带宽等限制不存在了,人们需要更复杂的互联网交互活动,就必须同服务器保持活动状态(简称 ...

  10. 学习笔记55_Nhibernate

    另一种ORM框架 1.添加各种dll 2.添加配置信息,根据文档直接复制粘贴.config //一般下载Nhibernate-3.0.0.Alpha2-bin包,会有Configuration_Tem ...