RocketMQ 学习笔记

基础概念

rocketMq 整体架构借鉴了kafka, 通过将 topic 分割为多个不同的队列, 分别存放在不同的服务中,从而实现了高性能. 一个 topic 就是概念上的一类消息的集合体, 所有这一类的消息都会向这个 topic 中推送. 但在实际推送中, 会根据生产者自身的负载均衡策略, 推送到不同的队列中去, 这些队列会分布在不同的服务器中.

1 消息模型

  1. prducer: 消息生产者.
  2. borker: mq 服务.
  3. consumer: 消息消费者.
  4. topic: 主题,一类消息的集合.
  5. name server: 名称服务, 其本质就是一个注册中心, mq 自己定制的,主要功能是提供路由信息.
  6. message: 消息. 每一条消息都有一个ID(虽然使用了复杂的算法,但仍不能保证消息ID一定是唯一的). message上可以打tag标签,用于消息的过滤. message还可以使用一些属性用于过滤. 但过滤消息最好的处理方式,仍然是根据tag过滤.这是由mq本身结构所决定的.
  7. producerGroup: 生产者组. 向相同 topic 生产消息的一组生产者.
  8. consumerGroup: 消费者组. rocketmq 的所有消费都是按照消费组作为一个整体进行的,包括记录用户消费进度也是按照消费组进行的记录的. 记录在 store 目录下的 consumerOffset.json 中.
  • 一个生产者组、消费者组 都只能对应一个相同 topic. 否则会产生错误. 因为 rocketMq 对 client(不论是productor还是consumer) 端交互时, 都是以组为单位的. 一旦同一个组对应的 topic 不同, 数据就会出现混乱. (例如 rebalance )

2 消息的存储

存储结构

来自rocketMq源码

rocketMq 收到消息后, 主要会在 3 种文件种存储信息

  1. commitLog:
    broker 收到的所有消息会第一时间存储在 commitLog 中. 这里存放了消息所有的信息, 但不会对消息分门别类, 只会按照接收的顺序存储. commitLog 文件为了提高读写速度, 采用顺序读写(在固态磁盘中, 几乎与内存读写无差异), 所以commitLog的文件大小是固定的.由于使用了mmap的零拷贝技术, 基于mmap对文件大小限制, 所以每个commitLog被固定为 1G . 文件名使用它的偏移量命名.
  2. consumerQueue: (后简写为 CQ)
    当broker启动后, 会启动一条线程专门用来做消息的分发工作(线程会循环检测是否有新的消息写入了commitLog而没有分发. 如果有, 会分发到 CQ 和 fileIndex 中). consumerQueue 对应了 topic 中的不同队列, 当消费者消费数据时, 需要将对应队列的消息偏移量发送给 broker. borker 会记录下每个 concumerGroup 的消费位置.
    来自rocketMq源码
    每一个consumerQueue 是由 30 万个小块组成的, 每一块有 20 个字节, 如上图所示. commitLog offset 存储的是消息在 commitLog 中的偏移量. size 存储了消息的大小, Message Tag Hashcode 存储了消息中 tag 字段的 hash 值. 从这里可以看出, 使用 tag 字段做消息的过滤, 可以在 consumerQueue 阶段就基本完成过滤, 效率很高.
  3. fileIndex:
    文件索引, 主要目的是用来通过 消息ID和Topic来查询到消息在 commitLog 中的偏移量, 从而得到完整的消息.
    来自rocketMq源码
    fileIndex 的 head 占用 40 个字节, 它主要存储本文件中, 第一条消息的创建时间、最后一条消息的创建时间、第一条消息对应的偏移量、最后一条消息对应的偏移量、已使用的slot个数、已存储的消息数量. slot table 部分类似与 hashTable 的数据结构, 它是由 500w 个 4byte(存储 int) 的槽构成. 每个槽会记录下最后一个索引文件对应的值(它是第几个写入的索引文件, 由于每个索引文件大小一致, 可以得到索引文件的位置). 而一个消息应该存储在哪个槽位中, 根据对topic#msgID 做 hashcode, 然后对 500w 取余得到. 插入时, 采用后插法, 每一个index 会记录同槽位的上一个 index 的位置. 每个 index 20 byte , 结构如上图所示.
    fileIndex 使用时间命名, 之所以这么做, 是为了在索引检索时, 如果知道消息的时间, 那么可以快速的定位到对应的索引文件.

