Netty 是 Java 里写网络程序的事实标准:Dubbo、RocketMQ、Elasticsearch、gRPC-Java 底层都是它。这篇讲它在生产里真正会用到的部分。
一、线程模型:为什么 Netty 快
Netty 用的是 Reactor 主从多线程模型。
二、启动模板(服务端)
EventLoopGroup boss = new NioEventLoopGroup(1);
EventLoopGroup worker = new NioEventLoopGroup(); // 默认 CPU 核数 × 2
try {
ServerBootstrap b = new ServerBootstrap();
b.group(boss, worker)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childOption(ChannelOption.TCP_NODELAY, true)
.childHandler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel ch) {
ch.pipeline()
.addLast(new IdleStateHandler(60, 30, 0)) // 读空闲 60s / 写空闲 30s
.addLast(new LengthFieldBasedFrameDecoder(1024 * 1024, 0, 4, 0, 4))
.addLast(new MyDecoder())
.addLast(new MyEncoder())
.addLast(bizGroup, new BizHandler()); // ★ 耗时业务丢到独立线程池
}
});
ChannelFuture f = b.bind(8080).sync();
f.channel().closeFuture().sync();
} finally {
boss.shutdownGracefully();
worker.shutdownGracefully();
}
addLast(EventExecutorGroup, handler) 这一行是性能与稳定的分水岭:加了它,handler 会被提交到业务线程池执行,EventLoop 不会被阻塞。
三、拆包粘包:四种解法
TCP 是字节流,没有消息边界。发送方写两次,接收方可能一次读到,也可能读半个。
| 方案 | 适用 | Netty 实现 |
|---|---|---|
| 固定长度 | 定长协议 | FixedLengthFrameDecoder |
| 分隔符 | 文本协议(如 \n) |
LineBasedFrameDecoder / DelimiterBasedFrameDecoder |
| 长度字段(推荐) | 二进制协议 | LengthFieldBasedFrameDecoder(最大帧长, 长度字段偏移, 长度字段字节数, 长度调整, 剥离字节数) |
| 自定义 | 私有协议 | 继承 ByteToMessageDecoder |
LengthFieldBasedFrameDecoder 的参数最容易搞错,记住一个例子:报文 = [4字节长度][报文体],长度字段的值只表示报文体长度,则:
// maxFrameLength, lengthFieldOffset=0, lengthFieldLength=4, lengthAdjustment=0, initialBytesToStrip=4
new LengthFieldBasedFrameDecoder(1024 * 1024, 0, 4, 0, 4);
四、ByteBuf:引用计数是核心
ByteBuf 用引用计数管理堆外内存,refCnt 归零才释放。规则:
- 谁最后使用,谁负责
release()。 SimpleChannelInboundHandler会自动帮你release一次(它泛型匹配的那种消息)。- 在 handler 里把 ByteBuf 传给别的线程,或者转成别的对象保留下来,就必须手动释放或
retain()。 - 忘了
release→ 堆外内存泄漏;release多了 →IllegalReferenceCountException。
检测手段:
-Dio.netty.leakDetection.level=PARANOID # 全量采样,抓泄漏专用(性能损耗大)
# 生产可设 SIMPLE(默认)或 ADVANCED
五、心跳与断线重连
IdleStateHandler(readerIdleTime, writerIdleTime, allIdleTime, unit) 本身不发心跳,只触发 IdleStateEvent,需要自己处理:
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
if (evt instanceof IdleStateEvent) {
IdleStateEvent e = (IdleStateEvent) evt;
if (e.state() == IdleState.WRITER_IDLE) {
ctx.writeAndFlush(new PingMessage()); // 空闲时主动发心跳
} else if (e.state() == IdleState.READER_IDLE) {
ctx.close(); // 长时间没收到对端数据,断开
}
}
}
客户端还要配重连:在 channelInactive 里用带退避的定时任务重连(ctx.channel().eventLoop().schedule),不要死循环重连,会打爆服务端。
六、生产排查两个真实案例
案例一:OutOfDirectMemoryError
现象:长连接网关运行几天后报 io.netty.util.internal.OutOfDirectMemoryError: failed to allocate ... byte(s) of direct memory。
排查:
- 加
-Dio.netty.leakDetection.level=PARANOID复现,日志里出现LEAK: ByteBuf.release() was not called before it's garbage-collected,并带有分配点的栈。 - 定位到自定义解码器里,异常分支提前
return而没有release已经读到的ByteBuf。 - 补
try/finally+ReferenceCountUtil.release(msg)。
顺带的配置:-XX:MaxDirectMemorySize 要显式设置(默认和 -Xmx 一样大,容易被忽略),并监控堆外内存使用。
案例二:EventLoop 阻塞导致大面积超时
现象:网关 CPU 不高,但大量连接读写超时,jstack 里 Worker 线程栈顶停在一个数据库查询上。
根因:有人在 channelRead0 里直接查了数据库(还是慢查询)。因为一个 EventLoop 管着几千个连接,这一个查询就把整批连接都卡住了。
修法:
- 把业务处理挪到独立线程池(前面那行
addLast(bizGroup, handler))。 - 慢查询本身也优化掉(加索引)。
- 加一个EventLoop 阻塞检测:Netty 提供
DefaultEventExecutorGroup配合-Dio.netty.eventLoop.maxPendingTasks,也可以用BlockHound在测试期直接拦住阻塞调用。
七、调优参数清单
| 参数 | 建议 |
|---|---|
SO_BACKLOG |
1024 以上,高并发下不够会出现连接被拒 |
TCP_NODELAY |
开启,禁用 Nagle,降低小包延迟 |
SO_KEEPALIVE |
开,但系统默认 2 小时太长,应用级心跳必须有 |
ALLOCATOR |
使用 PooledByteBufAllocator(默认已是),不要改成非池化 |
RCVBUF / SNDBUF |
高吞吐场景适当调大 |
bossGroup 线程数 |
1 就够(只 accept),多端口才需要更多 |
workerGroup 线程数 |
CPU 核数 × 2,IO 密集可再放大,靠压测定 |
八、一句话总结
Netty 的性能来自「一个连接一个线程串行处理 + 零拷贝 + 内存池」,而它的坑也来自同一处:任何阻塞都会放大成一批连接的故障。写 handler 时始终记住:你这段代码是跑在 IO 线程上的。
一起聊聊