花钱的年华

江南白衣,公众号:春天的旁边

Netty资料皆阵列在前

| Filed under 技术

自己收集的一些Netty资料,不断更新的大仓库,大家就别原文复制黏贴转载了。

 

体系化资料

 

自己码的文章

 

精挑细选

 

Netty项目

by calvin | tags : | 3

Netty高性能编程备忘录(下)

| Filed under 工作

前文再续,书接上一回:Netty高性能编程备忘录(上),想不到这次这么快就写了下集,把坑填了。

 

3. 内存篇

3.1 堆外内存池

堆外内存是Netty被说的最多的部分,网卡内核态与应用用户态之间零复制啊,无GC啊,不受堆内存大小限制啊,不重复。
内存池的算法也是Netty骄傲的地方(注意,在4.0的刚开始版本这也是经常改的)
Norman Maurer说,只有在输出时要需要编码对象直接操作bytes[]时,才有可能用回Heap Buffer。

3.1.1 ByteBuf释放

各种异常处理,一不留神,我的踩坑之作:Netty之有效规避内存泄漏

建议写足够的单元测试,在测试里将内存泄漏检查级别开到最高,然后每个用例执行完就System.gc()一次,同时加入一个测试用的appender监控Netty的logger有没有输出memory leak信息。
如果信心已足,在生产环境里,就可以加上"-Dio.netty.leakDetectionLevel=disabled”把检测关掉,提高那么点点理论上存在的性能。

3.1.2 Recycler

Netty的另一个得意设计是对象可以在线程内无锁的被回收重用。但有些场景里,某线程只创建对象,要到另一个线程里释放,一不小心,你就会发现应用缓缓变慢,heap dump时看到好多RecyleHandler对象。所以这块的设计其实在4.0.x的各个版本里变动了无数遍,貌似4.0.40版才终于在我的测试里不再泄漏了。

但有时觉得这么辛苦的重用一个对象(而不是ByteBuffer内存本身),不如干脆禁止掉这个功能,所以4.0.0.33里,我会在启动参数里加入 -Dio.netty.recycler.maxCapacity.default=0。无语的是,也几乎从这个版本开始,才能通过设为0禁止它。

 

3.2 避免复制:CompositeByteBuff, slice(), duplicate()

尽量,尽量不要进行ByteBuf内容复制。

场景1: 为了失败时重试,我要保留内容稍后使用,不想Netty在发送完毕后把内容释放了,最笨的方法是用copy()复制一个新的ByteBuf。

Bytebuf newBuf = oldBuf.duplicate().retain();

而上句只是复制出独立的读写Index, 而底下的ByteBuffer是共享的,同时将ByteBuffer的计数器+1.

场景2: 在Proxy型的应用里,输入输出的内容不变,只替换一些头信息。

聪明的做法是,用slice().retain()语句从旧的ByteBuf中切割出Header外的Body部分,同样是共享底层ByteBuffer。然后生成一个新的Header,然后用CompositeByteBuff,将新的Header 与 旧的Body拼接起来。

 

3.3 避免扩容: ByteBuf的大小预估与AdaptiveRecvByteBufAllocator

ByteBuf如果一开始申请的不足,到极限时会智能的扩容,但也和Java一样,需要重新申请两倍的内存,然后把旧的内容复制过去,一听就是个很消耗的动作,因此,反正是堆外内存池,一开始还是给多一点吧。

另一个有趣的思路是Netty的自适应算法。Netty收到一个请求时,什么都不知道啊,那会申请多大的内存来接收它呢?在Bootstrap里可以配置,默认是 AdaptiveRecvByteBufAllocator,根据每一次收到的请求动态变化。

那如果一个应用有几个不同接口,请求的大小变来变去,会不会玩死它呢?好像会的。不过服务化体系里的特征都是请求小,返回大,请求包的大小变化不会太剧烈。
 

3.4 烦人的rangeChecking

