docker使用堆外内存

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了docker使用堆外内存相关的知识,希望对你有一定的参考价值。

参考技术A 直接内存(堆外内存)假设你申请的docker有8G,划了2G给堆,1G给栈,程序计数器的可以忽略不计,那堆外内存就大约是5G。可以通过XX:MaxDirectMemorySize参数来设置最大可用直接内存,如果启动时未设置则默认为最大堆内存大小,即与-Xmx相同。

netty堆外内存的使用

一、java的堆外内存

堆外内存的限额默认与堆内内存(由-XMX 设定)相同,可用 -XX:MaxDirectMemorySize 重新设定

1、优缺点

优点:

(1)可以扩展至更大的内存空间。比如超过1TB甚至比主存还大的空间;

(2)理论上能减少GC暂停时间;

(3)可以在进程间(系统调用,aio)共享,减少JVM间的对象复制,使得JVM的分割部署更容易实现;

(4)它的持久化存储可以支持快速重启,同时还能够在测试环境中重现生产数据

(5)堆外内存能减少IO时的内存复制,不需要堆内存Buffer拷贝一份到直接内存,然后才写入Socket中

缺点:

(1)需要手动释放(或者在引用直接内存的对象回收的时候释放,相当于依赖gc)

(2)当依靠gc时,需要考虑gc时机如何确定,频繁显示调用System.gc()显然不是个好主意,因为不可控,而依靠堆内存控制,需要计算堆外内存的增长速率与堆内内存增长速率的关系,合理配置jvm参数,在堆外内存OOM之前触发gc

(3)Unsafe对象通常是不可见的,因为其实现并非开源,并且不同jre发行商都对此有不同实现和政策

2、申请

(1)通过Unsafe对象,调用native方法,与c++类似,
ptr =Unsafe.allocateMemory(size),
(2)Java.nio.ByteBuffer也可以申请直接内存,底层也是使用Unsafe,但是有较安全的管理机制。

3、释放

(1)Unsafe.freeMemory(ptr)。
(2)ByteBuffer在Oracle JVM中是通过Cleaner的机制去释放。
ByteBuffer通过将自身注册到Cleaner中,作为一个虚引用,在gc时的回调方法里释放直接内存。

二、Netty的堆外内存使用

Cleaner这个类可被重复执行,释放过了就不再释放。所以GC时再被动执行一次clean()也没所谓。

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

Netty的内存buffer对象都继承了ReferenceCounted接口,并且在netty handler的设计规范中,所有输入的数据在处理结束时都会调用ReferenceCountUtil.release()释放,只是具体释放方法根据池化、非池化、直接内存、堆内存的不同实现不同。

引用数工具类:ReferenceCountUtil

池化对象接口:ReferenceCounted

1、池化

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

在netty4.0以后,池化内存都在一个PoolArena基类型的线程变量中进行申请释放,对于不熟悉netty新版的开发者来说,经常会掉入这个大坑中。

2、非池化

一般在业务线程中发送数据时使用,因为一般netty的handler只处理ByteBuf类型的数据,我们可以用Unpooled.wrapper方法提供一个对byte[]的封装即可。

如果使用非池化堆外内存,其实就是DirectByteBuffer的一个封装,使用也与其类似。

三、Netty开发中的注意事项

1、线程模型

总的来说是IO线程池+业务线程池,IO线程在服务器可以把acceptor和读写分离,而对于轻业务的应用,可以省略业务线程,直接在io线程中处理。

在netty4.0以后的版本要特别注意每个连接的IO处理都是在“同一个线程”,打引号是因为在局部他确实坚持了这一原则,但在宏观配置下,这种特性也可以被打破,netty的线程被封装叫做EventExecutor, 通常我们获取EventExecutor的方式是通过ChannelHandlerContext或者Channel,ChannelHandlerContext引用了Channel.要非常注意的点就是,Channel的EventExecutor是WorkerGroup也就是常说的IO线程, 在连接建立的时候就由WorkerGroup线程组分配了一个线程,并在断开连接前都由这一个线程处理该Channel的IO工作,ChannelHandlerContext的EventExecutor是依赖配置,如果为空就使用Channel的EventExecutor。在netty底层代码中,经常可以看到这样的段落:

if (eventLoop.inEventLoop())  
     register0(promise); 
  else  
     try  
         eventLoop.execute(new Runnable()  
             @Override              
            public void run()  
                 register0(promise);             
          
 );

也就是说不管在任何线程执行代码,有的操作(底层一般是io)只会在同一个线程执行,这个原则主要是为了减少锁的存在,提高性能而且逻辑也简单很多,虽然在各种枝端末节都会重复出现上面的代码,但好在都隐藏在底层。

