3、RocketMQ之RPC通信功能原理分析

RocketMQ Remoting模块也是整个代码中比较简单的一个模块,在掌握基本的Netty知识之后就可以尝试对源码进行简单的阅读分析,我也是结合源码分析来进行Netty应用的学习。

该模块主要有两个类 NettyRemotingServer 和 NettyRemotingClient 。分别对应服务端和客户端,其中服务端主要在Broker和NameService中使用。

Push中存在的Pull请求是通过“长轮询”方式达到Push 效果的方法,长轮询方式既有 Pull 的优点,又兼 Push 方式的实时性。“长轮询”方式通过 Client 端和 Server 端的配合,达到既拥有 Pull 的优点,又能达到保证实时性的目的。

Push 方式 Server 端接收到消息后,主动把消息推送给 Client 端,实时性高 对于 个提供队列服务的 Server 来说,用 Push 方式主动推送有很多弊端:首先是加大 Server 端的 作量,进而影响 Server 的性能;其次, Client处理能力各不相同, Client 的状态不受 Server 控制,如果 Client 不能及时处理Server 推送过来的消息,会造成各种潜在问题。

Pull 方式是 Client 端循环地从 Server 端拉取消息,主动权在 Client 手里,自己拉取到一定量消息后,处理妥当了再接着取Pull 方式的问题是循环拉取消息的间隔不好设定,间隔太短就处在一个 忙等”的状态,浪费资源;每个Pull 的时间间隔太长 Server 端有消息到来时 有可能没有被及时处理。

@Override
public void run() {
    while (!this.isStopped()) {
        if (this.brokerController.getBrokerConfig().isLongPollingEnable()) {
            this.waitForRunning(5 * 1000);
        } else {
            this.waitForRunning(this.brokerController.getBrokerConfig().getShortPollingTimeMills());
        }
        long beginLockTimestamp = this.systemClock.now();
        this.checkHoldRequest();
        long costTime = this.systemClock.now() - beginLockTimestamp;
        if (costTime > 5 * 1000) {
            log.warn("PullRequestHoldService: check hold pull request cost {}ms", costTime);
        }
    }
}

protected void checkHoldRequest() {
    for (String key : this.pullRequestTable.keySet()) {
        String[] kArray = key.split(TOPIC_QUEUEID_SEPARATOR);
        if (2 == kArray.length) {
            String topic = kArray[0];
            int queueId = Integer.parseInt(kArray[1]);
            final long offset = this.brokerController.getMessageStore().getMaxOffsetInQueue(topic, queueId);
            this.notifyMessageArriving(topic, queueId, offset);
        }
    }
}

从Broker 的源码【PullRequestHoldService#run】中可以看出,服务端接到新消息请求后, 如果队列里没有新消息并不急于返回,通过一个循环不断查看状态,每次 waitForRunning一段时间 (默认是5秒)然后再 Check。默认情况下当 Broker 一直没有新消息, 第三次check 的时候, 等待时间超过 Request 里面的 BrokerSuspen MaxTimeMi11is 就返回空结果。在等待的过程中, Broker 收到了新的消息后会直接调用notifyMessageArriving 函数返回请求结果。“长轮询”的核心是, Broker HOLD 住客户端过来的请求一小段时间,在这个时间内有新消息到达,就利用现有的连接立刻返回消息给 Consumer。 “长轮询”的主动权还是掌握在 Consumer 手中, Broker 即使有大量消息积压,也不会主动推送给Consumer。

长轮询方式的局限性,是在HOLD住Consumer请求的时候需要占用资源,它适合用在消息队列这种客户端连接数可控的场景中。

1.NettyRemotingAbstract

public abstract class NettyRemotingAbstract {
	public void processMessageReceived(ChannelHandlerContext ctx, RemotingCommand msg) {
        if (msg != null) {
            switch (msg.getType()) {
                case REQUEST_COMMAND:
                    processRequestCommand(ctx, msg);
                    break;
                case RESPONSE_COMMAND:
                    processResponseCommand(ctx, msg);
                    break;
                default:
                    break;
            }
        }
    }
    public void processRequestCommand(final ChannelHandlerContext ctx, final RemotingCommand cmd) {
    	//根据 requestCode 获取与当前请求匹配的 处理器processor
	    Pair<NettyRequestProcessor, ExecutorService> matched = this.processorTable.get(cmd.getCode());
	    Pair<NettyRequestProcessor, ExecutorService> pair = 
	    								null == matched ? this.defaultRequestProcessorPair : matched;
	    int opaque = cmd.getOpaque();
	    ...
	    Runnable run = buildProcessRequestHandler(ctx, cmd, pair, opaque);
	    ...
	    RequestTask requestTask = new RequestTask(run, ctx.channel(), cmd);
	    pair.getObject2().submit(requestTask);
	}
	
	
	private Runnable buildProcessRequestHandler(ChannelHandlerContext ctx, RemotingCommand 
							cmd,Pair<NettyRequestProcessor, ExecutorService> pair, int opaque) {
	    return () -> {
	        Exception exception = null;
	        RemotingCommand response;
	        String remoteAddr = RemotingHelper.parseChannelRemoteAddr(ctx.channel());
	        ...// 执行处理器processor相关处理逻辑
	        response = pair.getObject1().processRequest(ctx, cmd);
	        ...
	        writeResponse(ctx.channel(), cmd, response);
	    };
	}
}

