在一些业务处理ChannelHandler中执行一些耗时任务或者其他任务时,通常不会使用Nio线程来做,而是调度到其他的业务线程池去做。
线程池ThreadPoolExecutor的
CallerRunsPolicy策略:如果当前线程池任务队列满了,新增的任务交由调度前的线程来执行。
如果后端业务逻辑处理慢,则会导致业务线程池阻塞队列积压,当积压达到容量上限时,JDK会抛出RejectedExecutionException异常,由于业务设置了CallerRunsPolicy策略,就会由调用方的线程NioEventLoop执行业务逻辑,最终导致 NioEventLoop线程被阻塞,无法读取请求消息。
造成的现象就是,如果NioEventLoop线程阻塞,则一段时间内客户端的网络消息IO会丢失,这段时间服务端接收不到消息。从阻塞状态恢复后就又可以接收到消息了。
除了JDK线程池异常处理策略使用不当,有些业务人员喜欢自己写阻塞队列,当队列满时,向队列加入新的消息会阻塞当前线程,直到消息能够加入队列。案例中的车联网服务端真实业务代码就有此类问题:当转发到下游系统发生某些故障时,会导致业务定义的阻塞队列无法弹出消息进行处理,当队列积压满时,就会阻塞Netty的NIO线程,而且无法自动恢复。
【防止NioEventLoop挂死】
防止Nio线程挂死的方法就是,NioEventLoop只处理网络I/O,业务消息调度给业务线程池处理。
【netty内多线程最佳实践】
(1)
创建两个NioEventLoopGroup,用于逻辑隔离NIO Acceptor和 NIOI/O线程。
(2)尽量不要在ChannelHandler中启动用户线程(解码后用于将POJO消息派发到后端业务线程的除外)。
(3)
解码要放在NIO线程调用的解码Handler中进行,不要切换到用户线程完成消息的解码,相对于切换线程的开销收益更大。
(4)
如果业务逻辑操作非常简单(纯内存操作),没有复杂的业务逻辑计算,也没有可能会导致线程被阻塞的磁盘操作、数据库操作、网络操作等,可以直接在NIO线程上完成业务逻辑编排,不需要切换到用户线程,相对于切换线程的开销收益更大。
(5)如果业务逻辑复杂,不要在 NIO线程上完成,建议将解码后的POJO消息封装成任务,
派发到业务线程池中由业务线程执行,以保证NIO线程尽快被释放,处理其他的1/O操作。
推荐的线程数量计算公式有以下两种。
(1)公式1:线程数量=(线程总时间/瓶颈资源时间)x瓶颈资源的线程并行数。
(2)公式2:QPS= 1000/线程总时间×线程数。