基本回答
一. 对于Java来说,Docker毕竟是一个较新的环境,其内存、CPU等资源限制是通过ControlGroup实现的。早期的JDK版本并不能识别这些限制,进而会导致一些基础问题。
1.如果未配置合适的JVM堆和元数据区、直接内存等参数,Java就有可能试图使用超过容器限制的内存,最终被容器OOM kill,或者自身发生OOM。
2.错误判断了可获取的CPU资源,例如,Docker限制了CPU的核数,JVM就可能设置不合适的GC并行线程数等。
二. 从应用打包、发布等角度出发JDK自身就比较大,生成的镜像就更为臃肿,当我们的镜像非常多的时候,镜像的存储等开销就比较明显了。
三. 如果考虑到微服务、Serverless等新的架构和场景,Java自身的大小、内存占用、启动速度,都存在一定局限性,因为Java早期的优化大多是针对长时间运行的大型服务器端应用。
如果真的没做过,可以从操作系统、容器原理、JVM内部机制、软件开发实践等角度展示系统性分心新问题、新场景的能力,毕竟变化才是世界永恒的主题,能够在新变化中找出共性与关键,是优秀工程师的必备能力。
扩展
Java在容器环境的局限性来源,Docker到底有什么特别?
虽然看起来Docker之类容器和虚拟机非常相似,例如,有自己的shell,能独立安装软件包,运行时与其他容器互不干扰。但是深入分析会发现,Docker并不是一种完全的虚拟化技术,而更是一种轻量级的隔离技术。
1.从技术角度讲,基于namespace,Docker为每个容器提供了单独的命名空间,对网络,PID,用户,IPC通信,文件系统挂载点等实现了隔离。对于CPU\内存,磁盘IO等资源,则是通过ControlGroup等进行管理。
2.Docker仅在类似Linux内核之上实现了有限的 隔离和虚拟化,并不是像传统虚拟化软件那样,独立运行与一个新的操作系统。如果是虚拟化的操作系统,不管是Java还是其他程序,只要调用的是同一个系统API,都可以透明的获取所需的信息,基本不需要额外的兼容性改变。
容器虽然省略了虚拟操作系统的开销,实现了轻量级的目标,但也带来了额外复杂性,它限制对于应用是不透明的,需要用户理解Docker的新行为。所以有人曾说过,“幸运的是Docker没有完全隐藏底层信息,但是不幸的也是Docker没有隐藏底层信息”。
对于Java平台来说,这些未隐藏的底层信息带来很多意外的困难,只要体现在以下几个方面:
1). 容器环境对计算资源的管理方式是全新的,ControlGroup作为相对比较新的技术,历史版本的Java显然并不能自然的理解相应的资源机制。
2). namespace对于容器内的应用细节增加了一些微妙的差异,比如jcmd、jstack等工具会依赖于"/proc"下面提供的部分信息,但是Docker的设计改变了这部分信息的原有结构,我需要对原有工具进行修改以适应这种变化。
3.从JVM运行机制的角度,为什么这些沟通障碍会导致OOM问题?
这个问题实际反映了JVM如何根据系统资源(内存,CPU等)情况,在启动时设置默认参数。
这就是所谓的Ergonomics机制,例如:
1)JVM会根据检测到的内存大小,设置最初启动时的堆大小为系统内存的1/64;并将堆最大值,设置为系统内存的1/4
2)而JVM检测到系统的CPU核数,则直接影响到了Parallel GC的并行线程数目和JIT complier线程数目,甚至是我们应用中ForkJoinPool等机制的并行等级。
这些默认参数,是根据通用场景选择的初始值。但是由于容器环境的差异,Java的判断很可能是基于错误信息而做出的,这就类似我以为我住的是整栋别墅,实际上却只有一个房间是给我住的。
更加严重的是,JVM的一些原有诊断或备用机制也受到影响,为保证服务的可用性,一种常见的选择是依赖 -XX:OnOutOfMemoryError 功能,通过调用处理脚本的形式来做一些补救措施,比如自动重启服务等,但是这种机制是基于fork实现的,当Java进程已经过度提交内存时,fork新的进程往往已经不可能正常运行了。
该如何解决这些问题呢?
1))首先,如果能升级到最新的JDK版本,就一切ok了
针对这种情况,JDK9中引入了一些实验性的参数,以方便Docker和Java沟通,通过针对内存限制,可以使用下面的参数设置:
-XX:+UnlockExperimentaVMOptions
-XX:+UserCGroupMemoryLimitForHeap
这两个参数是顺序敏感的,并且只支持Linux环境。而对于CPU核数限定,Java已经可以正确理解-cpuset-cpus等设置,,无需单独设置参数。
2))如果可以切换到JDK10或者更新的版本,问题就更加简单了。Java对容器Docker的支持已经比较完善,默认就会自适应各种资源限制和实现差异,前面提到的实验性参数已经被标记为废弃。于此同时,新增了参数用以明确指定CPU核心的数目。
-xx:ActiveProcessorCount=N
如果实践中发现有问题,也可以使用-XX:UseContainerSupport,关闭Java容器的支持特性,这可作为一种防御型机制,避免新特性破坏原有功能。
幸运的是,JKD9中的实验性改进已经被移植到Oracle JDK 8u131中了,可以直接下载镜像,并配置UseCGroupMemoryLimitForHeap
3))如果暂时只能用JDK老版本怎么办?
第一:明确设置堆、元数据区等内存区域大小,保证Java进程的总大小可控,
限制容器内存: $ docker run -it --rm --name your container -p 8080:8080 -m 800M repo/your -java-container:openjdk
这样就可以额外配置下面的环境变量,直接指定JVM堆大小 -e JAVA_OPTIONS='xMX300m'
第二:明确GC和JIT并行线程数目,以避免二者占用过多资源。
-XX:ParallelGCTreads
-XX:CICompilerCount
第三:Java在Docker环境中会意外Swap。
当内存消耗达到一定门限,操作系统会试图将不活跃的进程唤出