3.x 线程模型
In:
- Work线程将消息从TCP缓冲区读取到SocketChannel的接收缓冲区;
- Work线程负责生成相应事件,触发事件向上执行,调度到ChannelPipeline中;
- Word线程调度Handler链的对应方法;
- 业务方法放在线程池中并发执行(ExecutionHandler维护),work线程返回;
Out:
- Netty将写操作封装成写事件,触发事件向下传播;
- 业务线程将编码后的消息Push到发送队列中,然后返回;
- Netty的work线程从队列中取出消息,调用SocketChannel的write方法写入通道。
分析:
Outbound操作由业务线程执行,通常业务会使用线程池并行处理业务消息,这就意味着在某一个时刻会有多个业务线程同时操作ChannelHandler,我们需要对ChannelHandler进行并发保护,通常需要加锁。如果同步块的范围不当,可能会导致严重的性能瓶颈,这对开发者的技能要求非常高,降低了开发效率;
Outbound操作过程中,例如消息编码异常,会产生Exception,它会被转换成Inbound的Exception并通知到ChannelPipeline,这就意味着业务线程发起了Inbound操作!它打破了Inbound操作由I/O线程操作的模型,如果开发者按照Inbound操作只会由一个I/O线程执行的约束进行设计,则会发生线程并发访问安全问题。由于该场景只在特定异常时发生,因此错误非常隐蔽!一旦在生产环境中发生此类线程并发问题,定位难度和成本都非常大。
4.x 线程模型
In and Out:
- I/O线程NioEventLoop从SocketChannel中读取数据报,将ByteBuf投递到ChannelPipeline,触发ChannelRead事件;
- I/O线程NioEventLoop调用ChannelHandler链,直到将消息投递到业务线程,然后I/O线程返回,继续后续的读写操作;
- 业务线程调用ChannelHandlerContext.write(Object msg)方法进行消息发送;
- 如果是由业务线程发起的写操作,ChannelHandlerInvoker将发送消息封装成Task,放入到I/O线程NioEventLoop的任务队列中,由NioEventLoop在循环中统一调度和执行。放入任务队列之后,业务线程返回;
- I/O线程NioEventLoop调用ChannelHandler链,进行消息发送,处理Outbound事件,直到将消息放入发送队列,然后唤醒Selector,进而执行写操作。
分析:
Netty 4采用了串行化设计理念,从消息的读取、编码以及后续Handler的执行,始终都由I/O线程NioEventLoop负责,这就意外着整个流程不会进行线程上下文的切换。
每当有一个新的客户端接入,则从NioEventLoop线程组中顺序获取一个可用的NioEventLoop,当到达数组上限之后,重新返回到0,通过这种方式,可以基本保证各个NioEventLoop的负载均衡。一个客户端连接只注册到一个NioEventLoop上,这样就避免了多个I/O线程去并发操作它。
(1)EventLoopGroup继承自EventExecutorGroup,而EventExecutorGroup可以将它看成是一个线程池,而EventLoopGroup只不过是扩展了了线程池的功能,加入了一些事件的处理。例如channel的注册等。
(2)EventLoop继承自EventExecutor,而一个EventExecutor可以将其看成是一个线程池中的执行线程,而EventLoop扩展了EventExecutor的主循环,一般情况下线程池中的线程要做的就是不断的从任务队列里面取Task,然后执行他们,而EventLoop中加入了select以及IO的处理过程。
(3)每一个channel都注册到了其中一个EventLoop的selector上面,在这个channel上面产生的所有IO事件的处理都将会是在这个EventLoop中进行处理。