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使用是非常好的例子,可以直接參考整合到自己項目。