rocketmq中的broker设计与实现

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对应关系

roleprocessorcode
brokerSendMessageProcessorSEND_MESSAGE
SEND_MESSAGE_V2
SEND_BATCH_MESSAGE
CONSUMER_SEND_MSG_BACK
PullMessageProcessorPULL_MESSAGE
ReplyMessageProcessorSEND_REPLY_MESSAGE
SEND_REPLY_MESSAGE_V2
QueryMessageProcessorQUERY_MESSAGE
VIEW_MESSAGE_BY_ID
ClientManageProcessorHEART_BEAT
UNREGISTER_CLIENT
CHECK_CLIENT_CONFIG
ConsumerManageProcessorGET_CONSUMER_LIST_BY_GROUP
UPDATE_CONSUMER_OFFSET
QUERY_CONSUMER_OFFSET
EndTransactionProcessorEND_TRANSACTION
AdminBrokerProcessor默认的处理器
client(provider or consumer)ClientRemotingProcessorCHECK_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
namesrvClusterTestRequestProcessor集群测试时的默认处理器
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,并转发到主题对应的消息队列上,供消费者再次消费。

更新延迟队列拉取进度。

 
 
 
 

 

 

 
 


                
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

kgduu

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

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

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

打赏作者

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

抵扣说明:

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

余额充值