做内网长连接服务这段时间,一直在跟 Netty 打交道。很多人觉得 Netty 照着 Demo 写一遍,绑定端口,加几个 Handler 就能跑通。Demo 确实没问题,一旦放到真实业务,处理粘包拆包、空闲检测、异常输出、内存回收,各种奇奇怪怪的现象就冒出来。
网上大部分教程集中在讲解 Reactor、Pipeline 这些基础概念,很少讲业务跑起来之后会遇到的现实麻烦。这里记录几个实际调参、排错遇到的真实情况。
先说粘包拆包。这几乎是每一个上手 Netty 的人都会踩的坎。
一开始图省事,直接没有加任何解码器,业务里面直接拿到 ByteBuf 就转字符串解析。本地测试发送几条短消息一切正常。压测的时候就出状况,多条报文拼到一块,偶尔一条报文被切割成两段,业务解析直接报格式异常。
很多人第一反应就是加上 LineBasedFrameDecoder,按换行分割报文。但我们协议并不是换行符结尾,于是选用 LengthFieldBasedFrameDecoder。这里有个很容易踩的细节,很多人复制网上示例参数,没仔细核对报文协议里面长度字段的偏移、长度所占字节。
// 示例,参数一定要对照自己的协议文档
new LengthFieldBasedFrameDecoder(1024 * 1024,0,4,0,4)
maxFrameSize 设置过小是隐形坑。业务偶尔会有大包报文,超过限制 Netty 直接抛出 TooLongFrameException,连接直接断开。日志如果没有捕获这个异常,只会看到客户端莫名断开,看不到任何有效报错。还有,如果把这个解码器放在 pipeline 的靠后位置,前面处理器已经消费了部分 ByteBuf,解码器拿到残缺数据,会直接彻底乱掉。解码器必须放在 pipeline 的靠前位置。
然后是 ByteBuf 内存泄露,这个坑折磨我很久。
Netty 的堆外直接内存,如果没有正确释放,不会抛异常,不会立刻崩溃。服务跑几天,内存占用一点点往上抬,GC 看不出明显异常,直到操作系统杀掉进程。
按照规则,入站消息经过 SimpleChannelInboundHandler,会自动释放引用。但如果用普通 ChannelInboundHandlerAdapter,拿到 ByteBuf 之后,如果业务逻辑里面把这个 buf 丢到异步线程去处理,当前处理线程返回之后引用就释放了。异步线程再去读这个 ByteBuf,就会触发 IllegalReferenceCountException。
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf buf = (ByteBuf) msg;
// 错误:交给异步线程,当前方法走完 Netty 自动释放
executor.submit(() -> parse(buf));
}
这种偶现问题,开发环境很难复现,线上高并发才会频繁出现。解决办法手动 retain,业务处理完成之后再 release。一定要保证 retain 和 release 成对。排查内存泄露可以开启 JVM 参数 -Dio.netty.leakDetection.level=PARANOID,只适合测试环境,生产不要开,性能损耗很大。
再聊聊空闲检测和连接管理。
很多业务加上 IdleStateHandler,想把闲置太久的客户端连接关闭。
new IdleStateHandler(60,0,0)
很多人写完之后发现,连接明明已经闲置,却没有被关闭。原因很简单:IdleStateHandler 统计的是 IO 事件,不是业务心跳。如果操作系统缓冲区还有残留数据,就算业务层没有收发业务报文,IO 层面不算空闲。还有一个容易忽略点,这个 handler 必须放到 pipeline 的前面。放到业务处理器后面,空闲事件有可能被前面处理器吞掉,userEventTriggered 根本收不到事件。
另外一个现实问题:大量客户端异常掉线,没有正常发送关闭指令,TCP 半开连接。Netty 这边 Channel 还处于存活状态,一直占着资源。单纯依靠 IdleStateHandler 还不够,业务层最好自己实现心跳报文,不能完全依赖 TCP 底层断开。
还有线程模型踩过的坑。
Netty NIO 线程池(NioEventLoop)里面的线程是绝对不能执行耗时阻塞操作。
早期写代码图省事,直接在 channelRead 里面做数据库查询、http 远程调用。一旦有慢查询,NIO 工作线程被阻塞,整个 EventLoop 全部卡住,所有连接的读写全部停滞。现象就是部分客户端请求响应极慢,新连接可以建立,但是消息不处理。
刚开始排查还以为是网络问题,后来打印线程堆栈才看到 NIO 线程卡在数据库驱动代码上。
所有耗时业务逻辑,必须丢到业务自定义线程池,绝对不要占用 Netty IO 线程。这里也有一个细节,业务线程池不要用无界队列,客户端疯狂推送消息的时候,任务无限堆积,直接 OOM。
最后说异常处理。
很多人写 Netty 代码,没有重写 exceptionCaught。发生 IO 异常、报文解析异常的时候,异常悄悄吃掉。日志什么输出都没有,只看到连接断开。线上出问题完全没有线索。
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
log.error("channel exception",cause);
ctx.close();
}
不要觉得写了这个就万事大吉。部分异常发生在业务异步线程里面,不会走到这个回调,异步内部必须自己 try‑catch,否则直接把业务线程池搞崩。
写 Netty,看懂 API 只是入门。真正麻烦的不是 Demo 能够跑通,而是处理半包、内存回收、线程隔离、异常兜底这些边角场景。很多问题本地小流量完全看不到,只有线上多连接、高低速客户端混杂的环境才会暴露。遇到诡异现象,优先怀疑 ByteBuf 引用计数、handler 的摆放顺序、IO 线程有没有被阻塞。

502

被折叠的 条评论
为什么被折叠?



