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

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


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


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

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


堆栈分析以及解决方案

     排除上述原因,有可能是Netty的NioEventLoop线程阻塞,导致TCP缓冲区的数据没有及时读取,故障期间采用服务端的线程堆栈进行分析,结果如图:
在这里插入图片描述
     如图,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服务端接收不到消息时,首先需要检查是客户端没有发送数据到服务端,还是服务端没有读取消息。导致服务端无法读取消息的原因很多,常见包括GC导致的应用暂停、服务端的NioEventLoop线程被意外阻塞等。通过网络I/O线程和业务逻辑线程分离,可以实现双方的并行处理,提升系统的可靠性。对于用户而言,在编写代码时始终需要考虑NioEventLoop线程是否被业务代码阻塞,只有消除所有可能导致阻塞点,才能保证程序稳定运行。

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

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是一个简单的 Netty 服务端发送消息客户端Java 代码示例: ```java import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.codec.string.StringDecoder; import io.netty.handler.codec.string.StringEncoder; public class NettyServer { private int port; public NettyServer(int port) { this.port = port; } public void run() throws Exception { NioEventLoopGroup bossGroup = new NioEventLoopGroup(); NioEventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast(new StringDecoder()); pipeline.addLast(new StringEncoder()); pipeline.addLast(new NettyServerHandler()); } }); ChannelFuture future = bootstrap.bind(port).sync(); future.channel().closeFuture().sync(); } finally { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } public static void main(String[] args) throws Exception { new NettyServer(8080).run(); } private class NettyServerHandler extends SimpleChannelInboundHandler<String> { @Override protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception { // 接收客户端消息 System.out.println("接收客户端消息:" + msg); // 向客户端发送消息 ctx.writeAndFlush("服务端接收消息:" + msg); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.close(); } } } ``` 在上述代码中,我们创建了一个 Netty 服务端,并且在 `NettyServerHandler` 类中实现了处理客户端消息的方法 `channelRead0`。在该方法中,我们打印了客户端发送消息,并且使用 `ctx.writeAndFlush` 向客户端发送了一个回应消息。需要注意的是,在 Netty 中,所有的网络操作都是异步的,因此我们需要使用 `ChannelFuture` 来等待异步操作完成。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值