百万并发「零拷贝」技术系列之经典案例Netty
Netty在零拷贝思想上的实现可以理解为是广义的,它和wiki对零拷贝宽泛的定义特别吻合“CPU 不需要将数据从一块内存拷贝到另一块内存”,因为Netty主要是在用户空间尽量减少内存的拷贝次数,而非系统层面的用户空间和内核空间数据的拷贝。
Netty作为Java界知名的NIO网络通讯框架,凭借其高性能木秀于mina、twisted,其因素之一就如官方所述:“减少了不必要的内存拷贝”。
在零拷贝实现上,它有借助于Java NIO的tranferTo实现的FileRegion用于文件传输,也有通过巧妙设计buffer数据结构来避免由于拆分、组合而带来的拷贝。尤其是后者,因为buffer是用来化零为整降低I/O操作频率的重要技术手段,对性能的影响至关重要。
FileRegion
FileRegion的零拷贝是体现在系统层面的,它包装了Java NIO的FileChannel.tranferTo方法进行文件传输,从FileRegion的默认实现类DefaultFileRegion可以一探究竟
tranferTo在上一篇推文有较详细的讲解,此处不再累述。
ByteBuf
Netty使用了它自己封装的buffer API替代了Java NIO的ByteBuffer:ByteBuf。官方列出了它的一些比较酷的特性
- You can define your buffer type if necessary.根据需要可以定制自己的buffer类型。
- Transparent zero copy is achieved by built-in composite buffer type.通过内建的组合类型可以实现透明的零拷贝。
- A dynamic buffer type is provided out-of-the-box, whose capacity is expanded on demand, just like `StringBuffer`.它是一个开箱即用可根据需求动态扩展的buffer,就像`StringBuffer`。
- There's no need to call the `flip()` method anymore.不再需要调用`flip()` 方法。
- It is often faster than `ByteBuffer`.通常比`ByteBuffer`更快速。
DirectByteBuffer
实际上ByteBuf提供了非常丰富的实现类如下图所列,在逻辑上主要分为堆内buffer(HeapByteBuf)和堆外buffer(DirectByteBuf)。
DirectByteBuf是用Java NIO的DirectByteBuffer实现的,所谓堆外buffer是相对于JVM堆内而言的,但网上有些资料把它和DMA混淆了。DirectByteBuffer只是避免了JVM堆内向堆外的拷贝,但这个“堆内、堆外”依然是用户空间的范畴,因为 DirectByteBuffer是malloc() 分配出来的内存,用户空间和内核空间请参考上一篇。
拿网络传输来说,对于传统的read/write的I/O方式,一般情况而言是这样的:Java堆内存—>用户空间的堆外内存—>内核socket缓冲区—>DMA—>网卡—>网卡—>DMA—>内核socket缓冲区—>用户空间的堆外内存—>Java堆内存。
DirectByteBuffer的使用是有一定的风险的,可能会造成OutOfMemory,官方是这样描述的
allocating many short-lived direct NIO buffers often causes an OutOfMemoryError.
堆内和堆外内存各有优势和劣势,需要根据场景自行选择
网络传输的过程中对数据的拆包、组包等操作十分常见也很频繁,Netty提供了warp、Composite和slice方法来减少数据的拷贝,达到性能的提升的目标。
wrap包装
Netty可以通过各种wrap方法, 将 byte[]、ByteBuf、ByteBuffer等包装成一个ByteBuf对象,而不需要进行数据的拷贝。实际上Java NIO的ByteBuffer也有wrap,但Netty的ByteBuf提供了更丰富和便捷的wrap。
通常将一个对象比如byte数组转换成一个ByteBuffer,传统的作法是把数组拷贝到ByteBuffer对象中,而wrap的方式无须拷贝,它们共用同一块内存。
byte[] tmp=new byte[]{1,2};
//Java NIO
//传统方式(拷贝)
ByteBuffer byteBuffer=ByteBuffer.allocate(2);
byteBuffer.put(tmp);
//wrap方式
byteBuffer=ByteBuffer.wrap(tmp);
//Netty ByteBuf
//传统方式(拷贝)
ByteBuf byteBuf = Unpooled.buffer();
byteBuf.writeBytes(tmp);
//wrap方式
byteBuf=Unpooled.wrappedBuffer(tmp);
CompositeByteBuf组包
CompositeByteBuf将多个ByteBuf组合成一个ByteBuf而不需要数据拷贝,每个ByteBuf都是独立存在的,只是在逻辑上的组合,提高性能的同时可以统一使用ByteBuf的API。假设有一个数据包是由三部分组成header、body、footer,它们可能是由不同的模块创建的,组合示意和代码如下
ByteBuf header = Unpooled.wrappedBuffer(tmp);
ByteBuf body = Unpooled.wrappedBuffer(tmp);
ByteBuf footer = Unpooled.wrappedBuffer(tmp);
//不建议的作法
ByteBuf wholeBuf = Unpooled.buffer(header.readableBytes()
+ body.readableBytes()+footer.readableBytes());
wholeBuf.writeBytes(header);
wholeBuf.writeBytes(body);
wholeBuf.writeBytes(footer);
//建议使用组合
CompositeByteBuf compositeByteBuf=Unpooled.compositeBuffer();
//第一个参数increaseWriterIndex,为true会自动增加writerIndexss
compositeByteBuf.addComponents(true,header,body,footer);
slice拆分包
slice将一个ByteBuf分解为多个ByteBuf,但没有数据拷贝,而是共享同一个存储区域的,这在拆分包操作时非常有用,如上例数据包由header、body、footer三部分组成,拆分示意和代码如下
ByteBuf byteBuf4slice=.....;
ByteBuf header=byteBuf4slice.slice(0,5);
ByteBuf body=byteBuf4slice.slice(5,15);
ByteBuf footer=byteBuf4slice.slice(15,20);
polled池化
Netty 4.x提供了池化的Buffer,类似于线程池或数据库连接池的思想,避免了Buffer频繁的创建和释放带来的性能低效及GC压力。池化和非池化的性能对比如下
int loop = 3000000;
byte[] content="this is a test".getBytes();
//池化buffer
long startTime = System.currentTimeMillis();
ByteBuf pooledBuf = null;
for (int i = 0; i < loop; i++) {
pooledBuf= PooledByteBufAllocator.DEFAULT.buffer(1024);
pooledBuf.writeBytes(content);
pooledBuf.release();
}
long pooledTime=System.currentTimeMillis()-startTime;
System.out.println("3百万次池化buffer消耗的时间:"+pooledTime);
//非池化buffer
startTime = System.currentTimeMillis();
ByteBuf unPooledBuf = null;
for (int i = 0; i < loop; i++) {
unPooledBuf= Unpooled.buffer(1024);
unPooledBuf.writeBytes(content);
unPooledBuf.release();
}
long unPooledTime=System.currentTimeMillis()-startTime;
System.out.println("3百万次池化buffer消耗的时间:"+unPooledTime);
//性能提升
System.out.println("池化buffer性能提升:"+Double.valueOf(
String.format("%.2f",(unPooledTime-pooledTime)/(double)unPooledTime))*100
+"%");
执行后从输出可见,池化后的buffer性能提升20%左右,非常可观
3百万次池化buffer消耗的时间:766
3百万次池化buffer消耗的时间:989
池化buffer性能提升:23.0%
写在最后
Netty在Java界经之所以久不衰自有它的优势,虽然Netty5夭折了,但Netty4依然足够哦强大,开发者不仅把它用于实现各种通讯应用,还在各种框架中起着顶梁柱的角色,比如阿里的Dubbo。
零拷贝系列以计算机组成及操作系统入手,以零拷贝思想在Linux和Java中的实现为传承,最终以Netty作为经典案例分析收尾,希望能对您有所启发,感谢关注。
End
版权归@码农神说所有,转载须经授权,翻版必究
- Linux同步机制 - 多线程开发总结
- 谷歌发布升级版语音合成系统,直接从字符合成语音
- 无锁编程 - 大纲
- 无锁编程(一) - Double-checked Locking
- 无锁编程(二) - 原子操作
- 我所理解的Remoting(3):创建CAO Service Factory使接口和实现相互分离
- 无锁编程(三) - 忙等待
- Enterprise Library深入解析与灵活应用(9):个人觉得比较严重的关于CachingCallHandler的Bug
- 无锁编程(四) - CAS与ABA问题
- Linux Kernel CMPXCHG函数分析
- 无锁编程(五) - RCU(Read-Copy-Update)
- 无锁编程(六) - seqlock(顺序锁)
- 无锁编程(七) - 实战
- zookeeper的python客户端安装
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- PAT (Advanced Level) Practice 1147 Heaps (30 分)
- Java自动化测试(app自动化环境搭建 31)
- PAT (Basic Level) Practice (中文)1038 统计同成绩学生 (20 分)
- 数据结构题集(严书)串 常见习题代码
- PAT (Basic Level) Practice (中文)1040 有几个PAT (25 分)
- 201909-4ccf计算机职业资格认证考试 第四题 推荐系统
- 【Linux_Shell 脚本编程学习笔记四、监控系统内存并报警企业案例脚本】
- PAT (Basic Level) Practice (中文)1042 字符统计 (20 分)
- Pytorch 中的 5 个非常有用的张量操作
- k-近邻算法实现数字识别
- 【Linux_Shell 脚本编程学习笔记五、Oracle JDK1.8 安装shell 脚本】
- vue中子组件使用$emit传值的种种情况
- 前端工程化建设
- 机器学习101-从JAX的角度去实现
- Spring 系列之jdbcTemplate的使用