Norman Maurer说,如果你要搜索某个Byte是否存在,请用 byteBuf.forEachByte(ByteProcessor processor), 比循环的遍历地调用byteBuf.readByte()要快得多。原因无它,ByteBuf有Java其他集合同样的rangeChecking。

每次readByte()都不是读一个字节这么简单,首先要判断refCnt()>0,然后再做范围检查防止越界。getByte(i=int)更加一层又一层的检查函数,JVM没有帮你内联或者Profiler工具阻止了你的内联的话,够呛。

 

3.5 readInt(),不要readBytes(bytes[],0,4)

比如Thrift,它会做一层封装,先用byteBuf.readBytes(bytes,0,4)读取byte[],再自己转成int。

但实测证明,我将所有的读写short, int, long, boolean, byte的函数,改造为直接使用Netty的原生函数时,性能从7万QPS提升到7.4万QPS,而消耗CPU不变。

 

3.6 对String说不的 ASCIIString

Netty收到的bytes[],大部分时候最终都要变回String。但String的内部是char[]啊,出入都要经过CharsetEncoder进行转换成byte[],既浪费CPU,又浪费内存。

ByteBufUtils类提供了写入UTF-8和ASCII的优化,不需要从String创建并编码一个bytes[]再开始写入ByteBuf,而是遍历一个个char,当场编码当场写入。可惜此优化对于thrift这种需要先得到byte[]长度的编码器无效。

而Netty 4.1开始,提供了实现 CharSequence接口的ASCIIString。原理就是,String要存char[],是因为UTF-8这样的不定长Encoder,会把char转成1~3个byte。但如果我的Header的名称与某些值,肯定是ASCII字符时(比如服务名,服务版本),那一个char只对应一个byte啊,那你直接在构造函数里把byte[]交给我内部存起来就行了啊,不需要任何转换啊,不费CPU又不废内存了啊。

 

4. 工具类篇

Netty 为了高效编程,或写或借,搞了一些高效的工具类,在自己的应用里同样可以借用一下。

Netty自己有一篇Using as a generic library,介绍了其中的一些。本文主要介绍与性能相关的。
 

4.1 FastThreadLocal

Netty威武,居然太岁头上动土,搞出个比JDK的ThreadLocal还快的ThreadLocal。详见《Netty精粹之设计更快的ThreadLocal》

JDK的ThreadLocal,实现原理是Thread对象里有个HashMap性质的数组,每个ThreadLocal的id是个Hashcode,算法是currentValue+0x61c88647,hashCode取模数组大小得到threadLocal存的位置,如果桶里已有其他元素,key.nextHashCode()找下一个桶,小学学过的HashMap实现之一开放地址法就不啰嗦了。

而FastThreadLocal的id则是一个自增的int,FastThreadLocalThread里放一个数组,直接按下标获取,没有hash,没有比较,没有冲突。不过需要在Netty地界里用,业务线程池就要自己定义ThreadFactory,创建FastThreadLocalThread 而不是Thread。

 

4.2 移植JDK8的宝贝到JDK7

JDK8重写了ConcurrentHashMap,原来的Load Factor,Current Level都没有作用了。
ThreadLocalRandom就是把原来有全局锁的Random,通过ThreadLocal化取消了锁。
LongAdder则是把AtomicLong打散成几个,平时++的时候找其中一个执行,减少CMS冲突的概率,等get()的时候才把几个counter累计起来,适合increment()多,get()少的情况。

Netty把这些类都复制黏贴了一份,封装在 PlatformDependent里,根据JDK版本决定返回JDK原生的还是它复制的。

 

4.3 其他宝贝

4.3.1 ThreadLocal的StringBuilder

之前写过StringBuilder在高性能场景下的正确用法 ,才发现Netty和我做了同样的事,通过ThreadLocal的保存,重用StringBuilder对象,节约内存和分配内存的时间。当然,如果字符串只是很短就未必有必要。

4.3.2 IntHashMap

