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()方法实现异步等待连接事件。
网友评论