netty應用中一次DefaultChannelPromise異常的思考 1. 問題的產生與現象 2 問題分析 3 問題的解決

1. 問題的產生與現象

  近日在開發一個基於netty的網絡應用中,需要考慮一個channel斷鏈後重連的場景。在ChannelInboundHandlerAdapter中,當channelInactive函數被觸發時提示斷鏈,並在channelInactive函數中執行斷鏈重連函數reConnectChannel。
  reConnectChannel在指定次數與間隔內執行重連操作,重連操作的代碼大致如下:

public boolean reConnectChannel(LoginUserInfo lui, boolean isThirdParty) throws Exception {
... ...
Bootstrap bootstrap = ncs.getNewBootstrap();
        ChannelFuture future = bootstrap.connect(lui.getSingalServerIp(), lui.getSingalServerPort()).sync();
        Channel channel = future.channel();
        lui.setSignalChannel(channel);
        log.info("create channel:channel={}",channel);
... ...
}

  代碼很簡單,就是每次生成一個新的Bootstrap(NioSocketChannel),然後用這個新的Bootstrap做connect操作去連接服務器。其中getNewBootstrap函數代碼如下:

public Bootstrap getNewBootstrap() {
        Bootstrap bs = new Bootstrap();
        bs.group(connectGroup).channel(NioSocketChannel.class);
        bs.option(ChannelOption.TCP_NODELAY, true);
        bs.option(ChannelOption.SO_KEEPALIVE, true);
        bs.option(ChannelOption.SO_REUSEADDR, true);
        bs.option(ChannelOption.SO_SNDBUF, 128);
        bs.handler(clientChannelInitializer);
        return bs;
    }

  可以看到,每個新生成的Bootstrap註冊到一個connectGroup(線程數爲10)中的某個線程,由這個線程對該Bootstrap(NioSocketChannel)進行事件監聽。
  在執行過程,打開netty的debug打印,可以看到一次正常重連(沒連上)的打印如下:

2019-12-18 10:09:43.372 DEBUG 16380 --- [nioEventLoopGroup-2-4] io.netty.handler.logging.LoggingHandler  : [id: 0xf00db0ac] REGISTERED
2019-12-18 10:09:43.373 DEBUG 16380 --- [nioEventLoopGroup-2-4] io.netty.handler.logging.LoggingHandler  : [id: 0xf00db0ac] CONNECT: /172.16.249.205:9907
2019-12-18 10:09:43.374 DEBUG 16380 --- [nioEventLoopGroup-2-4] io.netty.handler.logging.LoggingHandler  : [id: 0xf00db0ac] CLOSE
2019-12-18 10:09:43.375 DEBUG 16380 --- [nioEventLoopGroup-2-4] io.netty.handler.logging.LoggingHandler  : [id: 0xf00db0ac] UNREGISTERED

  而一次異常重連的打印如下:

2019-12-18 10:42:42.889 DEBUG 12940 --- [nioEventLoopGroup-2-1] io.netty.handler.logging.LoggingHandler  : [id: 0xc120d598] REGISTERED
2019-12-18 10:42:42.889 ERROR 12940 --- [nioEventLoopGroup-2-1] c.k.v.o.service.pojo.LoginUserInfo       : channel reconnect throw exception:DefaultChannelPromise@447aeba5(incomplete)

  在10次重連中會出現一到兩次DefaultChannelPromise的異常。產生的現象就是當網絡恢復之後,本應只有一個Bootstrap(channel)重連成功,但之前在斷鏈重連中拋出異常的Bootstrap(如上例中的[id: 0xc120d598])也重連成功了。

2 問題分析

  分析上面的問題與現象,可以發現拋出異常的Bootstrap所執行的線程(如上例中nioEventLoopGroup-2-1)與執行reConnectChannel的線程是同一個。
  我們知道在NIO編程中,一個NioSocketChannel(Bootstrap)被註冊到一個select線程中(connectGroup中的某個線程),由這個select線程監聽這個NioSocketChannel的事件,一個select線程可能監聽處理多個NioSocketChannel的事件,並調用事件響應函數,這是一個序列化的過程,即只有處理完這個NioSocketChannel的監聽到的事件,纔會處理下一個NioSocketChannel的事件。
  我們在一個NioSocketChannel的斷鏈事件處理中新建了一個NioSocketChannel去進行重連,如果這個新的NioSocketChannel和原斷鏈的NioSocketChannel被同一個select線程所監聽,而我們又使用bootstrap.connect().sync()在那同步連接,則會出現死鎖阻塞:sync()需要select線程監聽處理到連接事件才退出,而select線程監聽處理連接事件則需要調用sync()的channelInactive函數先退出。
  netty檢測到這種死鎖條件,因而拋出DefaultChannelPromise異常,而該NioSocketChannel未被CLOSE和UNREGISTERED,因而在網絡恢復後又重新進行了連接。

3 問題的解決

   如果繼續使用bootstrap.connect().sync()方法,則需要保證執行bootstrap.connect().sync()方法的線程與bootstrap所註冊的select線程不爲同一線程。可將bootstrap.connect().sync()函數在一新建的線程中處理。
   如果不使用bootstrap.connect().sync()方法,則可改寫爲bootstrap.connect().addListener()方法實現異步等待連接事件。

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