1.1.NettyRemotingServer

public class NettyServerHandler extends SimpleChannelInboundHandler<RemotingCommand> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, RemotingCommand msg) {
        int localPort = RemotingHelper.parseSocketAddressPort(ctx.channel().localAddress());
        NettyRemotingAbstract remotingAbstract = NettyRemotingServer.this.remotingServerTable.get(localPort);
        if (localPort != -1 && remotingAbstract != null) {
            remotingAbstract.processMessageReceived(ctx, msg);
            return;
        }
        RemotingHelper.closeChannel(ctx.channel());
    }
	// 只有服务端初始化 requestCode & 处理器processor 之间的对应关系
	@Override
    public void registerProcessor(int requestCode, NettyRequestProcessor processor, ExecutorService executor) {
        ExecutorService executorThis = executor;
        if (null == executor) {
            executorThis = this.publicExecutor;
        }
        Pair<NettyRequestProcessor, ExecutorService> pair = new Pair<>(processor, executorThis);
        this.processorTable.put(requestCode, pair);
    }
}

1.2.NettyRemotingClient

public class NettyRemotingClient extends NettyRemotingAbstract implements RemotingClient {
	class NettyClientHandler extends SimpleChannelInboundHandler<RemotingCommand> {
	    @Override
	    protected void channelRead0(ChannelHandlerContext ctx, RemotingCommand msg) throws Exception {
	        processMessageReceived(ctx, msg);
	    }
	}
}

1、NettyRemotingClient

NettyRemotingClient模块是应用方接触最多的核心通信模块类。该类主要负责客户端与服务端之间数据的通信,即服务端读取的队列的数据都是通过NettyRemotingClient通知到应用方消费监听器。

在RocketMQ RPC通信是通过Netty实现的,在初始化Bootstrap中众多handler中其中NettyClientHandler是最后的一个handler,同时也是Netty框架与应用框架间数据流转的最后一站。

2、NettyClientHandler

每一条待消费的数据都是通过该Handler投递给消费者实例的。对于顺序消息来说发送时已经获悉需要发送的队列,而在broker端也知道该队列对应的消费者实例信息,从而保证broker端精准投递消息至特定的消费者实例。

class NettyClientHandler extends SimpleChannelInboundHandler<RemotingCommand> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, RemotingCommand msg) throws Exception {
        processMessageReceived(ctx, msg);//msg就是服务端响应的数据
    }
}
public void processMessageReceived(ChannelHandlerContext ctx, RemotingCommand msg) throws Exception {
    final RemotingCommand cmd = msg;
    if (cmd != null) {
        switch (cmd.getType()) {
            case REQUEST_COMMAND:
                processRequestCommand(ctx, cmd);
                break;
            case RESPONSE_COMMAND:
                processResponseCommand(ctx, cmd);
                break;
            default:
                break;
        }
    }
}

public void processResponseCommand(ChannelHandlerContext ctx, RemotingCommand cmd) {
    final int opaque = cmd.getOpaque();
    final ResponseFuture responseFuture = responseTable.get(opaque);//#2
    if (responseFuture != null) {
        responseFuture.setResponseCommand(cmd);//#3
        responseTable.remove(opaque);
        if (responseFuture.getInvokeCallback() != null) {
            executeInvokeCallback(responseFuture);//#1
        } 
    } 
}
  1. 步骤1调用NettyRemotingClient抽象父类NettyRemotingAbstract#executeInvokeCallback方法。
  2. 步骤2中的responseTable在生产者发送时会初始化完毕,并且设置key为opaque,value为ResponseFuture。
  3. 将在broker上获取的待消费数据赋值给ResponseFuture。用于消费实例继续消费。

4、ResponseFuture

public class ResponseFuture {
    private final int opaque;
    private final Channel processChannel;
    private final long timeoutMillis;
    private final InvokeCallback invokeCallback;

    private final SemaphoreReleaseOnlyOnce once;

    private volatile RemotingCommand responseCommand;// 存在消息体body

    public ResponseFuture(Channel channel, int opaque, long timeoutMillis, InvokeCallback invokeCallback,
        SemaphoreReleaseOnlyOnce once) {
        this.opaque = opaque;
        this.processChannel = channel;
        this.timeoutMillis = timeoutMillis;
        this.invokeCallback = invokeCallback;
        this.once = once;
    }