对于每个handler都可以分配独立的线程池,当然在一般需求中很难有多层次的异步解耦,而且非常不建议在IO处理的过程中加入多个线程池,可能会导致内存泄露,原因后面会说。4.1之后有一个新特性,就是对于业务线程池可以设置是否在固定的线程中执行handler,这和IO线程的方式一样,是一开始就分配一个线程还是总是从线程池中取出一个空闲的线程使用,这就根据业务情况自行选择了,业务是否有顺序和同步的要求。

2、内存池

DirectByteBuffer的释放依赖于gc,所以在DirectByteBuffer的实践中,为了防止OOM,每次申请新的堆外内存都调用System.gc(),当然这样做的副作用就是增加了gc的次数,但由于该方法触发gc的延迟特性,也可能导致偶发性的OOM,更甚至大部分java程序的jvm启动参数是禁用显示的gc调用。所以netty为了缓解这个冲突性的问题,把直接内存池化,在稳定的业务环境中可以保证使用的堆外内存不增长,但为了更加安全,还需要对具体的程序进行调优。
netty的内存池实现类是PoolThreadCache,其中包含直接内存的池与堆内存的池(PoolArena),从名称可以看出,PoolThreadCache是一个线程变量,因为其中涉及内存块的拆分和合并,为了免除锁的使用,所有的申请和释放都只能在同一个线程内完成。在netty的默认配置中,所有IO线程都是使用池化的直接内存读写网络数据,所以如果要加入异步业务时,一定要在池化内存传递到另一个线程前拷贝到非池化对象,然后释放池化资源。

3、Handler

与大部分服务器管道模式相似,netty的Handler chain对应spring的filter chain,与filter都是单例不同的在于netty中的ChannelHandler有单例共用的类型,也有复用的类型,一般通过ChannelHandlerInitializer去配置新建的连接,如果不是新的实例那肯定是复用类型,如果你的单例handler没有使用@Sharable注解,还会收到一个警告。

大部分handler都是单例,而基本上帧解码器都是新实例,因为tcp粘包的需要,半包的字节累加器对于每个连接都是独立的。

对于ChannelHandler还有一点要注意,一般来说输入(InBound)处理会把已处理的对象在结尾进行回收处理,解码器还会对输出的对象也回收,但在此之前handler会把输出对象传递到下一个handler,一般传递的Handler处理都是同步的,但如果handler分配了不同的线程池就会变成异步,有可能在下个handler处理前就把你将要处理的对象进行回收操作。ByteBuf一般都是个封装,底层的直接内存、堆内存有可能就被释放掉了,为了避免回收,主要是针对ByteBuf类型,一般要做引用数+1的操作。

4、Jvm参数

一般情况下,netty的网络IO都要使用直接内存,所以部署netty项目时,一定要考虑堆外内存的大小,由于这部分直接内存是池化的而且仅用于IO,所以对于稳定的上传下载流量下,内存池会增长到一个平衡点,这个大小约等于瞬时峰值的接收字节+发送字节数量,减少直接内存的借还周期就可以有效地控制直接内存池的大小,这也是为什么一般不在IO线程中处理业务,而把业务放到另外的线程中进行的原因之一。基本上可以用(最大并发数*最大包体)来估计直接内存的大小。
如果有业务需要使用到直接内存,就需要考虑何时回收直接内存,手动释放是一个选择,但可能会使逻辑复杂化,并难免遗漏。对于大部分项目来说还是希望通过GC自动去回收,所以如何保证在直接内存达到一定大小时触发fullGC呢?
一种方式是调整一个较小的老年代,并且生存区的阶段也尽可能短,促使保守的GC,当然这样对TPS肯定有影响。
方法二,根据堆内存的增长速率与直接内存的增长速率之间的关系去制定合理的GC触发机制,但这个需要业务比较单一,堆内存和直接内存之间的相关性比较高才可行。
方法三,对于堆内存增长速率比较稳定的项目,可以选择业务较少的时段定时触发fullGC,
至于实现方法可以使用jmap -live。



作者:gofun成都技术中心
链接:https://www.jianshu.com/p/7504e2cbe8db
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

以上是关于docker使用堆外内存的主要内容,如果未能解决你的问题,请参考以下文章

关于docker运行Java程序JVM配置参数使用jconsole的简单量化过程

Java 堆外内存回收原理

Java堆外内存的使用

java 堆外内存使用

docker 用于 com.docker.hyperkit 中的 mac 内存使用情况

docker 如何限制和查看container 内存 和cpu