存储流程

  1. borker 启动
//BrokerController核心的启动方法
    public void start() throws Exception {
        // messageStore 是一个 sleep(1) 的线程,它会不停的检测 commitLog中是否有新的数据,然后将新数据一条一条读取下来,加入到 consumerQueue 和 indexFile 中。
        // 如果开启了 长轮询 LongPolling。会将消息推送给 pullMessageExecutor 中执行。通过 PullRequestHoldService 获取到所有的RemotingCommand,遍历处理。
        if (this.messageStore != null) {
            this.messageStore.start();
        }
        // netty 服务
        if (this.remotingServer != null) {
            this.remotingServer.start();
        }
		...
    }
  1. remotingServer.start() 启动一个netty服务, 关注它的业务handler中的 read0() 方法, 会调用如下方法
public void processMessageReceived(ChannelHandlerContext ctx, RemotingCommand msg) throws Exception {
        final RemotingCommand cmd = msg;
        if (cmd != null) {
            switch (cmd.getType()) {
                case REQUEST_COMMAND:
                	// 请求, client端发送给broker的消息(包括 prducer 和 consumer)
                    processRequestCommand(ctx, cmd);
                    break;
                case RESPONSE_COMMAND:
                	// 响应, borker端发送给client的消息
                    processResponseCommand(ctx, cmd);
                    break;
                default:
                    break;
            }
        }
    }
  1. 关注请求方法
