周一放送:如何解决JAVA运行在Docker上的问题
两个周末终于看完了<微服务设计>这本书,强烈推荐给像我这样的小白,它会让你在一个高度上去思考全局,不仅仅讲解了技术,也剖析了人性。
“不管一开始看起来什么样,它永远是人的问题。请记住,如果没有把人们拉到一条船上,你想要的任何变化从一开始就注定会失败。”
Java运行在Docker上的问题
上周一个同事做压力测试,描述的测试现象是java吃内存很厉害,不一会儿就docker容器就crash重启。让我很自然的联想到了曾经看到过的一位oracle老师写的文章,它的题目大约是《java程序运行在docker容器上可能会有哪些问题?》。
摘抄下当时记录下的笔记:
Docker 仅在类似 Linux 内核之上实现了有限的隔离和虚拟化,并不是像传统虚拟化软件那样,独立运行在一个新的操作系统。容器虽然省略了虚拟操作系统的开销,实现了轻量级的目标,但也带来了额外的复杂度,它的限制对于应用不是透明的,需要用户理解docker的新行为。所以,有专家层级说过,“幸运的是Docker没有完全隐藏底层信息,但不幸的是也是Docker没有隐藏底层信息。”
所以,对于Java平台而言,历史版本的Java显然并不能理解CGroup这种对资源的限制和隔离的技术。
从JVM运行机制来看,例如:JVM会根据检测到的系统内存大小,启动默认的初始堆大小(1/64),堆的最大值为系统内存的1/4; 同时,它还会根据检查到的CPU个数直接设置Parallel GC的并行数目和JIT complier线程数。而这些参数的判断,很可能是错误信息做出的。印象很深的一句话是:“我以为我住的大别墅,结果实际我只有一个房间的使用权。”
如何解决?
- 升级到最新的JDK版本,JDK9中引入了Docker和JVM进行资源沟通的参数设置:
-XX:+UnlockExperimentalVMOptions
-XX:+UseCGroupMemoryLimitForHeap
注意这两个参数的顺序敏感,并且只支持linux环境。
- 如何升级到JDK10, 那么默认java会适应各种资源限制和实现差异。同时还增加了参数以明确指定CPU核心数目:
-XX:ActiveProcessorCount=N
- 上面第一点提及到JDK9中的参数设置已经被移植到了Oracle JDK 8u131+。
如果JDK老版本使用Docker呢
老师的建议是,明确设置堆,元数据等内存区域大小,保证JAVA进程的总大小可控。
例如,
- 限制容器内存。
- 在dockerFile里,明确指定JVM堆大小。
-e JAVA_OPTIONS='-Xmx300m'
- 明确配置GC和JIT并行线程数目,避免二者占用过多计算资源。
-XX:ParallelGCThreads
-XX:CICompilerCount
- 如果还是出现swap的现象,建议配置下面的参数,让JVM明确系统内存的限额
-XX:MaxRAM=
cat /sys/fs/cgroup/memory/memory.limit_in_bytes
结论:平时还是要多读书啊
后来在docker file里添加了对jvm的设置, 终于没有出现docker内存消耗并导致重启的现象了。
ENV JAVA_OPTS="-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XX:MaxRAMFraction=2 -XshowSettings:vm"