RocketMQ源碼分析 producer啓動以及消息發送流程,producer與broker網絡交互過程,發送和接收方式總結

1.proucer發送消息本質就是把消息通過網絡發送給服務器(broker),broker接收到消息存儲應答producer成功。

要發送的消息在producer包裝爲Message,到了broker端變爲MessageExtBrokerInner,producer客戶端的啓動和發送比較簡單,貼個大圖

上圖就是producer的啓動以及消息的發送。

生產中producer發送消息通常採用同步發送,如果消息大則採用異步發送(消息大的情況比較少) 。

本質producer就是個netty client,它的主要功能就是連接上namesvr:9876,獲取到broker、topic、queue等信息緩存到producer本地,實際緩存到DefaultMQProducerImpl.topicPublishInfoTable,然後發送消息的時候選擇一個broker和消息隊列進行發送。

這裏主要說下rmq消息發送的設計。

rmq對於client發送消息,都會最終包裝爲一個遠程命令RemotingCommand,這是個命令模式。

RemotingCommand.code存放的是發送的命令,broker收到後知道客戶端要做什麼

RemotingCommand.body 存放的是真實要發送的消息。

RemotingCommand.customHeader,是自定義的命令頭,是CommandCustomHeader

CommandCustomHeader有許多具體子類,比如producer發送消息是SendMessageRequestHeader,消費端拉取消息是PullMessageRequestHeader,CommandCustomHeader存放自定義的消息信息,比如SendMessageRequestHeader存放的是topic、queueid。

code、body、customHeader組成了RemotingCommand是發送數據。在broker收到消息後,解析RemotingCommand,根據命令進入不同的處理器處理,broker響應消息也是放在RemotingCommand返回。

producer發送消息到broker之間的交互:

 producer啓動的大圖中已經寫了client server端的inboud outbound處理器,它們的執行順序參考上圖,除了對RemotingCommand編解碼的NettyEncoder NettyDecoder,重要的就是圖中標註綠色的方法。在server端決定由哪個線程池處理processor,在client端決定處理響應結果,對於同步調用喚醒等待發送線程,對於異步調用,執行回調結果。

重點說下org.apache.rocketmq.remoting.netty.NettyRemotingAbstract.processMessageReceived(ChannelHandlerContext, RemotingCommand)方法,調用堆棧如圖,正好分別是客戶端和服務端的inbound事件中調用。

對於服務端,接收到的是請求,執行NettyRemotingAbstract.processRequestCommand(ChannelHandlerContext, RemotingCommand),根據RemotingCommand.code即請求命令從緩存NettyRemotingAbstract.processorTable獲取對應的執行處理器和線程池,那麼processorTable是怎麼有值的呢?對於服務端是在broker啓動過程中添加的命令和處理器、線程池,查看堆棧如下

對於client端,是在客戶端啓動時候添加處理器

 

服務端處理請求NettyRemotingAbstract.processRequestCommand(ChannelHandlerContext, RemotingCommand)

把pair.getObject1()即處理器(比如SendMessageProcessor)保存到新鍵的task中,然後把task提交到pair.getObject2()即線程池中,這樣就可以併發處理客戶端請求。task被提交線程池後就會執行,繼而執行對應的處理器(比如SendMessageProcessor)而且對於所有的處理器都是使用同一個這樣模式,這樣設計的很巧妙。

客戶端處理響應NettyRemotingAbstract.processResponseCommand(ChannelHandlerContext, RemotingCommand)

客戶端同步發送

發送入口是org.apache.rocketmq.remoting.netty.NettyRemotingAbstract.invokeSyncImpl(Channel, RemotingCommand, long)方法,創建一個ResponseFuture保存到NettyRemotingAbstract.responseTable併發集合,然後發送數據到broker,接着ResponseFuture同步等待broker響應結果。看如下代碼

public RemotingCommand invokeSyncImpl(final Channel channel, final RemotingCommand request,
    final long timeoutMillis)
    throws InterruptedException, RemotingSendRequestException, RemotingTimeoutException {
    final int opaque = request.getOpaque();//每次請求都生成個唯一鍵,用於匹配原ResponseFuture

    try {
        final ResponseFuture responseFuture = new ResponseFuture(channel, opaque, timeoutMillis, null, null);//發送前創建ResponseFuture,功能就類似jdk的future
        this.responseTable.put(opaque, responseFuture);//把ResponseFuture保存到responseTable集合
        final SocketAddress addr = channel.remoteAddress();//服務器地址
        channel.writeAndFlush(request).addListener(new ChannelFutureListener() {//writeAndFlush就是發送,發送成功後執行ChannelFutureListener監聽器,注意發送成功不代表就接收到了響應,只是表示數據已經發送出去了
            @Override
            public void operationComplete(ChannelFuture f) throws Exception {
            	//監聽器實際是由netty的線程執行,而非當前發送線程,因此發生成功直接return,並不會進入到finally。發送失敗也不會進入finally。如果不懂得netty源碼,這裏代碼容易讓人混亂的
                if (f.isSuccess()) {//發送成功回調
                    responseFuture.setSendRequestOK(true);//成功則返回,responseTable存在該responseFuture,在線程[ServerHouseKeepingService]執行NettyRemotingAbstract.scanResponseTable()進行處理
                    return;
                } else {
                    responseFuture.setSendRequestOK(false);//發送失敗回調
                }
				//發送失敗,就不需要處理響應了,因此把ResponseFuture從responseTable集合移除
                responseTable.remove(opaque);
                responseFuture.setCause(f.cause());
                responseFuture.putResponse(null);
                log.warn("send a request command to channel <" + addr + "> failed.");
            }
        });

        RemotingCommand responseCommand = responseFuture.waitResponse(timeoutMillis);//當前線程等待timeoutMillis時間被喚醒
        if (null == responseCommand) {
            if (responseFuture.isSendRequestOK()) {
                throw new RemotingTimeoutException(RemotingHelper.parseSocketAddressAddr(addr), timeoutMillis,
                    responseFuture.getCause());//接收broker響應超時
            } else {
                throw new RemotingSendRequestException(RemotingHelper.parseSocketAddressAddr(addr), responseFuture.getCause());//發送失敗
            }
        }

        return responseCommand;
    } finally {
        this.responseTable.remove(opaque);//如果超時or發送失敗,則把ResponseFuture從responseTable集合移除
    }
}