public void processRequestCommand(final ChannelHandlerContext ctx, final RemotingCommand cmd) {
        // 根据不同的命令编码, 获取对不同命令对处理器
        final Pair<NettyRequestProcessor, ExecutorService> matched = this.processorTable.get(cmd.getCode());
        // 默认值
        final Pair<NettyRequestProcessor, ExecutorService> pair = null == matched ? this.defaultRequestProcessor : matched;
        final int opaque = cmd.getOpaque();
        
        if (pair != null) {
            Runnable run = new Runnable() {
                @Override
                public void run() {
                    try {
                        doBeforeRpcHooks(RemotingHelper.parseChannelRemoteAddr(ctx.channel()), cmd);
                        final RemotingResponseCallback callback = new RemotingResponseCallback() {
                            @Override
                            public void callback(RemotingCommand response) {
                                doAfterRpcHooks(RemotingHelper.parseChannelRemoteAddr(ctx.channel()), cmd, response);
                                if (!cmd.isOnewayRPC()) {
                                    if (response != null) {
                                        response.setOpaque(opaque);
                                        response.markResponseType();
                                        try {
                                            ctx.writeAndFlush(response);
                                        } catch (Throwable e) {
                                            log.error("process request over, but response failed", e);
                                            log.error(cmd.toString());
                                            log.error(response.toString());
                                        }
                                    } else {
                                    }
                                }
                            }
                        };
                        // 根据对应的处理器, 执行请求(同步/异步)
                        if (pair.getObject1() instanceof AsyncNettyRequestProcessor) {
                            AsyncNettyRequestProcessor processor = (AsyncNettyRequestProcessor)pair.getObject1();
                            processor.asyncProcessRequest(ctx, cmd, callback);
                        } else {
                            NettyRequestProcessor processor = pair.getObject1();
                            RemotingCommand response = processor.processRequest(ctx, cmd);
                            callback.callback(response);
                        }
                    } catch (Throwable e) {
                        log.error("process request exception", e);
                        log.error(cmd.toString());
                        if (!cmd.isOnewayRPC()) {
                            final RemotingCommand response = RemotingCommand.createResponseCommand(RemotingSysResponseCode.SYSTEM_ERROR,
                                RemotingHelper.exceptionSimpleDesc(e));
                            response.setOpaque(opaque);
                            ctx.writeAndFlush(response);
                        }
                    }
                }
            };
            
            // 拒绝
            if (pair.getObject1().rejectRequest()) {
                final RemotingCommand response = RemotingCommand.createResponseCommand(RemotingSysResponseCode.SYSTEM_BUSY,
                    "[REJECTREQUEST]system busy, start flow control for a while");
                response.setOpaque(opaque);
                ctx.writeAndFlush(response);
                return;
            }
            // 交给处理器执行请求任务,会调用前面写的 run 对象的 run() 方法
            try {
                final RequestTask requestTask = new RequestTask(run, ctx.channel(), cmd);
                pair.getObject2().submit(requestTask);
            } catch (RejectedExecutionException e) {
                if ((System.currentTimeMillis() % 10000) == 0) {
                    log.warn(RemotingHelper.parseChannelRemoteAddr(ctx.channel())
                        + ", too many requests and system thread pool busy, RejectedExecutionException "
                        + pair.getObject2().toString()
                        + " request code: " + cmd.getCode());
                }

                if (!cmd.isOnewayRPC()) {
                    final RemotingCommand response = RemotingCommand.createResponseCommand(RemotingSysResponseCode.SYSTEM_BUSY,
                        "[OVERLOAD]system busy, start flow control for a while");
                    response.setOpaque(opaque);
                    ctx.writeAndFlush(response);
                }
            }
        } else {
            String error = " request type " + cmd.getCode() + " not supported";
            final RemotingCommand response =
                RemotingCommand.createResponseCommand(RemotingSysResponseCode.REQUEST_CODE_NOT_SUPPORTED, error);
            response.setOpaque(opaque);
            ctx.writeAndFlush(response);
            log.error(RemotingHelper.parseChannelRemoteAddr(ctx.channel()) + error);
        }
    }
  1. CommitLog 写入: 以 SendMsgProcessor 的 processRequest 方法为例 (这里会根据消息的不同, 调用多种不同的 processor), 它会调用到 commitLog 的异步刷盘方法 asyncPutMessage
 public PutMessageResult putMessage(final MessageExtBrokerInner msg) {
        // Set the storage time
        msg.setStoreTimestamp(System.currentTimeMillis());
        // Set the message body BODY CRC (consider the most appropriate setting
        // on the client)
        msg.setBodyCRC(UtilAll.crc32(msg.getBody()));
        // Back to Results
        AppendMessageResult result = null;

        StoreStatsService storeStatsService = this.defaultMessageStore.getStoreStatsService();

        String topic = msg.getTopic();
        int queueId = msg.getQueueId();

        final int tranType = MessageSysFlag.getTransactionValue(msg.getSysFlag());
        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);
            }
        }

        InetSocketAddress bornSocketAddress = (InetSocketAddress) msg.getBornHost();
        if (bornSocketAddress.getAddress() instanceof Inet6Address) {
            msg.setBornHostV6Flag();
        }

        InetSocketAddress storeSocketAddress = (InetSocketAddress) msg.getStoreHost();
        if (storeSocketAddress.getAddress() instanceof Inet6Address) {
            msg.setStoreHostAddressV6Flag();
        }

        long elapsedTimeInLock = 0;

        MappedFile unlockMappedFile = null;
        //mappedFile 零拷贝实现
        MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile();
      
        putMessageLock.lock(); //spin or ReentrantLock ,depending on store config
        try {
            long beginLockTimestamp = this.defaultMessageStore.getSystemClock().now();
            this.beginTimeInLock = beginLockTimestamp;

            // Here settings are stored timestamp, in order to ensure an orderly
            // global
            msg.setStoreTimestamp(beginLockTimestamp);

            if (null == mappedFile || mappedFile.isFull()) {
                mappedFile = this.mappedFileQueue.getLastMappedFile(0); // Mark: NewFile may be cause noise
            }
            if (null == mappedFile) {
                log.error("create mapped file1 error, topic: " + msg.getTopic() + " clientAddr: " + msg.getBornHostString());
                beginTimeInLock = 0;
                return new PutMessageResult(PutMessageStatus.CREATE_MAPEDFILE_FAILED, null);
            }
            //直接以Append的方式写入文件
            result = mappedFile.appendMessage(msg, this.appendMessageCallback);
            //文件写入的结果
            switch (result.getStatus()) {
                case PUT_OK:
                    break;
                //文件写满了,就创建一个新文件,重写消息
                case END_OF_FILE:
                    unlockMappedFile = mappedFile;
                    // Create a new file, re-write the message
                    mappedFile = this.mappedFileQueue.getLastMappedFile(0);
                    if (null == mappedFile) {
                        // XXX: warn and notify me
                        log.error("create mapped file2 error, topic: " + msg.getTopic() + " clientAddr: " + msg.getBornHostString());
                        beginTimeInLock = 0;
                        return new PutMessageResult(PutMessageStatus.CREATE_MAPEDFILE_FAILED, result);
                    }
                    result = mappedFile.appendMessage(msg, this.appendMessageCallback);
                    break;
                case MESSAGE_SIZE_EXCEEDED:
                case PROPERTIES_SIZE_EXCEEDED:
                    beginTimeInLock = 0;
                    return new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, result);
                case UNKNOWN_ERROR:
                    beginTimeInLock = 0;
                    return new PutMessageResult(PutMessageStatus.UNKNOWN_ERROR, result);
                default:
                    beginTimeInLock = 0;
                    return new PutMessageResult(PutMessageStatus.UNKNOWN_ERROR, result);
            }

            elapsedTimeInLock = this.defaultMessageStore.getSystemClock().now() - beginLockTimestamp;
            beginTimeInLock = 0;
        } finally {
            putMessageLock.unlock();
        }

        if (elapsedTimeInLock > 500) {
            log.warn("[NOTIFYME]putMessage in lock cost time(ms)={}, bodyLength={} AppendMessageResult={}", elapsedTimeInLock, msg.getBody().length, result);
        }

        if (null != unlockMappedFile && this.defaultMessageStore.getMessageStoreConfig().isWarmMapedFileEnable()) {
            this.defaultMessageStore.unlockMappedFile(unlockMappedFile);
        }

        PutMessageResult putMessageResult = new PutMessageResult(PutMessageStatus.PUT_OK, result);

        // Statistics
        storeStatsService.getSinglePutMessageTopicTimesTotal(msg.getTopic()).incrementAndGet();
        storeStatsService.getSinglePutMessageTopicSizeTotal(topic).addAndGet(result.getWroteBytes());
        //文件刷盘
        handleDiskFlush(result, putMessageResult, msg);
        //主从同步
        handleHA(result, putMessageResult, msg);

        return putMessageResult;
    }
  1. ConsumerQueue 和 IndexFile : 当 broker 启动后, 会启动一 DefaultMessageStore.start() 方法, 这里调用 ReputMessageService.start() 方法, 最终来到 ReputMessageService. doReput(), 在 ReputMessageService.start() 中会一直循环调用 doReput() 方法, 但每次调用会睡眠 1 毫秒.
    长轮询 是为了解决消费端请求拉去消息时(不论是push 还是 pull 方式, 其根源都是 pull 消息, 也就是说, consumer 获取消息的方法只有一种, 就是向 borker 发送一个请求, broker 收到请求后, 根据请求的不同, 选择推送一次消息还是, 一直提供消息), broker 没有可提供的消息, 那么, 如果 consumer 想要及时的收到最新消息, 就要不停的发送请求信息. 这会对 broker 造成很大的压力. 而长轮询就是为了解决这个问题. 当客户端发送一个请求, 但 broker 没有可提供但消息时, broker 会将这个请求暂存起来. 当commitLog 写入新消息时, 会进行分发, 在分发时, 长轮询方法会将这个新的消息通知给 PullRequestHoldService, 这个服务会判断新存盘的消息应该通知给哪个暂存的请求, 并发送过去.
