Netty 的零拷贝是怎么实现的?
结论先行:Netty 的“零拷贝”是两类东西的合集。操作系统层面的零拷贝指
sendfile、mmap、writev,减少用户态与内核态之间的数据拷贝;Netty 层面的零拷贝指CompositeByteBuf、slice()、duplicate()、wrap()这些“共享底层内存、不复制字节”的缓冲区操作。前者省 CPU 与内存带宽,后者省 JVM 堆内存与 GC。
一、两类零拷贝
| 类型 | 手段 | 省掉什么 |
|---|---|---|
| OS 零拷贝 | FileRegion(sendfile) | 内核缓冲区 → 用户缓冲区的拷贝 |
| OS 零拷贝 | mmap 映射文件 | 同上,但可随机读 |
| OS 零拷贝 | writev 散写 | 多次小写合并为一次系统调用 |
| Netty 零拷贝 | CompositeByteBuf | 合并多个 buffer 时的内存复制 |
| Netty 零拷贝 | slice() / duplicate() | 新建视图而不复制内容 |
| Netty 零拷贝 | Unpooled.wrappedBuffer() | 包装已有数组/ByteBuffer |
| Netty 零拷贝 | 堆外 DirectByteBuf | JVM 堆 → 直接内存的一次拷贝 |
二、传统读写与 sendfile 的拷贝次数
传统 read + write(4 次拷贝 + 4 次上下文切换)
磁盘 → 内核页缓存 → 用户缓冲区 → Socket 缓冲区 → 网卡
sendfile(2 次拷贝,DMA 参与)
磁盘 → 内核页缓存 ─────────────→ Socket 缓冲区 → 网卡
// 用 FileRegion 传输文件,走 sendfile
FileInputStream in = new FileInputStream("data.bin");
FileRegion region = new DefaultFileRegion(in.getChannel(), 0, in.getChannel().size());
ctx.writeAndFlush(region).addListener(ChannelFutureListener.CLOSE);
注意:FileRegion 只在支持 sendfile 的传输实现上生效(如 NioSocketChannel 在 Linux 上),且不支持 SSL/TLS,因为加密必须读进用户态。
三、CompositeByteBuf 的逻辑合并
两条消息分别编码到 buf1、buf2
传统做法:新建 buf3,把 buf1、buf2 的内容 copy 进去(2 次内存复制)
CompositeByteBuf:只记录 [buf1, buf2] 两个引用
┌───────────────┐
│ header(buf1) │ readerIndex 跨组件移动
├───────────────┤
│ body(buf2) │
└───────────────┘
零复制,读取时按组件边界自动切换
CompositeByteBuf composite = Unpooled.compositeBuffer();
composite.addComponents(true, headerBuf, bodyBuf);
ctx.writeAndFlush(composite);
四、slice 与 duplicate
ByteBuf buf = ctx.alloc().buffer(16);
buf.writeBytes("HEADER-PAYLOAD".getBytes());
ByteBuf header = buf.slice(0, 6); // 共享内存,不复制
ByteBuf body = buf.slice(6, 7); // 同上
// 修改 header 会反映到 buf,因为是同一块内存
retainedSlice() 会额外把引用计数加一,避免父缓冲区释放后子视图变成野指针——这是最容易出错的地方。
常见追问
- 追问:Netty 的零拷贝和 Kafka 的零拷贝是一回事吗?不完全。Kafka 用的是 OS 层 sendfile(
FileChannel.transferTo),Netty 同时提供 OS 层与缓冲区层两类。 - 追问:零拷贝一定更快吗?小数据量下
CompositeByteBuf的组件遍历有额外开销,不一定比复制快;数据量大、复制成本高时收益明显。 - 追问:用了零拷贝还要 release 吗?要。引用计数与是否复制无关,
retainedSlice拿到的视图必须自己释放。
小结
- OS 零拷贝省内核与用户态之间的拷贝,Netty 零拷贝省堆内复制。
FileRegion走 sendfile,但不支持 TLS。CompositeByteBuf与slice通过共享内存实现逻辑拼接。- 视图与引用计数绑定,漏 release 会内存泄漏。