接收入口就是NettyRemotingAbstract.processResponseCommand(ChannelHandlerContext, RemotingCommand),從responseTable併發集合根據opaque(每次請求都會生成一個唯一值,用來查找匹配原請求)取出發送之前保存的ResponseFuture,把broker響應結果保存到ResponseFuture並喚醒發送線程。

/*
     * 處理響應結果,通過opaque匹配到原ResponseFuture,對於同步則喚醒等待線程,對於異步則執行回調。
     */
    public void processResponseCommand(ChannelHandlerContext ctx, RemotingCommand cmd) {
        final int opaque = cmd.getOpaque();//opaque是每次請求都生成一個,該值是遞增的,根據該唯一值匹配到原ResponseFuture
        final ResponseFuture responseFuture = responseTable.get(opaque);//發送之前會把ResponseFuture保存到responseTable集合,這樣在處理響應的時候就可以獲取到
        if (responseFuture != null) {//說明ResponseFuture還沒被scanResponseTable()操作從集合中移除
            responseFuture.setResponseCommand(cmd);//把響應結果保存到responseFuture

            responseTable.remove(opaque);//移除,如果不移除就內存泄漏了

            if (responseFuture.getInvokeCallback() != null) {//異步調用纔有回調
                executeInvokeCallback(responseFuture);//異步發送執行這裏
            } else {
                responseFuture.putResponse(cmd);//喚醒同步發送的等待線程
                responseFuture.release();//同步發送,無功能,因爲ResponseFuture.once==null
            }
        } else {
            log.warn("receive response, but not matched any request, " + RemotingHelper.parseChannelRemoteAddr(ctx.channel()));
            log.warn(cmd.toString());
        }
    }

 這裏就有個問題,如果客戶端發送很快,但是服務端響應很慢或者不響應,同步調用大量超時,這樣就導致NettyRemotingAbstract.responseTable集合不斷增加(因爲每次調用都向該集合添加一個ResponseFuture),最終會導致oom,這裏就有可能內存泄漏了,怎麼解決呢?在本篇最開始貼的大圖中,有個計劃線程每1s執行一次NettyRemotingAbstract.scanResponseTable(),在該方法內,會把超時的ResponseFuture從NettyRemotingAbstract.responseTable集合移除,這樣就不會導致內存泄漏了。

客戶端異步調用

rmq4.4版本是沒有異步發送,入口org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl.send(Message, SendCallback, long)見圖

執行進入到org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl.sendDefaultImpl(Message, CommunicationMode, SendCallback, long)的時候發生模式是異步直接return null。

 異步發送暫時無法分析了。不過也能猜到大概,發送後直接返回,服務端什麼時候處理完畢了,吧響應返回,這樣客戶端在接收響應(在netty inbound事件)得到了ResponseFuture,然後調用回調即可。具體異步發送是有什麼bug呢才被去除呢?暫時不清楚

客戶端oneway調用

oneway是隻發不收,因此就沒有ResponseFuture。主要用於非業務型請求,比如REGISTER_BROKER、UPDATE_CONSUMER_OFFSET,具體入口是org.apache.rocketmq.remoting.netty.NettyRemotingAbstract.invokeOnewayImpl(Channel, RemotingCommand, long),onway方式雖然只發送,但是服務端照樣會把響應給返回,只是客戶端不處理響應(查了下REGISTER_BROKER、UPDATE_CONSUMER_OFFSET命令在服務端對onway發送方式的處理確實是會返回數據)。這樣是否有個問題?數據會緩存在網絡層,這樣是否會導致最終tcp接收緩衝區滿了?是不是服務端在writeAndFlush響應數據前應該判斷下isOnewayRPC()呢?答案是服務端對於oneway請求是不會的返回數據的,服務端處理請求的入口是org.apache.rocketmq.remoting.netty.NettyRemotingAbstract.processRequestCommand(ChannelHandlerContext, RemotingCommand),該方法執行步驟是先根據命令找到處理器和線程池,吧task提交到線程池處理,task執行的時候先執行處理器,處理器返回處理結果,接着判斷是否是oneway,oneway方式不返回數據,從下圖可以看出

 

總結說明:rokcetmq採用netty通訊,netty是個異步非阻塞,producer作爲一個netty client,發送本質就是個異步,但是做成同步情況就是異步轉同步的情況,那麼必須要採用接收線程(netty io 線程)必須匹配到原請求(producer發送線程),那麼通常的匹配方式就是根據唯一key從ConcurrentHashMap集合,可以找到原請求,如果找不到,說明則超時了,已經被掃描線程給移除了。我的工作中同步轉異步也是這樣做的,不同之處是用的wait和notify等待和喚醒,rmq採用的countdownlatch。

rmq的netty使用是非常好的例子,可以直接參考整合到自己項目。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章