private void doReput() {
            if (this.reputFromOffset < DefaultMessageStore.this.commitLog.getMinOffset()) {
                log.warn("The reputFromOffset={} is smaller than minPyOffset={}, this usually indicate that the dispatch behind too much and the commitlog has expired.",
                    this.reputFromOffset, DefaultMessageStore.this.commitLog.getMinOffset());
                this.reputFromOffset = DefaultMessageStore.this.commitLog.getMinOffset();
            }
            for (boolean doNext = true; this.isCommitLogAvailable() && doNext; ) {

                if (DefaultMessageStore.this.getMessageStoreConfig().isDuplicationEnable()
                    && this.reputFromOffset >= DefaultMessageStore.this.getConfirmOffset()) {
                    break;
                }

                SelectMappedBufferResult result = DefaultMessageStore.this.commitLog.getData(reputFromOffset);
                if (result != null) {
                    try {
                        this.reputFromOffset = result.getStartOffset();

                        for (int readSize = 0; readSize < result.getSize() && doNext; ) {
                            //从CommitLog中获取一个DispatchRequest,拿到一份需要进行转发的消息,也就是从commitlog中读取的。
                            DispatchRequest dispatchRequest =
                                DefaultMessageStore.this.commitLog.checkMessageAndReturnSize(result.getByteBuffer(), false, false);
                            int size = dispatchRequest.getBufferSize() == -1 ? dispatchRequest.getMsgSize() : dispatchRequest.getBufferSize();

                            if (dispatchRequest.isSuccess()) {
                                if (size > 0) {
                                    //分发CommitLog写入消息,会从一个列表中获取到两个分发, 他们分别会分发到 consumerQueue 和 IndexFile 到写入方法.
                                    DefaultMessageStore.this.doDispatch(dispatchRequest);
                                    // 判断是否要开启长轮询. 
                                    if (BrokerRole.SLAVE != DefaultMessageStore.this.getMessageStoreConfig().getBrokerRole()
                                        && DefaultMessageStore.this.brokerConfig.isLongPollingEnable()) {
                                        //唤醒NotifyMessageArrivingListener的arriving方法,也就是开始了长轮训
                                        DefaultMessageStore.this.messageArrivingListener.arriving(dispatchRequest.getTopic(),
                                            dispatchRequest.getQueueId(), dispatchRequest.getConsumeQueueOffset() + 1,
                                            dispatchRequest.getTagsCode(), dispatchRequest.getStoreTimestamp(),
                                            dispatchRequest.getBitMap(), dispatchRequest.getPropertiesMap());
                                    }

                                    this.reputFromOffset += size;
                                    readSize += size;
                                    if (DefaultMessageStore.this.getMessageStoreConfig().getBrokerRole() == BrokerRole.SLAVE) {
                                        DefaultMessageStore.this.storeStatsService
                                            .getSinglePutMessageTopicTimesTotal(dispatchRequest.getTopic()).incrementAndGet();
                                        DefaultMessageStore.this.storeStatsService
                                            .getSinglePutMessageTopicSizeTotal(dispatchRequest.getTopic())
                                            .addAndGet(dispatchRequest.getMsgSize());
                                    }
                                } else if (size == 0) {
                                    this.reputFromOffset = DefaultMessageStore.this.commitLog.rollNextFile(this.reputFromOffset);
                                    readSize = result.getSize();
                                }
                            } else if (!dispatchRequest.isSuccess()) {

                                if (size > 0) {
                                    log.error("[BUG]read total count not equals msg total size. reputFromOffset={}", reputFromOffset);
                                    this.reputFromOffset += size;
                                } else {
                                    doNext = false;
                                    // If user open the dledger pattern or the broker is master node,
                                    // it will not ignore the exception and fix the reputFromOffset variable
                                    if (DefaultMessageStore.this.getMessageStoreConfig().isEnableDLegerCommitLog() ||
                                        DefaultMessageStore.this.brokerConfig.getBrokerId() == MixAll.MASTER_ID) {
                                        log.error("[BUG]dispatch message to consume queue error, COMMITLOG OFFSET: {}",
                                            this.reputFromOffset);
                                        this.reputFromOffset += result.getSize() - readSize;
                                    }
                                }
                            }
                        }
                    } finally {
                        result.release();
                    }
                } else {
                    doNext = false;
                }
            }
        }