原始类型的map,比如key是int而不是Integer的Map,在某些次元里挺流行的,Trove,Koloboke, FastUtil等等,好处一是int比Integer省地方,int是4bytes,Integer是12+4 bytes,另外数据结构与解决冲突的方式也不同,详见高性能场景下,关于HashMap的补课

比起FastUtils.jar 穷举各种原始类型-原始类型/对象类型的组合,动不动10M大小。Netty只有IntHashMap一个类, 4.1又增加了LongHashMap等,够用了。

4.3.3 JCTools的Queue

针对Multiple Producer-Single Consumer,Single Producer-Multiple Consumer等不同场景专门设计,做到最少的锁。
不过并不提供阻塞等待的函数,所以不能拿来替换ArrayBlockingQueue。

4.3.4 RecyclableArrayList

如果你需要经常创建很长的ArrayList,不想浪费了,可以考虑用它来节约GC,不过到底哪边的代价大,一定要真正测试后决定。详见Netty精粹之轻量级内存池技术实现原理与应用

 

5. 其他零碎篇

主要来自Norman Maurer的文章:

1. ctx.writeAndFlush() 与 channel.writeAndFlush()的区别在于,channel要经过整条Pipeline,而ctx直接找下一个outboundHandler。

2. channel.writeAndFlush(buf, channel.voidPromise() )
writeAndFlush不管你用不用默认构造返回一个Promise(Future),有点浪费内存。没有用的话,用一个公共的 voidPromise ,减少大家花费。但低版本的Netty不能用。

3. 3. 空闲连接管理,因为刚才说的ctx.writeAndFlush()可能不经过IdleHander,所以只监控读空闲就够了。而且如果每次请求都要READ/WRITE/ALL IDEL三个值算一遍,也白白消耗性能。

4. writeAndFlush()不要太多,毕竟调用了系统调用。

5. Handler能共用就标上Shareable Annotation然后共用,不要每个Channel建一个。

 
暂时只想到这么多。其他想起来再写吧。 最后吐槽一句,Netty即使用的再溜,你的内核参数设定,你的业务代码,其实也有很大的影响,优化时不要光盯着Netty。

转载请保留原文链接,否则视为侵权。。。。http://calvin1978.blogcn.com/articles/netty-performance2.html

Netty高性能编程备忘录(上)

| Filed under 技术

网上赞扬Netty高性能的文章不要太多,但如何利用Netty写出高性能网络应用的文章却甚少,此文权当抛砖引玉。

估计很快就要被拍砖然后修改,因此转载请保持原文链接,否则视为侵权...

http://calvin1978.blogcn.com/articles/netty-performance.html

参考资料:

 

1. 连接篇

1.1 Netty Native

Netty Native用C++编写JNI调用的Socket Transport,是由Twitter将Tomcat Native的移植过来,现在还时不时和汤姆家同步一下代码。

经测试,的确比JDK NIO更省CPU。

也许有人问,JDK的NIO也用EPOLL啊,大家有什么不同? Norman Maurer这么说的:

  • Netty的 epoll transport使用 edge-triggered 而 JDK NIO 使用 level-triggered
  • C代码,更少GC,更少synchronized
  • 暴露了更多的Socket配置参数

第一条没看懂,反正测试结果的确更快更省CPU。

用法倒是简单,只要几个类名替换一下,详见Netty的官方文档1文档2

但要注意,它跟OS相关且基于GLIBC2.10编译,而CentOS 5.8就只有GLIBC2.5(别问为什么,厂大怪事多,我厂就是还有些CentOS5.8的机器),所以最好还是不要狠狠的直接全文搜索替换,而是用System.getProperty(“os.name”)System.getProperty(“os.version”) 取出操作系统名称与版本,做成一个开关。

另外,Netty很多版本都有修复Netty Native相关的bug,看得人心里发毛,好在最近的版本终于不再说了,所以要用就用Netty的新版。

最后,Netty Native还包含了Google的boringssl(A fork of OpenSSL),JDK的原生SSL实现比OpenSSL慢很多很多,而大家把SSL Provider配置成OpenSSL时,又要担心操作系统有没装OpenSSL,或者版本会不会太旧。现在好了。

 

