文档章节

百万并发「零拷贝」技术系列之经典案例Netty

码农神说
 码农神说
发布于 07/29 08:00
字数 1905
阅读 1W
收藏 16

「深度学习福利」大神带你进阶工程师,立即查看>>>




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)。

DirectBy teBuf是用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



版权归@码农神说所有,转载须经授权,翻版必究

可回复关键字“转载”联系助手开白




缓存穿透、缓存击穿、缓存雪崩看这篇就够了,文末还送福利哦! 2020-07-15
一口气讲透一致性哈希(Hash),助力「码农变身」 2020-07-13
漫画 | 架构设计中的那些事,文末送福利 2020-07-10
Java中异常处理的9个最佳实践 2020-07-08
Intellij IDEA必备插件,提高效率的“七种武器”! 2020-07-06
接住喽🤗,送你个装逼的技能: JDK动态代理 2020-07-04
给“小白”漫画+图示讲解MyBatis原理,就问香不香! 2020-07-01
面试官:CAP都搞不清楚,别跟我说你懂微服务! 2020-06-23


本文分享自微信公众号 - 码农神说(codeceo)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

码农神说
粉丝 8
博文 43
码字总数 47799
作品 0
深圳
高级程序员
私信 提问
加载中
请先登录后再评论。
Netty那点事(三)Channel与Pipeline

Channel是理解和使用Netty的核心。Channel的涉及内容较多,这里我使用由浅入深的介绍方法。在这篇文章中,我们主要介绍Channel部分中Pipeline实现机制。为了避免枯燥,借用一下《盗梦空间》的...

黄亿华
2013/11/24
2W
22
用vertx实现高吞吐量的站点计数器

工具:vertx,redis,mongodb,log4j 源代码地址:https://github.com/jianglibo/visitrank 先看架构图: 如果你不熟悉vertx,请先google一下。我这里将vertx当作一个容器,上面所有的圆圈要...

jianglibo
2014/04/03
4.3K
3
Swift百万线程攻破单例(Singleton)模式

一、不安全的单例实现 在上一篇文章我们给出了单例的设计模式,直接给出了线程安全的实现方法。单例的实现有多种方法,如下面: class SwiftSingleton { } 这段代码的实现,在shared中进行条...

一叶博客
2014/06/20
3.5K
16
WSGI Web服务器--UV-Web

uv-web是一个轻量级的支持高并发的WSGI Web服务器,基于libuv构建,部分代码源于开源项目bjoern,本质是python的C扩展,所以适用于部署绝大部分 python web应用(如 Django) 特性 兼容 HTTP 1...

Jone.x
2013/03/04
1.8K
0
WebUI自动化测试框架--Dagger

Dagger是网易杭州研究院QA团队开发的一个轻量级、运行稳定的WebUI自动化测试框架,主要基于Selenium及TestNg可以认为是对Selenium进行二次封装的一个框架(俗称 造轮子 )。之所以把这个轮子...

ChenKan
2013/03/05
2.8W
6

没有更多内容

加载失败,请刷新页面

加载更多

《ClickHouse 源码阅读 —— SQL的前世今生》

简介: 作者:逍凯 注:以下分析基于开源 v19.15.2.2-stable 版本进行,社区最新版本代码改动较大,但是总体思路是不变的。 用户提交一条查询SQL背后发生了什么? 在传统关系型数据库中,SQL...

一肥仔
昨天
8
0
神的编辑器 Emacs 快捷键

基础命令 C-x C-c 退出 C-x C-s 保存 C-x C-w 另存为 C-x C-f 打开文件 C-g 取消当前操作 C-x u 撤销 移动命令 M-Shift-< 文档开头 M-Shift-> 文档结尾 C-a 行首 C-e 行尾 剪切复制粘贴 C-sp...

lemos
昨天
13
0
GeoPandas入门 | 03-空间关系和操作

03-空间关系和操作 源代码请看此处 %matplotlib inlineimport pandas as pdimport geopandas countries = geopandas.read_file("zip://./data/ne_110m_admin_0_countries.zip")cities =......

酱肉包-
昨天
8
0
检查Ruby中的数组中是否存在值 - Check if a value exists in an array in Ruby

问题: I have a value 'Dog' and an array ['Cat', 'Dog', 'Bird'] . 我有一个值'Dog'和一个阵列['Cat', 'Dog', 'Bird'] 。 How do I check if it exists in the array without looping thr......

fyin1314
昨天
9
0
最新情报:所有的递归都可以改写成非递归?

前言 本文收录于专辑:http://dwz.win/HjK,点击解锁更多数据结构与算法的知识。 你好,我是彤哥,一个每天爬二十六层楼还不忘读源码的硬核男人。 上一节,我们使用位图介绍了12306抢票算法的...

彤哥读源码
昨天
23
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部