public CompletableFuture<PutMessageStatus> submitFlushRequest(AppendMessageResult result, PutMessageResult putMessageResult,
                                                                  MessageExt messageExt) {
        // Synchronization flush
        if (FlushDiskType.SYNC_FLUSH == this.defaultMessageStore.getMessageStoreConfig().getFlushDiskType()) {
            final GroupCommitService service = (GroupCommitService) this.flushCommitLogService;
            if (messageExt.isWaitStoreMsgOK()) {
                GroupCommitRequest request = new GroupCommitRequest(result.getWroteOffset() + result.getWroteBytes(),
                        this.defaultMessageStore.getMessageStoreConfig().getSyncFlushTimeout());
                service.putRequest(request);
                return request.future();
            } else {
                service.wakeup();
                return CompletableFuture.completedFuture(PutMessageStatus.PUT_OK);
            }
        }
        // Asynchronous flush
        else {
            if (!this.defaultMessageStore.getMessageStoreConfig().isTransientStorePoolEnable()) {
                flushCommitLogService.wakeup();
            } else  {
                commitLogService.wakeup();
            }
            return CompletableFuture.completedFuture(PutMessageStatus.PUT_OK);
        }
    }

存储安全

rocketMq 为了保证 broker 端不丢数据. 可以设置数据存盘和主从同步按同步的方式进行. 也就是会在等待上述步骤完成后, 才会给客户端回复成功. rocketMq 的主从同步时通过 dledger 基于 raft 协议完成的. 启用时, 会将commitLog 文件完全交给 dledger 处理.

