Netty-服务端接收不到客户端发送消息案例

得益于高性能、低时延的优势,Netty被广泛应用于物联网领域,用于海量终端设备的协议接入、消息收发和数据处理。
当服务端出现性能瓶颈或者阻塞时,就会导致终端设备连接超时和掉线,引发各种问题,因此在物联网场景下,一定要防止服务端代码因为编码不当导致的意外阻塞,进而无法处理终端请求消息。


   服务端接收不到客户端发送消息案例
   堆栈分析以及解决方案
   NioEventLoop线程防死锁策略
   总结


服务端接收不到客户端发送消息案例

     服务端使用Netty构建,接受客户端请求消息,然后下发给后端其他系统,最后返回应答给客户端。系统运行一段时间后发现服务端收不到客户端发送的信息,导致业务终端。
     服务端运行一段时间后相关日志:
Netty-服务端接收不到客户端发送消息案例_第1张图片
     服务端每隔一段时间会接收不到消息,隔一段时间后恢复,然后又没消息,周而复始。根据实际情况排查了客户端没法消息的情况以及CPU资源导致的周期性阻塞还有内存导致频繁GC引起业务线程暂停。


堆栈分析以及解决方案

     排除上述原因,有可能是Netty的NioEventLoop线程阻塞,导致TCP缓冲区的数据没有及时读取,故障期间采用服务端的线程堆栈进行分析,结果如图:
Netty-服务端接收不到客户端发送消息案例_第2张图片
     如图,Netty的NioEventLoop读到消息后,调用业务线程池执行业务逻辑时,RejectedExecution出现异常,由于后续的业务逻辑由NioEventLoop线程执行,可以判定业务拒绝策略选择了CallerRunsPolicy策略(在业务线程池消息队列满了以后,由调用方的线程来执行当前任务),然后NioEventLoop在执行业务任务时发生了阻塞,导致NioEventLoop线程无法处理网络读写消息,因此这段时间内服务端没有消息接入,当阻塞状态回复之后,就可以继续处理I/O信息。
     相关源码:

public class IotCarsServerHandler extends ChannelInboundHandlerAdapter {
     
    static AtomicInteger sum  = new AtomicInteger(0);
    static ExecutorService executorService = new ThreadPoolExecutor(1,3,30, TimeUnit.SECONDS,
            new ArrayBlockingQueue<Runnable>(1000),new ThreadPoolExecutor.CallerRunsPolicy());
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
     
        System.out.println(new Date() + "--> Server receive client message : " + sum.incrementAndGet());
        executorService.execute(()->
        {
     
            ByteBuf req = (ByteBuf) msg;
            //其它业务逻辑处理,访问数据库
            if (sum.get() % 100 == 0 || (Thread.currentThread()== ctx.channel().eventLoop()))
                try
                {
     
                    //访问数据库,模拟偶现的数据库慢,同步阻塞15秒
                    TimeUnit.SECONDS.sleep(15);
                }
                catch (Exception e)
                {
     
                    e.printStackTrace();
                }
            //转发消息,此处代码省略,转发成功之后返回响应给终端
            ctx.writeAndFlush(req);
        });
    }

}

     如果后端业务逻辑处理慢,会导致业务线程池阻塞队列积压,当积压达到容量上限时,JDK会抛出RejectedExecutionException异常,由于业务设置了CallerRunsPolicy策略,就会由调用方线程NioEventLoop执行业务逻辑,最终导致NioEventLoop线程被阻塞,无法读取到请求信息。
     当系统拥塞时,可采用丢弃当前消息或流控的方式避免问题进一步恶化,防止阻塞Netty的NioEventLoop线程。


NioEventLoop线程防死锁策略

     由于ChannelHandler是业务代码和Netty框架交汇的地方,ChannelHandler里面的业务逻辑通常由NioEventLoop线程执行,因此防止业务代码阻塞NioEventLoop线程就显得非常重要,常见阻塞情况:

  1. 直接在ChannelHandler中写可能导致程序阻塞的代码,如数据库操作,第三方服务调用,中间件服务调用,同步获取锁,Sleep等
  2. 切换到业务线程池或者业务消息队列做异步处理时发生了阻塞,比如阻塞队列,同步获取锁等。

     一般情况下,推荐采用业务处理线程和I/O线程分离的策略,原因如下:

  1. 充分利用多核并行处理能力:I/O线程和业务线程分离,双方可以并行处理网络I/O和业务逻辑,充分利用多核的并行计算能力,提升性能。
  2. 故障隔离:后端业务线程池处理各种类型的业务消息,有些是I/O密集型,有些是CPU密集型,有些是纯内存计算型,不同的业务处理时延,以及发生故障的概率都是不同的。如果把业务线程和I/O线程合并,可能存在如下问题:
    2.1 某类业务处理慢,阻塞I/O线程,导致其他处理较快的业务消息的响应无法及时发送出去。
    2.2 即便同类业务,使用同一个I/O线程同时处理业务逻辑和I/O读写,如果请求信息的业务逻辑处理较慢,同样会导致相应消息无法及时发送出去。
    3.可维护性:I/O线程和业务线程分离之后,双方职责单一,有利于代码维护和问题定位。如果合并在一起执行,当RPC调用时延增大时,到底是网络问题、I/O线程问题还是业务逻辑问题不好确定,问题定位难度大。如在业务线程中访问缓存或者数据库偶尔时延增大,就会导致I/O线程被阻塞,这时确切定位问题就很麻烦。
    Netty-服务端接收不到客户端发送消息案例_第3张图片

总结

     当Netty服务端接收不到消息时,首先需要检查是客户端没有发送数据到服务端,还是服务端没有读取消息。导致服务端无法读取消息的原因很多,常见包括GC导致的应用暂停、服务端的NioEventLoop线程被意外阻塞等。通过网络I/O线程和业务逻辑线程分离,可以实现双方的并行处理,提升系统的可靠性。对于用户而言,在编写代码时始终需要考虑NioEventLoop线程是否被业务代码阻塞,只有消除所有可能导致阻塞点,才能保证程序稳定运行。

     下一篇:Netty-NioEventLoop线程工作机制

你可能感兴趣的:(Netty,java,netty,多线程)