1.2 异步连接,异步传输,告别Commons Pool

异步化最牛头不对马嘴的事情就是,给它配一个类似Commons Pool这样,有借有还的连接池。

在很多异步化的场景里,都用channel.writeAndFlush()原子的发送数据,发完不用同步等response,这时其实不需要独占一条Channel,不需要把它借出去,再还回池里。一来连接池出入之间有并发锁,二来并发请求一多就要狂建连接,到了连接池上限时还要傻傻的等待别人释放连接,而这可能毫无必要。

此时,建议直接建一个连接数组,随机到哪个连接就直接用它发送数据。如果那个连接还没建立或者已经失效,那就建立连接。

顺便说一句,异步的世界里,连建立连接的过程也是异步的,主线程不要等在建连接上,而是把发送的动作封成一个ChannelCallback,等连接建立了,再回调它发送数据,避免因为连接建立的缓慢或网络根本不通,把线程都堵塞了。

Netty4.0.28开始也有ChannelPool了,供需要独占Channel的场景如HTTP1.1,比之Commons Pool的特色之一也是这个异步的建连接过程。

 

1.3 最佳连接数:一条连接打天下?还有传说中的海量连接?

NIO这么神奇,有一种做法是只建一条连接,如Memcached的客户端SpyMemcached。还有一种是既然你能支持海量连接,几千几万的,那我就无节制的可劲的建了。

测试表明,一条连接有瓶颈,毕竟只用到了一个CPU核。 海量连接,CPU和内存在燃烧。。。。

那最佳连接数是传说中的CPU核数么?依然不是。

一切还是看你的场景,连接数在满足传输吞吐量的情况下,越少越好。

举个例子,在我的Proxy测试场景里:

  • 2条连接时,只能有40k QPS。
  • 48条连接,升到62k QPS,CPU烧了28%
  • 4条连接,QPS反而上升到68k ,而且CPU降到20%。

 

1.4 Channel参数设定

TCP/Socket的大路设置,无非 SO_REUSEADDR, TCP_NODELAY, SO_KEEPALIVE 。另外还有SO_LINGER , SO_TIMEOUT, SO_BACKLOG, SO_SNDBUF, SO_RCVBUF。

而用了Native后又加了TCP_CORK和KeepAlive包发送的时间间隔(默认2小时),详见EpoolSocketChannelConfig的JavaDoc

所有这些参数的含义,不一一描述了,自己搜索,比如Linux下高性能网络编程中的几个TCP/IP选项

而Netty自己的参数CONNECT_TIMEOUT_MILLIS,是Netty自己起一个定时任务来监控建立连接是否超时,默认30秒太长谁也受不了,一般会弄短它。

 

2. 线程篇

基本知识:《Netty in Action》中文版—第七章 EventLoop和线程模型

2.1 WorkerGroup 与 Boss Group

大家都知道,Boss Group用于服务端处理建立连接的请求,WorkGroup用于处理I/O。

EventLoopGroup的默认大小都是是2倍的CPU核数,但这并不是一个恒定的最佳数量,为了避免线程上下文切换,只要能满足要求,这个值其实越少越好。

Boss Group每个端口平时好像就只占1条线程,无论配了多少。
 

2.2 上下游线程的绑定

在服务化的应用里,一般处理上游请求的同时,也会向多个下游的服务集群佳节又重阳发送请求,但调优指南里都说,尽量,全部重用同一个EventLoop。否则,处理上游请求的线程,就要把后续任务以Runnable的方式,提交到下游Channel的处理线程。

但,一个EventLoop线程可以处理多个Channel的信息,而一个Channel只能注册一个EventLoop线程。所以没办法保证处理上游的Channel,与下游多个连接的Channel,刚好是属于一个EventLoop?

因此,追求极致的Proxy型应用,可能会放弃前面的固定连接池的做法,而是为每个处理上游请求的线程,对应每一台下游服务器创建一条Channel,而且设定它的工作线程就是本上游线程,然后存到threadLocal里。这样的做法连接数可能会增多,但减少了切换,要自行测试权衡。
 

