Netty在RocketMQ中的應用----服務端

RocketMQ中角色有Producer、Comsumer、Broker和NameServer,它們之間的通訊是通過Netty實現的。在之前的文章RocketMQ是如何通訊的?中,對RocketMQt通訊進行了一些介紹,但是底層Netty的細節涉及的比較少,這一篇將作爲其中的一個補充。

服務端啓動

 ServerBootstrap childHandler =
            this.serverBootstrap.group(this.eventLoopGroupBoss, this.eventLoopGroupSelector)
                .channel(useEpoll() ? EpollServerSocketChannel.class : NioServerSocketChannel.class)
                .option(ChannelOption.SO_BACKLOG, 1024)
                .option(ChannelOption.SO_REUSEADDR, true)
                .option(ChannelOption.SO_KEEPALIVE, false)
                .childOption(ChannelOption.TCP_NODELAY, true)
                .childOption(ChannelOption.SO_SNDBUF, nettyServerConfig.getServerSocketSndBufSize())
                .childOption(ChannelOption.SO_RCVBUF, nettyServerConfig.getServerSocketRcvBufSize())
                .localAddress(new InetSocketAddress(this.nettyServerConfig.getListenPort()))
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    public void initChannel(SocketChannel ch) throws Exception {
                        ch.pipeline().addLast(
                            defaultEventExecutorGroup,
                            new NettyEncoder(),
                            new NettyDecoder(),
                            new IdleStateHandler(0, 0, nettyServerConfig.getServerChannelMaxIdleTimeSeconds()),
                            new NettyConnectManageHandler(),
                            new NettyServerHandler());
                    }
                });

        if (nettyServerConfig.isServerPooledByteBufAllocatorEnable()) {
            childHandler.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
        }

服務端的啓動和客戶端不同的一點,在於支持了Epoll模式。Epoll是支持高效網絡編程的的一把利器,通過它可以提高應用性能。關於Epoll可以參考下面這篇文章:linux下IO複用與Epoll詳解
如果啓用了Epoll(默認是啓用的),那麼selectotrGroup就會使用EpollEventLoopGroup。同時Channel也會使用EpollServerSocketChannel,而不是我們通常的NioServerSocketChannel。
如果啓用了對象池,那麼還會設置ALLOCATOR選項爲PooledByteBufAllocator.DEFAULT。
Netty服務端在配置完成後就開始綁定端口並監聽請求了。

 try {
            ChannelFuture sync = this.serverBootstrap.bind().sync();
            InetSocketAddress addr = (InetSocketAddress) sync.channel().localAddress();
            this.port = addr.getPort();
        } catch (InterruptedException e1) {
            throw new RuntimeException("this.serverBootstrap.bind().sync() InterruptedException", e1);
        }

接收消息

服務端處理消息也在NettyRemotingAbstract裏面的processMessageReceived進行處理,客戶端請求來的消息類型是REQUEST_COMMAND,所以我們關注processRequestCommand(ctx, cmd);方法即可。
首先,服務端根據請求來的REQUEST_CODE,找到之前註冊好了的處理器(Propcessor)。
然後,服務端判斷是否需要拒絕掉這次請求。拒絕的一般是在Producer發送消息時Broker的原因,例如系統頁緩存繁忙。
如果沒有拒絕這次請求,那麼就會將這次請求包裝成線程任務RequestTask供線程池調度執行。下面我們來看看這個包裝的線程任務。

            Runnable run = new Runnable() {
                @Override
                public void run() {
                    try {
                        RPCHook rpcHook = NettyRemotingAbstract.this.getRPCHook();
                        if (rpcHook != null) {
                            rpcHook.doBeforeRequest(RemotingHelper.parseChannelRemoteAddr(ctx.channel()), cmd);
                        }

                        final RemotingCommand response = pair.getObject1().processRequest(ctx, cmd);
                        if (rpcHook != null) {
                            rpcHook.doAfterResponse(RemotingHelper.parseChannelRemoteAddr(ctx.channel()), cmd, response);
                        }

                        if (!cmd.isOnewayRPC()) {
                            if (response != null) {
                                response.setOpaque(opaque);
                                response.markResponseType();
                                try {
                                    ctx.writeAndFlush(response);
                                } catch (Throwable e) {
                                    PLOG.error("process request over, but response failed", e);
                                    PLOG.error(cmd.toString());
                                    PLOG.error(response.toString());
                                }
                            } else {

                            }
                        }
                    } catch (Throwable e) {
                        PLOG.error("process request exception", e);
                        PLOG.error(cmd.toString());

                        if (!cmd.isOnewayRPC()) {
                            final RemotingCommand response = RemotingCommand.createResponseCommand(RemotingSysResponseCode.SYSTEM_ERROR, //
                                RemotingHelper.exceptionSimpleDesc(e));
                            response.setOpaque(opaque);
                            ctx.writeAndFlush(response);
                        }
                    }
                }

處理的流程非常清晰,先調用鉤子函數,如果需要對消息做一些預處理,可以註冊一個鉤子函數。然後就是Processor處理請求,處理完成後,繼續調用鉤子函數。如果是單向請求,就結束了。需要返回則調用ctx.writeAndFlush(response);刷出站消息。

超時responseFuture處理

請求時,responseFuture是放在responseTable中的。在客戶端請求時服務端(或者服務端請求客戶端時),在異步調用中,如果超時了,那麼responseTable中的responseFuture遲遲得不到處理,這顯然是不行的。同步調用因爲超時後會直接刪掉該responseFuture,但在極端情況下也可能來不及刪除被留在了responseTable中。因此我們需要新的定時任務,定期掃描該表。

 this.timer.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                try {
                    NettyRemotingServer.this.scanResponseTable();
                } catch (Exception e) {
                    log.error("scanResponseTable exception", e);
                }
            }
        }, 1000 * 3, 1000);

定時任務的週期是1秒處理一次,需要取出超時的responseFuture刪除。在異步調用中,如果有回調,還需要調用回調。回調同樣是通過包裝成線程,通過線程池調用。

public void scanResponseTable() {
        final List<ResponseFuture> rfList = new LinkedList<ResponseFuture>();
        Iterator<Entry<Integer, ResponseFuture>> it = this.responseTable.entrySet().iterator();
        while (it.hasNext()) {
            Entry<Integer, ResponseFuture> next = it.next();
            ResponseFuture rep = next.getValue();

            if ((rep.getBeginTimestamp() + rep.getTimeoutMillis() + 1000) <= System.currentTimeMillis()) {
                rep.release();
                it.remove();
                rfList.add(rep);
                PLOG.warn("remove timeout request, " + rep);
            }
        }

        for (ResponseFuture rf : rfList) {
            try {
                executeInvokeCallback(rf);
            } catch (Throwable e) {
                PLOG.warn("scanResponseTable, operationComplete Exception", e);
            }
        }
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章