    public void executeInvokeCallback() {
        if (invokeCallback != null) {
            if (this.executeCallbackOnlyOnce.compareAndSet(false, true)) {
                invokeCallback.operationComplete(this);//#1
            }
        }
    }
}
  1. 步骤1中的invokeCallback在生产者发送消息时赋值给ResponseFuture。

5、MQClientAPIImpl

private void pullMessageAsync(String addr,RemotingCommand request,long timeoutMillis,PullCallback pullCallback) {

    this.remotingClient.invokeAsync(addr, request, timeoutMillis, new InvokeCallback() {
        @Override
        public void operationComplete(ResponseFuture responseFuture) {
            RemotingCommand response = responseFuture.getResponseCommand();//#1
            if (response != null) {
                try {
                PullResult pullResult = MQClientAPIImpl.this.processPullResponse(response, addr);
                assert pullResult != null;
                pullCallback.onSuccess(pullResult);//DefaultMQPushConsumerImpl#PullCallback#onSuccess
            } 
        }
    });
}
  1. 步骤1中从ResponseFuture中获取待消费的数据。

6、DefaultMQPushConsumerImpl#PullCallback

PullCallback pullCallback = new PullCallback() {
    @Override
    public void onSuccess(PullResult pullResult) {
        if (pullResult != null) {
           pullResult = DefaultMQPushConsumerImpl.this.pullAPIWrapper.processPullResult(pullRequest.getMessageQueue(), pullResult,subscriptionData);
            switch (pullResult.getPullStatus()) {
                case FOUND:
                   boolean dispatchToConsume = processQueue.putMessage(pullResult.getMsgFoundList());//#1
                   DefaultMQPushConsumerImpl.this.consumeMessageService.submitConsumeRequest(//#2
                            pullResult.getMsgFoundList(),processQueue,pullRequest.getMessageQueue(),dispatchToConsume);
                    break;
                case NO_NEW_MSG:
                case NO_MATCHED_MSG:
                    ...
                case OFFSET_ILLEGAL:
                    ...
                default:
                    break;
            }
        }
    }
};
  1. 步骤1:将需要消费的数据存储到processQueue#msgTreeMap中。
  2. 步骤2:触发消费线程池执行。

7、ConsumeMessageOrderlyService

public class ConsumeMessageOrderlyService implements ConsumeMessageService {
	private final ThreadPoolExecutor consumeExecutor;
	
	public ConsumeMessageOrderlyService(DefaultMQPushConsumerImpl defaultMQPushConsumerImpl,
        MessageListenerOrderly messageListener) {
        this.defaultMQPushConsumerImpl = defaultMQPushConsumerImpl;
        this.messageListener = messageListener;

        this.defaultMQPushConsumer = this.defaultMQPushConsumerImpl.getDefaultMQPushConsumer();
        this.consumerGroup = this.defaultMQPushConsumer.getConsumerGroup();
        this.consumeRequestQueue = new LinkedBlockingQueue<Runnable>();

        this.consumeExecutor = new ThreadPoolExecutor(
            this.defaultMQPushConsumer.getConsumeThreadMin(),
            this.defaultMQPushConsumer.getConsumeThreadMax(),
            1000 * 60,
            TimeUnit.MILLISECONDS,
            this.consumeRequestQueue,
            new ThreadFactoryImpl("ConsumeMessageThread_"));
        this.scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryImpl("ConsumeMessageScheduledThread_"));
    }

	public void submitConsumeRequest(//#3
        final List<MessageExt> msgs,
        final ProcessQueue processQueue,
        final MessageQueue messageQueue,
        final boolean dispathToConsume) {
        if (dispathToConsume) {
            ConsumeRequest consumeRequest = new ConsumeRequest(processQueue, messageQueue);
            this.consumeExecutor.submit(consumeRequest);
        }
    }

	class ConsumeRequest implements Runnable {
	    private final ProcessQueue processQueue;
	    private final MessageQueue messageQueue;
	
	    public ConsumeRequest(ProcessQueue processQueue, MessageQueue messageQueue) {
	        this.processQueue = processQueue;
	        this.messageQueue = messageQueue;
	    }
	    ...
	
	    @Override
	    public void run() {
	        final Object objLock = messageQueueLock.fetchLockObject(this.messageQueue);//#3
	        synchronized (objLock) {
	   if (MessageModel.BROADCASTING.equals(ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.messageModel())
	                || (this.processQueue.isLocked() && !this.processQueue.isLockExpired())) {
	                ...
	                final beginTime = System.currentTimeMillis();
	                for (boolean continueConsume = true; continueConsume; ) {
	                    ...
	                    List<MessageExt> msgs = this.processQueue.takeMessages(consumeBatchSize);//#1
	                    ...
	                    status = messageListener.consumeMessage(Collections.unmodifiableList(msgs), context);//#2
	                    continueConsume = ConsumeMessageOrderlyService.this.processConsumeResult(msgs, status, context, 
	                    	this);
	                }
	            }
	        }
	    }
	}
}