3 生产者

生产者(producer)负责生产消息, 并依据负载均衡策略向不同的 broker 中的某个 queue 发送消息, 生产者可以推送多种消息类型.

  1. 普通消息: 普通的消息,它将轮询的向一个一个队列发送数据.
  2. 顺序消息: 由于一个 topic 有多个队列, 且它们并不在一个 borker 上. 正常的一串消息发送出去后, 消费端的消费顺序一定会产生乱序. 为了达成有序, 当使用顺序消息时, 生产者会将一串消息都推送到一个特定的队列, 而消费端需要配合开启顺序消费(不会从多个队列拉去消息, 而是一个队列拉空了, 再去拉去另一个队列). 这样可以达成局部有序(一组有序的消息消费顺序是有序的, 但中间可能会夹杂一些其他的消息).
  3. 广播消息: 一个广播消息会被所有监听者收到
  4. 延迟消息: 延迟消息的实现就是将相同延迟时间的消息推送到一个特定的队列中. 这样就可以顺序的拉去并做延迟发送.所以在 rocketMq 的开源版本中, 延迟队列的延迟时间是固定的.
  5. 过滤消息: rocketMq 的过滤是在 broker 端完成的. 过滤是通过 Tag 这个属性完成的. rocketMq 支持使用 sql 表达式完成过滤. 但 tag 属性只支持一个, 所以不能支持复杂但过滤情况.
  6. 批量消息: 一次发送一批消息. rocketMq 对批量消息做了限制, 它们不能超过 4M. 而且批量消息只能是普通消息. 对于批量消息, rocketMq 会将它打包成一个 batchMessage, 将它当作一个普通消息一样发送.
  7. 事务消息: rocketMq 生产者的消息安全是通过事务消息保证的. 事务消息的消息流程: 1. 发送 half 消息给 broker (用来确认 broker 可用). 2. 收到 half 消息存入Half Message Queue 并回复. 3. 向 rocketMq 发送状态信息(回滚、提交) 4. broker 收到消息, 回滚: 丢弃 half 消息, 不再处理. 提交: 将 half 消息存入目标 topic 下的队列, 等待消费. 不回复: 过一段时间后, 再次询问对该消息的处理方案, 如果询问多次仍不知道处理方案(这里每次询问的时间间隔都会延长), 消息会进入死信队列. 事务消息可以保证 producer 和 broker 之间的原子性.

负载均衡策略

对于生产者, 它的负载均衡策略就是从目标 topic 中选择一个队列发送. 简单来说, 就是向所有队列逐一发送.

public MessageQueue selectOneMessageQueue() {
        int index = this.sendWhichQueue.getAndIncrement();
        int pos = Math.abs(index) % this.messageQueueList.size();
        if (pos < 0)
            pos = 0;
        return this.messageQueueList.get(pos);
    }

当发送失败时, 如果时同步发送, 会进行重试. 当开启 sendLatencyFaultEnable. 会刨除失败的 broker, 尝试其他 broker 中的队列.
其他消息类型发送的负载均衡都相同(顺序消息不会做, 它会发送到一个特定队列中).

4 消费者

消费者有两种消费模式, push、pull. 但不论是哪种模式对于 rocketmq 都是 pull 消息. consumer 会根据负载均衡策略向目标队列发送拉去数据请求. 请求是异步发送的. 当收到数据时, 会根据 cunsumer 配置的接收数据的监听模式 (orderly / concurrent). orderly 会顺序监听收到的数据,也就是说, 在处理完从某一个队列收到的数据前, 不会消费其他队列收到的数据. 而concurrent 模式会并发的消费, 但一组消息但数量不会超过32条,超过是会将一组收到的消息分割(broker 发送过来的消息不是一条一条的, 而是一组. 这是为了提高效率). 所以如果需要使用顺序消息, 那么消费者的消费模式也要配合使用 orderly. 但由于 orderly 消费是“单线程”的(消费时会开启锁, 并发收到的消息也只能以顺序的模式消费), 所以处理效率肯定不及并发模式.

