1、broker启动
启动逻辑在BrokerStartup和BrokerController中.
监听端口是1091。
默认存储目录是System.getProperty("user.home")+"/store",commitLog目录是在System.getProperty("user.home")+"/store/commitlog"
commitlog中每个MappedFile默认大小是1G。
创建topic,包括SELF_TEST_TOPIC,TBW102(依赖是否启动自动创建topic)、BenchmarkTest、DefaultCluster、brokername(主机名)、OFFSET_MOVED_EVENT、SCHEDULE_TOPIC_XXXX、RMQ_SYS_TRACE_TOPIC(是否开启跟踪topic)、DefaultCluster_REPLY_TOPIC。从System.getProperty("user.home")+"/store/config/topic.json"加载topic信息。
ConsumerOffsetManager从System.getProperty("use.home")+"/store/config/consumerOffset.json"加载consumerOffset信息。
SubscriptionGroupManager添加订阅组TOOLS_CONSUMER、FILTERSRV_CONSUMER、SELF_TEST_C_GROUP、CID_ONS-HTTP-PROXY、CID_ONSAPI_PULL、CID_ONSAPI_PERMISSION、CID_ONSAPI_OWNER。从System.getProperty("use.home")+"/store/config/subscriptionGroup.json"加载subscriptionGroup信息。
ConsumerFilterManager从System.getProperty("use.home")+"/store/config/consumerFilter.json"加载consumerFilter信息
DefaultMessageStore启动时会加载ccmmitLog,consumerqueue,checkpoint(store/checkpoint),加载目录下index的文件。
创建NettyServer,及不同业务的线程池,注册网络请示处理器。
开启周期性任务。24小时记录broker状态,默认5s持久化consumeroffset,10持久化consumerFilter,3分钟执行保护broker,1秒输出watermark,60s输出behind commit log字节,
其时序图为
2、Netty层的抽象设计
3、broker消息接收
其时序图为
4、broker存储层
其类层次图
创建MappedFile是根据当前是否有文件及当前文件是否已满。其主要思路从起始偏移为0开始,首先找到最后一个文件,如果没有找到,则计算新建文件的起始偏移点。如果找到并且文件已满,则用当前文件的表示的物理偏移起始点+文件大小作为新建文件的起始偏移。
public MappedFile getLastMappedFile(final long startOffset, boolean needCreate) {
long createOffset = -1;
MappedFile mappedFileLast = getLastMappedFile();
if (mappedFileLast == null) {
createOffset = startOffset - (startOffset % this.mappedFileSize);
}
if (mappedFileLast != null && mappedFileLast.isFull()) {
createOffset = mappedFileLast.getFileFromOffset() + this.mappedFileSize;
}
if (createOffset != -1 && needCreate) {
String nextFilePath = this.storePath + File.separator + UtilAll.offset2FileName(createOffset);
String nextNextFilePath = this.storePath + File.separator
+ UtilAll.offset2FileName(createOffset + this.mappedFileSize);
MappedFile mappedFile = null;
if (this.allocateMappedFileService != null) {
mappedFile = this.allocateMappedFileService.putRequestAndReturnMappedFile(nextFilePath,
nextNextFilePath, this.mappedFileSize);
} else {
try {
mappedFile = new MappedFile(nextFilePath, this.mappedFileSize);
} catch (IOException e) {
log.error("create mappedFile exception", e);
}
}
if (mappedFile != null) {
if (this.mappedFiles.isEmpty()) {
mappedFile.setFirstCreateInQueue(true);
}
this.mappedFiles.add(mappedFile);
}
return mappedFile;
}
return mappedFileLast;
}
CommitLog消息存储格式为
(1)TOTALSIZE:该消息条目总长度,4字节
(2)MAGICCODE:魔数,4字节
(3)BODYCRC:消息体crc校验码,4字节
(4)QUEUEID:消息消费队列ID,4字节
(5)FLAG:消息FLAG,不做处理,供应用程序使用,默认4字节
(6)QUEUEOFFSET:消息在消息消费队列的偏移量,8字节
(7)PHYSICALOFFSET:消息在CommitLog文件中的偏移量,8字节
(8)SYSFLAG:消息系统FLAG,例如是否压缩,是否是事务消息,4字节
(9)BORNTIMESTAMP:消息生产者调用消息发送API的时间戳,8字节
(10)BORNHOST:消息发送者IP,端口号,8字节
(11)STORETIMESTAMP:消息存储时间戳,8字节
(12)STOREHOSTADDRESS:B服务器IP+端口号,8字节
(13)RECONSUMERTIMES:消息重试次数,4字节
(14)Prepared Transaction Offset:事务消息物理偏移量,8字节
(15)BodyLength:消息体长度,4字节
(16)Body:消息体内容,长度为bodyLength中存储的值
(17)TopicLength:主题存储长度,1字节,表示主题名称不能超过255个字符
(18)Topic:主题,长度为TopicLength中存储的值
(19)PropertiesLength:消息属性长度,2字节,表示消息属性长度不能超过65536个字符。
(20)Properties:消息属性,长度为PropertiesLength中存储的值
每个CommitLog至少会空闲8个字节,高4字节存储剩余空间,低4字节存放CommitLog.BLANK_MAGIC_CODE
if ((msgLen + END_FILE_MIN_BLANK_LENGTH) > maxBlank) {
this.resetByteBuffer(this.msgStoreItemMemory, maxBlank);
// 1 TOTALSIZE
this.msgStoreItemMemory.putInt(maxBlank);
// 2 MAGICCODE
this.msgStoreItemMemory.putInt(CommitLog.BLANK_MAGIC_CODE);
// 3 The remaining space may be any value
// Here the length of the specially set maxBlank
final long beginTimeMills = CommitLog.this.defaultMessageStore.now();
byteBuffer.put(this.msgStoreItemMemory.array(), 0, maxBlank);
return new AppendMessageResult(AppendMessageStatus.END_OF_FILE, wroteOffset, maxBlank, msgId, msgInner.getStoreTimestamp(),
queueOffset, CommitLog.this.defaultMessageStore.now() - beginTimeMills);
}
AppendMessageResult的几种返回值
PROPERTIES_SIZE_EXCEEDED:消息属性超过最大允许值
MESSAGE_SIZE_EXCEEDED:消息长度超过最大允许长度
END_OF_FILE:超过文件大小
PUT_OK:追加成功
ConsumeQueue文件
单个ConsumeQueue文件中默认是包含30万个条目,每个条目占20字节(offset-8字节,size-4字节,tagscode-8字节),提供了根据逻辑偏移和根据时间戳来查找消息
public SelectMappedBufferResult getIndexBuffer(final long startIndex) {
int mappedFileSize = this.mappedFileSize;
long offset = startIndex * CQ_STORE_UNIT_SIZE;
if (offset >= this.getMinLogicOffset()) {
MappedFile mappedFile = this.mappedFileQueue.findMappedFileByOffset(offset);
if (mappedFile != null) {
SelectMappedBufferResult result = mappedFile.selectMappedBuffer((int) (offset % mappedFileSize));
return result;
}
}
return null;
}
public long getOffsetInQueueByTime(final long timestamp) {
MappedFile mappedFile = this.mappedFileQueue.getMappedFileByTime(timestamp);
if (mappedFile != null) {
long offset = 0;
int low = minLogicOffset > mappedFile.getFileFromOffset() ? (int) (minLogicOffset - mappedFile.getFileFromOffset()) : 0;
int high = 0;
int midOffset = -1, targetOffset = -1, leftOffset = -1, rightOffset = -1;
long leftIndexValue = -1L, rightIndexValue = -1L;
long minPhysicOffset = this.defaultMessageStore.getMinPhyOffset();
SelectMappedBufferResult sbr = mappedFile.selectMappedBuffer(0);
if (null != sbr) {
ByteBuffer byteBuffer = sbr.getByteBuffer();
high = byteBuffer.limit() - CQ_STORE_UNIT_SIZE;
try {
while (high >= low) {
midOffset = (low + high) / (2 * CQ_STORE_UNIT_SIZE) * CQ_STORE_UNIT_SIZE;
byteBuffer.position(midOffset);
long phyOffset = byteBuffer.getLong();
int size = byteBuffer.getInt();
if (phyOffset < minPhysicOffset) {
low = midOffset + CQ_STORE_UNIT_SIZE;
leftOffset = midOffset;
continue;
}
long storeTime =
this.defaultMessageStore.getCommitLog().pickupStoreTimestamp(phyOffset, size);
if (storeTime < 0) {
return 0;
} else if (storeTime == timestamp) {
targetOffset = midOffset;
break;
} else if (storeTime > timestamp) {
high = midOffset - CQ_STORE_UNIT_SIZE;
rightOffset = midOffset;
rightIndexValue = storeTime;
} else {
low = midOffset + CQ_STORE_UNIT_SIZE;
leftOffset = midOffset;
leftIndexValue = storeTime;
}
}
if (targetOffset != -1) {
offset = targetOffset;
} else {
if (leftIndexValue == -1) {
offset = rightOffset;
} else if (rightIndexValue == -1) {
offset = leftOffset;
} else {
offset =
Math.abs(timestamp - leftIndexValue) > Math.abs(timestamp
- rightIndexValue) ? rightOffset : leftOffset;
}
}
return (mappedFile.getFileFromOffset() + offset) / CQ_STORE_UNIT_SIZE;
} finally {
sbr.release();
}
}
}
return 0;
}
IndexFile的存储格式
5、更新ConsumeQueuee和IndexFile
分别是由CommitLogDispatcherBuildConsumeQueue和CommitLogDispatcherBuildIndex来更新,其相关类设计 为
其更新的时序图为
6、Processor的设计
code与Processor对应关系
role | processor | code |
broker | SendMessageProcessor | SEND_MESSAGE |
SEND_MESSAGE_V2 | ||
SEND_BATCH_MESSAGE | ||
CONSUMER_SEND_MSG_BACK | ||
PullMessageProcessor | PULL_MESSAGE | |
ReplyMessageProcessor | SEND_REPLY_MESSAGE | |
SEND_REPLY_MESSAGE_V2 | ||
QueryMessageProcessor | QUERY_MESSAGE | |
VIEW_MESSAGE_BY_ID | ||
ClientManageProcessor | HEART_BEAT | |
UNREGISTER_CLIENT | ||
CHECK_CLIENT_CONFIG | ||
ConsumerManageProcessor | GET_CONSUMER_LIST_BY_GROUP | |
UPDATE_CONSUMER_OFFSET | ||
QUERY_CONSUMER_OFFSET | ||
EndTransactionProcessor | END_TRANSACTION | |
AdminBrokerProcessor | 默认的处理器 | |
client(provider or consumer) | ClientRemotingProcessor | CHECK_TRANSACTION_STATE |
NOTIFY_CONSUMER_IDS_CHANGED | ||
RESET_CONSUMER_CLIENT_OFFSET | ||
GET_CONSUMER_STATUS_FROM_CLIENT | ||
GET_CONSUMER_RUNNING_INFO | ||
CONSUME_MESSAGE_DIRECTLY | ||
PUSH_REPLY_MESSAGE_TO_CLIENT | ||
namesrv | ClusterTestRequestProcessor | 集群测试时的默认处理器 |
DefaultRequestProcessor | 不是集群测试时的默认处理器 |
7、订阅组配置
groupName:消费组名
consumeEnable:是否可以消费, 默认是truee。如果为false,该消费组无法拉取消息,从而无法消费消息
consumeFromMinEnable:默认为true,是否允许从队列最小偏移量开始消费。
consumeBroadcastEnable:默认为true,设置该消费组是否能以广播模式消费。
retryQueueNums:重试队列个数,默认是1,第一个broker上一个重试队列。
retryMaxTimes:消息最大重试次数,默认为16
brokerId:masterId
whichBrokerWhenConsumSlowly:如果消息堵塞,将转向brokerId的服务器上拉取拉取消息,默认为1
notifyConsumerIdsChangedEnable:当消息发送变化时是否立即进行消息队列重新负载。
8、消费ACK消息处理(CONSUMER_SEND_MSG_BACK)
- 根据请求头中的group创建重试主题(%RETRY%+消费组名),队列id号为根据group查找得到的订阅配置组(SubscriptonGroupConfig)的重试队列中随机选取一个,并创建TopicConfig主题配置信息
- 根据请求头中的offset从commitlog文件中获取信息,将主题信息放入属性RETRY_TOPIC中
- 调置消息重试次数,如果重试次数超过订阅配置组中的retryMaxTimes,改变主题为 %DLQ%+消费组名,消息一旦进入DLQ队列中,rocketmq不负责再次调度进行消费,需要人工干预。
- 设置消息延迟级别
- 根据原先的消息创建一个新的消息对象,存入到commitlog中,存储时,会将主题设置为 SCHEDULE_TOPIC_XXXX,根据延迟级别转为对应的队列id,将消息的主题及队列id分别存在REAL_TOPIC和REAL_QID属性中。消息重试机制依赖于ScheduleMessageService服务的定时任务来实现
9、ScheduleMessageService任务处理
FIRST_DELAY_TIME:第一次调度时延迟时间,默认是1s
DELAY_FOR_A_WHILE:每一延时级别调度一次后延迟该时间间隔后再放入调度池,默认是100ms
DELAY_FOR_A_PERIOD:发送异常后延迟该时间后再继续参与调度,默认是10s
ConcurrentMap<Integer /* level */, Long/* delay timeMillis */> delayLevelTable:延迟级别,将"1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h"字符串解析成delayLevelTable,转换后的数据结构类似{1:1000, 2:5000, 3:30000, ...}
ConcurrentMap<Integer /* level */, Long/* offset */> offsetTable:延迟级别消息消费进度
defaultMessageStore:默认消息存储器
maxDelayLevel:最大消息延迟级别
rocketmq不支持任意的时间精度,只支持特定级别的延迟消息。消息延迟级别在Broker端通过mesageDelayLevel配置,默认为"1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h"。在将消息存入commitlog文件之前,会先判断消息的延迟级别,如果大于0,则将消息的主题设置为RMQ_SYS_SCHEDULE_TOPIC,队列id设置为delayLevel -1,同时会备份真实的topic,queueId
if (tranType == MessageSysFlag.TRANSACTION_NOT_TYPE
|| tranType == MessageSysFlag.TRANSACTION_COMMIT_TYPE) {
// Delay Delivery
if (msg.getDelayTimeLevel() > 0) {
if (msg.getDelayTimeLevel() > this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel()) {
msg.setDelayTimeLevel(this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel());
}
topic = TopicValidator.RMQ_SYS_SCHEDULE_TOPIC;
queueId = ScheduleMessageService.delayLevel2QueueId(msg.getDelayTimeLevel());
// Backup real topic, queueId
MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_TOPIC, msg.getTopic());
MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_QUEUE_ID, String.valueOf(msg.getQueueId()));
msg.setPropertiesString(MessageDecoder.messageProperties2String(msg.getProperties()));
msg.setTopic(topic);
msg.setQueueId(queueId);
}
}
定时消息实现类为ScheduleMessageService
9.1 启动
根据延迟级别创建对应的定时任务,启动定时任务持久化延迟消息队列进度存储。
根据延迟队列创建定时任务,遍历延迟级别,根据延迟级别level从offsetTable中获取消费队列的消费进度,如果不存在,则使用0。也就是说每一个延迟级别对应一个消息消费队列。然后创建定时任务,每一个定时任务任务第一次启动时默认延迟1s先执行一次定时任务,第二次调度开始才使用相应的延迟时间。延迟级别与消息消费队列的映射关系为:消息队列id=延迟级别-1
创建定时任务,每隔10s持久化一次延迟队列的消息消费进度,持久化频率可以通过flushDelayOffsetInterval配置属性来设置。
9.2 定时调度
调用任务是通过DeliverDelayedMessageTimerTask实现,主要逻辑 在executeOnTimeup中
根据主题SCHEDULE_TOPIC_XXXX和queueId(delayLevel-1)找到ConsumeQueue,根据offset解析出消息的物理偏移量,消息长度,消息tag hashcode,为从commitlog加载具体的消息作准备。
根据消息物理偏移量与消息大小从commitlog文件 中查找消息,重新新的消息对象,清除DELAY属性,并且设置主题为属性中的REAL_TOPIC,queueId为属性中的REAL_QID,消息的消费次数不会消失。
将消息再次存入到commitlog,并转发到主题对应的消息队列上,供消费者再次消费。
更新延迟队列拉取进度。