2.2 业务线程池

Netty线程的数量一般固定且较少,所以很怕线程被堵塞,比如同步的数据库查询,比如下游的服务调用(又来罗嗦,future.get()式的异步在执行future.get()时还是堵住当前线程的啊)。

所以,此时就要把处理放到一个业务线程池里操作,即使要付出线程上下文切换的代价,甚至还有些ThreadLocal需要复制。

 

2.3 定时任务

像发送超时控制之类的一次性任务,不要使用JDK的ScheduledExecutorService,而是如下:

ctx.executor().schedule(new MyTimeoutTask(p), 30, TimeUnit.SECONDS)

首先,JDK的ScheduledExecutorService是一个大池子,多线程争抢并发锁。而上面的写法,TimeoutTask只属于当前的EventLoop,没有任何锁。

其次,如果发送成功,需要从长长Queue里找回任务来取消掉它。现在每个EventLoop一条Queue,明显长度只有原来的N分之一。

 

2.4 快速复习一下Netty的高性能线程池

Netty的线程池理念有点像ForkJoinPool,都不是一个线程大池子并发等待一条任务队列,而是每条线程自己一个任务队列。
不过Netty4的方法是建了N个只有一条线程的线程池,然后用前面说的选择器去选择。而曾经的Netty5 Alpha好像直接就用了ForkJoinPool。

而且Netty的线程,并不只是简单的阻塞地拉取任务,而是非常辛苦命的在每个循环同时做三件事情:

  • 先处理NIO的事件
  • 然后获取2.3里提到的本线程的定时任务,放到本线程的任务队列里
  • 再然后混合2.2里提到的其他线程提交给本线程的任务,一起执行

每个循环里处理NIO事件与其他任务的时间消耗比例,还能通过ioRatio变量来控制,默认是各占50%。

可见,Netty的线程根本没有阻塞等待任务的清闲日子,所以也不使用有锁的BlockingQueue如ArrayBlockingQueue来做任务队列了,而是直接使用下篇里提到的JCTools提供的无锁的MpscLinkedQueue(Mpsc 是Multiple Producer, Single Consumer的缩写)。

 

文章太长没人看,写到这里就停笔了。剩下内容请看 Netty高性能编程备忘录(下)

 

Netty之有效规避内存泄漏

| Filed under 技术

有过痛苦的经历,特别能写出深刻的文章 —— 凯尔文. 肖

直接内存是IO框架的绝配,但直接内存的分配销毁不易,所以使用内存池能大幅提高性能,也告别了频繁的GC。但,要重新培养被Java的自动垃圾回收惯坏了的惰性。

Netty有一篇必读的文档 官方文档翻译:引用计数对象 ,在此基础上补充一些自己的理解和细节。

 

1.为什么要有引用计数器

Netty里四种主力的ByteBuf,
其中UnpooledHeapByteBuf 底下的byte[]能够依赖JVM GC自然回收;而UnpooledDirectByteBuf底下是DirectByteBuffer,如Java堆外内存扫盲贴所述,除了等JVM GC,最好也能主动进行回收;而PooledHeapByteBuf 和 PooledDirectByteBuf,则必须要主动将用完的byte[]/ByteBuffer放回池里,否则内存就要爆掉。所以,Netty ByteBuf需要在JVM的GC机制之外,有自己的引用计数器和回收过程。

一下又回到了C的冰冷时代,自己malloc对象要自己free。 但和C时代又不完全一样,内有引用计数器,外有JVM的GC,情况更为复杂。

 