负载均衡策略

在消费者启动后, 会启动一个专用的线程, 不停的做 rebalance. 这里 rocketMq 使用了 countDownLaunch 做 rebalance 的检测判定. 如果收到重平衡通知( countDown() 方法被调用),那么会立刻重负载. 如果没有, 最高延迟 2 秒钟后 (默认, 可配置), 会重新 rebalance.
这里的负载均衡有多种模式, 根据配置不同, 有不同的算法分配消费者. 当没有重新计算前, 一个 consumer 消费的队列是固定的. 在一个消费组中, 一个 consumer 可能消费多个队列, 但一个队列只会有一个 consumer. 所以如果消费者多余队列, 那么多出来的服务是不能消费到消息的.

private void rebalanceByTopic(final String topic, final boolean isOrder) {
        switch (messageModel) {
            //广播模式,不需要进行负载。每个消费者都要消费。只需要更新负载信息。
            case BROADCASTING: {
                Set<MessageQueue> mqSet = this.topicSubscribeInfoTable.get(topic);
                if (mqSet != null) {
                    boolean changed = this.updateProcessQueueTableInRebalance(topic, mqSet, isOrder);
                    if (changed) {
                        this.messageQueueChanged(topic, mqSet, mqSet);
                        log.info("messageQueueChanged {} {} {} {}",
                            consumerGroup,
                            topic,
                            mqSet,
                            mqSet);
                    }
                } else {
                    log.warn("doRebalance, {}, but the topic[{}] not exist.", consumerGroup, topic);
                }
                break;
            }
            //集群模才需要负载均衡
            case CLUSTERING: {
                //订阅的主题
                Set<MessageQueue> mqSet = this.topicSubscribeInfoTable.get(topic);
                //客户端ID, 通过主题,获取所有的broker的address,轮询的取一个broker地址,再向broker中获取所有的客户端ID
                List<String> cidAll = this.mQClientFactory.findConsumerIdList(topic, consumerGroup);
                if (null == mqSet) {
                    if (!topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
                        log.warn("doRebalance, {}, but the topic[{}] not exist.", consumerGroup, topic);
                    }
                }

                if (null == cidAll) {
                    log.warn("doRebalance, {} {}, get consumer id list failed", consumerGroup, topic);
                }

                if (mqSet != null && cidAll != null) {
                    List<MessageQueue> mqAll = new ArrayList<MessageQueue>();
                    mqAll.addAll(mqSet);
                    //排序后才能保证消费者负载策略最终一致。
                    Collections.sort(mqAll);
                    Collections.sort(cidAll);
                    //MessageQueue的负载策略,有五种实现类
                    AllocateMessageQueueStrategy strategy = this.allocateMessageQueueStrategy;

                    List<MessageQueue> allocateResult = null;
                    try {
                        //按负载策略进行分配,返回当前消费者实际订阅的MessageQueue集合。
                        allocateResult = strategy.allocate(
                            this.consumerGroup,
                            this.mQClientFactory.getClientId(),
                            mqAll,
                            cidAll);
                    } catch (Throwable e) {
                        log.error("AllocateMessageQueueStrategy.allocate Exception. allocateMessageQueueStrategyName={}", strategy.getName(),
                            e);
                        return;
                    }

                    Set<MessageQueue> allocateResultSet = new HashSet<MessageQueue>();
                    if (allocateResult != null) {
                        allocateResultSet.addAll(allocateResult);
                    }
                    // 负载均衡完成后,检测是否有改变,需要重新做负载均衡
                    boolean changed = this.updateProcessQueueTableInRebalance(topic, allocateResultSet, isOrder);
                    if (changed) {
                        log.info(
                            "rebalanced result changed. allocateMessageQueueStrategyName={}, group={}, topic={}, clientId={}, mqAllSize={}, cidAllSize={}, rebalanceResultSize={}, rebalanceResultSet={}",
                            strategy.getName(), consumerGroup, topic, this.mQClientFactory.getClientId(), mqSet.size(), cidAll.size(),
                            allocateResultSet.size(), allocateResultSet);
                        this.messageQueueChanged(topic, mqSet, allocateResultSet);
                    }
                }
                break;
            }
            default:
                break;
        }
    }
  • 消费端重新负载均衡时, 是可能导致消息重复消费问题的(在记录消费位置之前).
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值