条件:
①构造EventLoopGroup的时候,boss组和worker组大小都设置为1。
// 配置服务端的NIO线程组
EventLoopGroup bossGroup = new NioEventLoopGroup(1,
new BossThreadFactory("TimeServer-boss", Thread.MAX_PRIORITY));
EventLoopGroup workerGroup = new NioEventLoopGroup(1,
new WorkerThreadFactory("TimeServer-worker", Thread.MAX_PRIORITY));
②在ServerHandler中添加sleep
这里是Worker的NioEventLoop线程执行的回调。然后在client端调用的时候会阻塞
③代码逻辑
服务端接收请求的时候返回当前时间戳返回给客户端,同时模拟繁忙业务处理,sleep了10s才返回时间戳给client端。异步启动两个客户端并向server端发起连接请求。
结果:
Server端日志:
17:24:17.084 [TimeServer-worker-3-1] INFO net.skyscanner.tea.io.netty.case3.TimeServerHandler - ====== The time server receive order : {} thread: [QUERY TIME ORDER, Thread[TimeServer-worker-3-1,10,main]]
17:24:27.102 [TimeServer-worker-3-1] INFO net.skyscanner.tea.io.netty.case3.TimeServerHandler - ====== The time server receive order : {} thread: [QUERY TIME ORDER, Thread[TimeServer-worker-3-1,10,main]]
说明Server在17:24:17.084接到请求,记录当前时间戳,然后会花10s去处理,并把该时间戳返回给客户端,然后在17:24:27.102又接收到了请求,然后又会花10s去处理
client端日志:
17:24:27.112 [TimeClient-1-1] INFO net.skyscanner.tea.io.netty.case3.TimeClientHandler - ====== Now is : {} thread: [Thu Mar 19 17:24:17 CST 2020, Thread[TimeClient-1-1,10,main]]
17:24:37.107 [TimeClient-2-1] INFO net.skyscanner.tea.io.netty.case3.TimeClientHandler - ====== Now is : {} thread: [Thu Mar 19 17:24:27 CST 2020, Thread[TimeClient-2-1,10,main]]
client端在17:24:27.112接收到服务器返回的第一个请求,在17:24:37.107接收到第二次请求
分析:
说明因为服务端处理读事件之后,要做的业务逻辑太重,同时,该读事件的处理是Worker的NioEventLoop的线程去执行的。因为上面构造的时候参数是1,所以只有一个Worker的NioEventLoop处理读事件(客户端发起的请求),如果阻塞了的话,就会影响其他client端的读(OP_READ),但是不会影响其他客户端的连接(OP_ACCEPT),因为OP_ACCEPT是由Boss组处理的。
服务端处理read操作的线程很宝贵,所以如果业务逻辑重,就开启业务线程来执行,就不会阻塞了。
这里有两种方式来使用业务线程池,第一种是在initChannel,为pipeline添加handler的时候可以添加EventExecutor,也可以在业务handler的channelRead回调中指定jdk的线程池去执行。注意在添加EventExecutor的时候,不要使用NioEventLoopGroup,因为它的模式是单线程模式,达不到多线程并行执行的的目的,具体解释看下文第4部分。
注意1:
如果channelRead中不是用ctx的writeAndFlush,这个时候如果用UnorderedThreadPoolExecutor或者jdk线程池,本次都不会触发flush,导致的现象就是,用netty的echo的case的时候,如果测试用户线程池异步写的时候可能在client端拿不到Server端回写的结果。
那什么时候才会flush呢?
调试发现,如果我们用自己的线程池去处理channelRead,在AbstractNioByteChannel的read方法中:
read:163, AbstractNioByteChannel$NioByteUnsafe->fireChannelRead
read:168, AbstractNioByteChannel$NioByteUnsafe->fireChannelReadComplete
这两个操作是串行处理的,但是fireChannelRead是在自己的线程异步执行的,所以在执行到fireChannelReadComplete的时候fireChannelRead还没有执行,而flush操作是在业务handler的channelReadComplete中执行的,所以这个时候还没有数据可供flush,但是下次再来数据触发这个流程就可以把上次的数据flush出去了。
还有种方式就是在业务handler的channelRead中直接writeAndFlush把数据flush出去。
注意2:
这里的write或者flush的具体操作还是由worker线程接管的,虽然发起ctx.write/flush/writeAndFlush的操作是在用户开启的线程中发起的
write:704, AbstractChannelHandlerContext
write:723, AbstractChannelHandlerContext
write:811, AbstractChannelHandlerContext
executor:115, AbstractChannelHandlerContext->这里取的是该context的pipeline的channel,就是NioSocketChannel,它对应的eventLoop就是Worker组的一个NioEventLoop
write:818, AbstractChannelHandlerContext->这里会走到else分支,因为不在EventLoop中
write:823, AbstractChannelHandlerContext->这里会为写动作创建一个task
write:825, AbstractChannelHandlerContext->用NioEventLoop去执行,又是熟悉的操作了,先addTask,然后判断是否inEventLoop,这里因为是业务线程,所以不在EventLoop中,然后判断该NioEventLoop是否已经启动,没启动的话就启动,然后就可以取这个task进行执行了。
所以可以看到,无论是write还是flush操作,即使中间的处理数据部分使用了业务线程,真正写或者刷的操作还是由Worker的NioEventLoop线程执行的,netty不会把这些核心操作让用户线程去操作。
我们知道一个NioEventLoop可能会管理多个channel,当channel的个数超过NioEventLoopGroup中的NioEventLoop个数的时候,那么就需要一个NioEventLoop管理一个以上的channel,比如channel A和channel B,那么这里如果一个channel的读或者写出现问题发生阻塞,因为只有一个线程,这些操作都是串行的,所以会大大影响性能。
所以像上面说的,我们要尽可能把业务处理放到业务线程池中。但即使这样也要关注netty的内部线程池处理读写的情况,防止发生阻塞而影响吞吐。
条件:
①构造EventLoopGroup的时候,boss组和worker组大小都设置为1。
②在ServerHandler中添加sleep
这里是Worker的NioEventLoop线程执行的回调。然后在client端调用的时候会阻塞
EventLoopGroup bossGroup = new NioEventLoopGroup(1,
new BossThreadFactory("TimeServer-boss", Thread.MAX_PRIORITY));
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup)
分析:
和上面情况1相比,这种情况更为恶劣。至少上面Boss和Worker是分开的,所以Boss处理连接的时候(响应OP_ACCEPT)不会受到业务处理以及读事件(OP_READ)的影响。但是这里只有一个group,所以无论是OP_ACCEPT还是OP_READ还是业务处理都是它来处理,如果它被阻塞的话,客户端的连接(OP_ACCEPT)都会受到影响。
NioEventLoop的runAllTasks会传入要执行task的时间
run:462, NioEventLoop->传入执行任务的时间,因为前面processSelectedKeys是执行IO操作的,然后记录该IO操作的时间,然后根据ioRatio计算出要给非IO操作的时间,然后传进来
runAllTasks:393, SingleThreadEventExecutor->这里如果有任务的话,会取出来执行,然后每16次取任务之后判断一下执行task的时间,如果超过了方法入参传进来的执行任务时间,就不再执行任务了,即使还有未完成的任务,因为到了该执行IO的时候了。
在为pipeline添加handler的时候,可以指定线程池来执行业务逻辑,这里要使用UnorderedThreadPoolExecutor,不要使用NioEventLoopGroup。因为在add的时候:
addLast:409, DefaultChannelPipeline
addLast:210, DefaultChannelPipeline->这里会把传过来的group中取一个executor绑定到context中
newContext:120, DefaultChannelPipeline
childExecutor:138, DefaultChannelPipeline->这里有个map维护group和EventExecutor的关系,一个group只对应一个EventExecutor,就是group.next(),且只会初始化一次,所以
childExecutor:140, DefaultChannelPipeline
next:82, UnorderedThreadPoolEventExecutor->这里返回的是自身,而它自身是一个jdk的线程池,执行的时候也是按照jdk线程池模型去执行的,所以可以利用多个线程。而NioEventLoopGroup的话,参考https://blog.csdn.net/xxcupid/article/details/104782845,它会利用NioEventLoop的特性,先添加task,然后启动该NioEventLoop的线程,然后用该NioEventLoop的线程去执行这些task,所以其实是单线程,而不是多线程。
addLast:226, DefaultChannelPipeline->这里用上面提到的context中的executor来执行,这个executor就是UnorderedThreadPoolExecutor,它是一个jdk的可调度线程池,这里的period是0,所以不是周期任务,只执行一次