2. 引用计数器常识

  • 计数器基于 AtomicIntegerFieldUpdater,为什么不直接用AtomicInteger?因为ByteBuf对象很多,如果都把int包一层AtomicInteger花销较大,而AtomicIntegerFieldUpdater只需要一个全局的静态变量。
  • 所有ByteBuf的引用计数器初始值为1。
  • 调用release(),将计数器减1,等于零时, deallocate()被调用,各种回收。
  • 调用retain(),将计数器加1,即使ByteBuf在别的地方被人release()了,在本Class没喊cut之前,不要把它释放掉。
  • 由duplicate(), slice()和order()所衍生的ByteBuf,与原对象共享底下的buffer,也共享引用计数器,所以它们经常需要调用retain()来显示自己的存在。
  • 当引用计数器为0,底下的buffer已被回收,即使ByteBuf对象还在,对它的各种访问操作都会抛出异常。

 

3.谁来负责Release

在C时代,我们喜欢让malloc和free成对出现,而在Netty里,因为Handler链的存在,ByteBuf经常要传递到下一个Hanlder去而不复还,所以规则变成了谁是最后使用者,谁负责释放。

另外,更要注意的是各种异常情况,ByteBuf没有成功传递到下一个Hanlder,还在自己地界里的话,一定要进行释放。

3.1 InBound Message

在AbstractNioByteChannel.NioByteUnsafe.read() 处创建了ByteBuf并调用 pipeline.fireChannelRead(byteBuf) 送入Handler链。

根据上面的谁最后谁负责原则,每个Handler对消息可能有三种处理方式

  • 对原消息不做处理,调用 ctx.fireChannelRead(msg)把原消息往下传,那不用做什么释放。
  • 将原消息转化为新的消息并调用 ctx.fireChannelRead(newMsg)往下传,那必须把原消息release掉。
  • 如果已经不再调用ctx.fireChannelRead(msg)传递任何消息,那更要把原消息release掉。

假设每一个Handler都把消息往下传,Handler并也不知道谁是启动Netty时所设定的Handler链的最后一员,所以Netty在Handler链的最末补了一个TailHandler,如果此时消息仍然是ReferenceCounted类型就会被release掉。
 

3.2 OutBound Message

要发送的消息由应用所创建,并调用 ctx.writeAndFlush(msg) 进入Handler链。在每个Handler中的处理类似InBound Message,最后消息会来到HeadHandler,再经过一轮复杂的调用,在flush完成后终将被release掉。

 

3.3 异常发生时的释放

多层的异常处理机制,有些异常处理的地方不一定准确知道ByteBuf之前释放了没有,可以在释放前加上引用计数大于0的判断避免释放失败;

有时候不清楚ByteBuf被引用了多少次,但又必须在此进行彻底的释放,可以循环调用reelase()直到返回true。

 

4. 内存泄漏检测

所谓内存泄漏,主要是针对池化的ByteBuf。ByteBuf对象被JVM GC掉之前,没有调用release()把底下的DirectByteBuffer或byte[]归还到池里,会导致池越来越大。而非池化的ByteBuf,即使像DirectByteBuf那样可能会用到System.gc(),但终归会被release掉的,不会出大事。

Netty担心大家不小心就搞出个大新闻来,因此提供了内存泄漏的监测机制。

Netty默认会从分配的ByteBuf里抽样出大约1%的来进行跟踪。如果泄漏,会有如下语句打印:

LEAK: ByteBuf.release() was not called before it's garbage-collected. Enable advanced leak reporting to find out where the leak occurred. To enable advanced leak reporting, specify the JVM option '-Dio.netty.leakDetectionLevel=advanced' or call ResourceLeakDetector.setLevel()

这句话报告有泄漏的发生,提示你用-D参数,把防漏等级从默认的simple升到advanced,就能具体看到被泄漏的ByteBuf被创建和访问的地方。

  • 禁用(DISABLED) - 完全禁止泄露检测,省点消耗。
  • 简单(SIMPLE) - 默认等级,告诉我们取样的1%的ByteBuf是否发生了泄露,但总共一次只打印一次,看不到就没有了。
  • 高级(ADVANCED) - 告诉我们取样的1%的ByteBuf发生泄露的地方。每种类型的泄漏(创建的地方与访问路径一致)只打印一次。对性能有影响。
  • 偏执(PARANOID) - 跟高级选项类似,但此选项检测所有ByteBuf,而不仅仅是取样的那1%。对性能有绝大的影响。