consumeExecutor:消息消费的线程池。多线程处理不同messageQueue是不需要控制并发,同一个messageQueue多线程消费需要严格控制消费顺序。

processQueue:msgTreeMap(存放拉取到的消息)、queueOffsetMax(消费到的最大offset)、msgSize(ProcessQueue中的总消息长度)、msgCount(ProcessQueue中的总消息数量)、msgAccCnt(表示broker端还有多少消息未被消费)。

TreeMap类型的msgTreeMap集合保证同一个messageQueue中多条消息是按照QueueOffset进行排序。

processQueue中msgTreeMap集合的读写是分开的,即存在不同线程对其进行读写。ReentrantReadWriteLock类型的lockTreeMap保证一个messageQueue某一时刻只能允许一个线程操作msgTreeMap。

for(boolean continueConsume = true;continueConsume)这个循环很有意思,表示当前MessageQueue是否可以持续消费,是否可以继续消费的标准有两个:

  1. consumeOffset的提交,如果提交成功,就可以继续消费,直到processQueue中msgTreeMap为空。
  2. consumeOffset没有正常提交.比如其它状态:SUSPEND_CURRENT_QUEUE_A_MOMENT。

如果返回到是SUSPEND_CURRENT_QUEUE_A_MOMENT,这个里面会把本次消费的消息重新放回到ProcessQueue中的msgTreeMap然后由下个线程接着消费,为什么说是下个线程呢? 因为这里会把continueConsume设置为false,返回时,执行当前ConsumeRequest进行消费的线程就会完成消费,下一个获取当前MessageQueue的锁的线程接着处理当前的processQueue(不同MessageQueue对应的processQueue永远是同一个对象)。

  1. 步骤1:从processQueue获取待消费的数据。
  2. 步骤2:回调消费者实例监听器消费队列中的数据。
  3. 步骤3:不同消费线程针对同一个messageQueue中的消息处理是通过锁控制消费顺序的。

7.1、消费状态

public boolean processConsumeResult(
    final List<MessageExt> msgs,
    final ConsumeOrderlyStatus status,
    final ConsumeOrderlyContext context,
    final ConsumeRequest consumeRequest
) {
    boolean continueConsume = true;
    long commitOffset = -1L;
    if (context.isAutoCommit()) {
        switch (status) {
            case COMMIT:
            case ROLLBACK:
            case SUCCESS:
                commitOffset = consumeRequest.getProcessQueue().commit();
                this.getConsumerStatsManager().incConsumeOKTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(), msgs.size());
                break;
            case SUSPEND_CURRENT_QUEUE_A_MOMENT:
                this.getConsumerStatsManager().incConsumeFailedTPS(consumerGroup, 
                										consumeRequest.getMessageQueue().getTopic(), msgs.size());
                if (checkReconsumeTimes(msgs)) {//重试消费次数判断
                    consumeRequest.getProcessQueue().makeMessageToConsumeAgain(msgs);
                    this.submitConsumeRequestLater(consumeRequest.getProcessQueue(),consumeRequest.getMessageQueue(),
                        context.getSuspendCurrentQueueTimeMillis());
                    continueConsume = false;
                } else {
                    commitOffset = consumeRequest.getProcessQueue().commit();
                }
                break;
            default:
                break;
        }
    } 
	...
    return continueConsume;
}

如果消息是自动提交:对于SUSPEND_CURRENT_QUEUE_A_MOMENT返回状态,如果没有达到重试次数则重新将当前ConsumeRequest放入到第六章阐述的任务队列中。

makeMessageToConsumeAgain:重新将当前消息放到TreeMap中,根据TreeMap特性保证重试消费也能保证顺序消费。

private void submitConsumeRequestLater(ProcessQueue processQueue,MessageQueue messageQueue,long suspendTimeMillis) {
    long timeMillis = suspendTimeMillis;//默认为-1
    if (timeMillis == -1) {
        timeMillis = this.defaultMQPushConsumer.getSuspendCurrentQueueTimeMillis();//默认为1000
    }

    if (timeMillis < 10) {
        timeMillis = 10;
    } else if (timeMillis > 30000) {
        timeMillis = 30000;
    }

    this.scheduledExecutorService.schedule(new Runnable() {

        @Override
        public void run() {
            ConsumeMessageOrderlyService.this.submitConsumeRequest(null, processQueue, messageQueue, true);
        }
    }, timeMillis, TimeUnit.MILLISECONDS);
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值