Netty2020 5-1——实战篇之Reactor模型的线程解析

模拟服务端繁忙

 

1.channelRead中添加sleep模拟长时间业务处理

条件:

①构造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不会把这些核心操作让用户线程去操作

2.Worker线程组个数大于1的情况

我们知道一个NioEventLoop可能会管理多个channel,当channel的个数超过NioEventLoopGroup中的NioEventLoop个数的时候,那么就需要一个NioEventLoop管理一个以上的channel,比如channel A和channel B,那么这里如果一个channel的读或者写出现问题发生阻塞,因为只有一个线程,这些操作都是串行的,所以会大大影响性能。

所以像上面说的,我们要尽可能把业务处理放到业务线程池中。但即使这样也要关注netty的内部线程池处理读写的情况,防止发生阻塞而影响吞吐。

3.如果Boss和Worker公用同一个group

条件:

①构造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)都会受到影响。

4.Ratio

NioEventLoop的runAllTasks会传入要执行task的时间

run:462, NioEventLoop->传入执行任务的时间,因为前面processSelectedKeys是执行IO操作的,然后记录该IO操作的时间,然后根据ioRatio计算出要给非IO操作的时间,然后传进来

runAllTasks:393, SingleThreadEventExecutor->这里如果有任务的话,会取出来执行,然后每16次取任务之后判断一下执行task的时间,如果超过了方法入参传进来的执行任务时间,就不再执行任务了,即使还有未完成的任务,因为到了该执行IO的时候了。

5.UnorderedThreadPoolExecutor

在为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,所以不是周期任务,只执行一次

 

你可能感兴趣的:(netty,netty)