实现细节

每当各种ByteBufAllocator 创建ByteBuf时,都会问问是否需要采样,Simple和Advanced级别下,就是以113这个素数来取模(害我看文档的时候还在瞎担心,1%,万一泄漏的地方有所规律,刚好躲过了100这个数字呢,比如都是3倍数的),命中了就创建一个Java堆外内存扫盲贴里说的PhantomReference。然后创建一个Wrapper,包住ByteBuf和Reference。

simple级别下,wrapper只在执行release()时调用Reference.clear(),Advanced级别下则会记录每一个创建和访问的动作。

当GC发生,还没有被clear()的Reference就会被JVM放入到之前设定的ReferenceQueue里。

在每次创建PhantomReference时,都会顺便看看有没有因为忘记执行release()把Reference给clear掉,在GC时被放进了ReferenceQueue的对象,有则以 "io.netty.util.ResourceLeakDetector”为logger name,写出前面例子里的Error级别的日日志。顺便说一句,Netty能自动匹配日志框架,先找Slf4j,再找Log4j,最后找JDK logger。

值得说三遍的事

一定要盯紧log里有没有出现 "LEAK: "字样,因为simple级别下它只会出现一次,所以不要依赖自己的眼睛,要依赖grep。如果出现了,而且你用的是PooledBuf,那一定是问题,不要有任何的侥幸,立刻用"-Dio.netty.leakDetectionLevel=advanced" 再跑一次,看清楚它创建和访问的地方。

功能测试时,最好开着"-Dio.netty.leakDetectionLevel=paranoid"。

但是,怎么测试都可能存在没有覆盖到的分支。如果内存尚够,可以适当把-XX:MaxDirectMemorySize 调大,反正只是max,平时也不会真用了你的。然后监控其使用量,及时报警。

 
片末招聘广告:唯品会广州总部的基础架构部招人!! 如果你喜欢纯技术的工作,对大型互联网企业的服务化平台有兴趣,愿意在架构的成长期还可以大展拳脚的时候加盟,请电邮 calvin.xiao@vipshop.com

 
文章持续修订,转载请保留原链接: http://calvin1978.blogcn.com/articles/netty-leak.html

by calvin | tags : | 7

Netty之Java堆外内存扫盲贴

| Filed under 技术

Java的堆外内存本来是高贵而神秘的东西,只在一些缓存方案的收费企业版里出现。但自从用了Netty,就变成了天天打交道的事情,毕竟堆外内存能减少IO时的内存复制,不需要堆内存Buffer拷贝一份到直接内存中,然后才写入Socket中;而且也没了烦人的GC。

好在,Netty所用的堆外内存只是Java NIO的 DirectByteBuffer类,通读一次很快。还有一些sun.misc.*的类木有源码,要自己跑去OpenJdk那看个明白。

 

1. 堆外内存的创建

在DirectByteBuffer中,首先向Bits类申请额度,Bits类有一个全局的 totalCapacity变量,记录着全部DirectByteBuffer的总大小,每次申请,都先看看是否超限 -- 堆外内存的限额默认与堆内内存(由-XMX 设定)相仿,可用 -XX:MaxDirectMemorySize 重新设定。

如果已经超限,会主动执行Sytem.gc(),期待能主动回收一点堆外内存。然后休眠一百毫秒,看看totalCapacity降下来没有,如果内存还是不足,就抛出大家最头痛的OOM异常。

如果额度被批准,就调用大名鼎鼎的sun.misc.Unsafe去分配内存,返回内存基地址,Unsafe的C++实现在此,标准的malloc。然后再调一次Unsafe把这段内存给清零。跑个题,Unsafe的名字是提醒大家这个类只给Sun自家用的,你们别用,不然哪天Sun把它藏起来了你们就哭死。果然,JDK9里就Oracle可能动手哦

