Netty 性能好的原因是什么?
结论先行:Netty 的高性能不是单点优化,而是四层合力——IO 模型(主从 Reactor + epoll 边缘触发)、线程模型(Channel 绑定单 EventLoop,串行无锁)、内存管理(池化 ByteBuf + 堆外内存 + 零拷贝)、数据结构(FastThreadLocal、MPSC 无锁队列)。任何一层缺失,另外三层都会被拖累。
一、四层优化拆解
| 层次 | 手段 | 收益 |
|---|---|---|
| IO 模型 | 主从 Reactor、epoll 边缘触发、多路复用 | 少量线程撑住大量连接 |
| 线程模型 | 一个 Channel 只由一个 EventLoop 处理 | 无锁串行,省掉同步开销 |
| 内存 | PooledByteBufAllocator、DirectByteBuffer、对象池 | 减少 GC 与内存拷贝 |
| 数据结构 | FastThreadLocal、MpscQueue、IntObjectHashMap | 减少哈希冲突与锁竞争 |
| 系统调用 | sendfile、writev、mmap | 减少用户态/内核态拷贝次数 |
二、主从 Reactor 的线程账
┌──────────────┐
连接接入 ───> │ bossGroup │ 1 个线程,只做 accept
└──────┬───────┘
│ 注册到 worker
┌──────────────┼──────────────┐
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
│EventLoop│ │EventLoop│ │EventLoop│ 默认 2×CPU 个
│ 线程 1 │ │ 线程 2 │ │ 线程 N │
└────┬────┘ └────┬────┘ └────┬────┘
多个 Channel 多个 Channel 多个 Channel
(每个 Channel 只属于一个 EventLoop)
一个 EventLoop 线程负责多个 Channel 的读写与事件分发,Channel 上的所有回调都在同一线程执行,天然串行,不需要给业务对象加锁。
三、内存与拷贝
传统方式:磁盘 → 内核缓冲区 → 用户缓冲区 → Socket 缓冲区 → 网卡 4 次拷贝
sendfile:磁盘 → 内核缓冲区 ──────────────→ Socket 缓冲区 → 网卡 2 次拷贝
Netty 的 FileRegion 走的就是 sendfile;CompositeByteBuf 把多个缓冲区逻辑合并,避免把数据拼进一块新内存;slice()/duplicate() 共享底层数组,只新建索引视图。
四、可调参数
ServerBootstrap b = new ServerBootstrap();
b.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
.childOption(ChannelOption.TCP_NODELAY, true) // 关闭 Nagle,降低小包延迟
.childOption(ChannelOption.SO_RCVBUF, 128 * 1024)
.childOption(ChannelOption.SO_SNDBUF, 128 * 1024)
.childOption(ChannelOption.WRITE_BUFFER_WATER_MARK,
new WriteBufferWaterMark(32 * 1024, 64 * 1024));
JVM 侧相关开关:
-Dio.netty.allocator.type=pooled
-Dio.netty.maxDirectMemory=0 # 0 表示不限,交给 JVM 参数控制
-Dio.netty.leakDetection.level=paranoid # 排查泄漏时临时打开
五、性能陷阱
| 陷阱 | 后果 | 处理 |
|---|---|---|
| 在 EventLoop 里做阻塞调用(DB、HTTP) | 该线程上的所有 Channel 卡住 | 业务丢到独立线程池 |
频繁 Unpooled.buffer() | GC 压力大 | 用池化分配器 |
忘了 release() | 堆外内存泄漏 | 引用计数 + 泄漏检测 |
关闭 TCP_NODELAY | 小包延迟上升到 40ms | 交互型协议打开 |
写操作不检查 isWritable | 内存堆积 OOM | 高水位时暂停读 |
常见追问
- 追问:Netty 比原生 NIO 快多少?同等工作量下通常差距在 10%~30%,真正的差距在稳定性与开发效率;性能优势主要来自池化与无锁设计。
- 追问:为什么默认 EventLoop 数是 2×CPU?IO 与计算混合时留出余量,纯 IO 场景可以调到 CPU 数,CPU 密集业务应把处理移出 EventLoop。
- 追问:堆外内存一定更快吗?省一次拷贝,但分配与回收成本更高,必须配合池化;小对象短生命周期用堆内反而更划算。
小结
- IO 模型、线程模型、内存管理、数据结构四层合力,缺一不可。
- Channel 绑定 EventLoop 是“无锁”的根源,也是不能阻塞 EventLoop 的原因。
- 零拷贝靠 sendfile、CompositeByteBuf、slice 三类手段。
- 参数调优有收益,但把阻塞调用移出 EventLoop 的收益更大。