JDK7开始,DirectByteBuffer分配内存时默认已不做分页对齐,不会再每次分配并清零 实际需要+分页大小(4k)的内存,这对性能应有较大提升,所以Oracle专门写在了Enhancements in Java I/O里。

最后,创建一个Cleaner,并把代表清理动作的Deallocator类绑定 -- 降低Bits里的totalCapacity,并调用Unsafe调free去释放内存。Cleaner的触发机制后面再说。

 

2. 堆外内存基于GC的回收

存在于堆内的DirectByteBuffer对象很小,只存着基地址和大小等几个属性,和一个Cleaner,但它代表着后面所分配的一大段内存,是所谓的冰山对象。通过前面说的Cleaner,堆内的DirectByteBuffer对象被GC时,它背后的堆外内存也会被回收。

快速回顾一下堆内的GC机制,当新生代满了,就会发生young gc;如果此时对象还没失效,就不会被回收;撑过几次young gc后,对象被迁移到老生代;当老生代也满了,就会发生full gc。

这里可以看到一种尴尬的情况,因为DirectByteBuffer本身的个头很小,只要熬过了young gc,即使已经失效了也能在老生代里舒服的呆着,不容易把老生代撑爆触发full gc,如果没有别的大块头进入老生代触发full gc,就一直在那耗着,占着一大片堆外内存不释放。

这时,就只能靠前面提到的申请额度超限时触发的system.gc()来救场了。但这道最后的保险其实也不很好,首先它会中断整个进程,然后它让当前线程睡了整整一百毫秒,而且如果gc没在一百毫秒内完成,它仍然会无情的抛出OOM异常。还有,万一,万一大家迷信某个调优指南设置了-DisableExplicitGC禁止了system.gc(),那就不好玩了。

所以,堆外内存还是自己主动点回收更好,比如Netty就是这么做的。

 

3. 堆外内存的主动回收

对于Sun的JDK这其实很简单,只要从DirectByteBuffer里取出那个sun.misc.Cleaner,然后调用它的clean()就行。

前面说的,clean()执行时实际调用的是被绑定的Deallocator类,这个类可被重复执行,释放过了就不再释放。所以GC时再被动执行一次clean()也没所谓。

在Netty里,因为不确定跑在Sun的JDK里(比如安卓),所以多废了些功夫来确定Cleaner的存在。

 

4. Cleaner如何与GC相关联?

涨知识的时间到了,原来JDK除了StrongReference,SoftReference 和 WeakReference之外,还有一种PhantomReference,Phantom是幻影的意思,Cleaner就是PhantomReference的子类。

当GC时发现它除了PhantomReference外已不可达(持有它的DirectByteBuffer失效了),就会把它放进 Reference类pending list静态变量里。然后另有一条ReferenceHandler线程,名字叫 "Reference Handler"的,关注着这个pending list,如果看到有对象类型是Cleaner,就会执行它的clean(),其他类型就放入应用构造Reference时传入的ReferenceQueue中,这样应用的代码可以从Queue里拖出这些理论上已死的对象,做佳节又重阳爱做的事情——这是一种比finalizer更轻量更好的机制。

 

5. 其实

专家们说,OpenJDK没有接受jemalloc(redis们在用)的补丁,直接用malloc在OS里申请一段内存,比在已申请好的JVM堆内内存里划一块出来要慢,所以我们在Netty一般用池化的 PooledDirectByteBuf 对DirectByteBuffer进行重用 ,《Netty权威指南》说性能提升了23倍,所以基本不需要头痛堆外内存的释放,顺便还告别了大数据流量下的频繁GC。

 
片末招聘广告:唯品会广州总部的基础架构部招人!! 如果你喜欢纯技术的工作,对大型互联网企业的服务化平台有兴趣,愿意在架构的成长期还可以大展拳脚的时候加盟,请电邮 calvin.xiao@vipshop.com

 
文章持续修订,转载请保留原链接: http://calvin1978.blogcn.com/articles/directbytebuffer.html