放在最前面,在写文章之后若干天,机缘巧合被毕昇JDK社区邀请去Compiler SIG做分享,偷懒把这篇文章润色了一下当做了分享主题,slide比文章内容更加丰富,可以直接下载Slide文件slides-docker.rar
/ 去看会议录屏(原谅我的渣渣口音),文章就不用看了,润色的内容没加到文章中去(别问为什么,问就是懒).
首先说一个老生常谈的限制:我们在对Docker中的Java应用使用诸如jmap等命令时常常会报错:
Can't attach to the process: ptrace(PTRACE_ATTACH, ..).
这个主要是因为像jstack、jmap等工具主要是通过两种方式来实现的:
- Attach机制(也可以叫做Vitural Machine.attach(),主要是用通过Socket 与目标JVM的Attach Listener线程进行交互,详情可看笨神文章《JVM源码分析之Attach机制实现完全解读》).
- Serviceability Agent(其实也是一种Attach,在Linux中要靠系统调用ptrace来实现).
而 Docker 自 1.10 版本开始,默认的 seccomp 配置文件中禁用了 ptrace,所以一些通过SA进行的操作如:jmap -heap就会报错,而Docker官方也给出了解决方法:
- 使用–cap-add=SYS_PTRACE明确添加指定功能:
[docker run --cap-add=SYS_PTRACE ...]
- 关闭 seccomp /将ptrace添加到允许的名单中:
docker run --security-opt seccomp:unconfined ...
除了这个限制,前一段时间我在翻JDK的JDK BUG SYSTEM的时候无意间发现了这么一个Bug:JDK-8140793
getAvailableProcessors may incorrectly report the number of cpus in Docker container
BUG大致描述的现象是,Java在Docker容器中运行时,获取到的CPU的数目可能是不正确的。
Docker大家都知道是依托于Cgroups和Namespace的,而Cgroups 是一种 Linux 内核功能,可以限制和隔离进程的资源使用情况(CPU、内存、磁盘 I/O、网络等),所以我猜可能是JVM在运行时并没有读取到Docker使用Cgroups进行的限制.
继续查看这个BUG,发现状态是RESOLVED,于是继续翻找,在官方的Blog中发现了这么一篇文章
:《Java SE support for Docker CPU and memory limits》(文章关联了反应Docker中CPU计算出错的JDK-8140793、Docker中内存限制的增强JDK-8170888、容器检测和资源配置使用率增强的JDK-8146115).
文章中提到在JDK8u121之前的版本中(Java SE 8u121 and earlier),JVM读取的CPU数以及内存等都是不受到Cgroups限制的数据,那么这么做又会出现什么问题呢?据我所知,在我们不显式的指明一些参数的时候,往往会用到JVM读取的数据做一些默认的配置。比如如果不显式的指定 -XX:ParallelGCThreads and -XX:CICompilerCount,那么JVM就会根据读到的CPU数目进行计算来设置数值,如在计算Parallel GC的Threads数目的地方 runtime\vm_version.cpp
(以下基于openJDK1.8 b120):
if (FLAG_IS_DEFAULT(ParallelGCThreads)) {
assert(ParallelGCThreads == 0, "Default ParallelGCThreads is not 0");
// For very large machines, there are diminishing returns
// for large numbers of worker threads. Instead of
// hogging the whole system, use a fraction of the workers for every
// processor after the first 8. For example, on a 72 cpu machine
// and a chosen fraction of 5/8
// use 8 + (72 - 8) * (5/8) == 48 worker threads.
unsigned int ncpus = (unsigned int) os::active_processor_count();
return (ncpus <= switch_pt) ?
ncpus :
(switch_pt + ((ncpus - switch_pt) * num) / den);
} else {
return ParallelGCThreads;
}
进入到获取CPU数目的os::active_processor_count()(linux实现os_linux.cpp)
int os::active_processor_count() {
// Linux doesn't yet have a (official) notion of processor sets,
// so just return the number of online processors.
int online_cpus = ::sysconf(_SC_NPROCESSORS_ONLN);
assert(online_cpus > 0 && online_cpus <= processor_count(), "sanity check");
return online_cpus;
}
我们发现确实是通过::sysconf(_SC_NPROCESSORS_ONLN)来读取的物理机的CPU,如此看来GC的线程数目的计算就会出现一定的问题,同理JIT compiler threads也会遇到同样的问题。
而除了CPU的读取会出错,内存也是如此,我们在不显式的指定一些参数时如-Xmx(MaxHeapSize)、-Xms(InitialHeapSize)时,JVM会根据它读取到的机器的内存大小做一些默认的设置如:
void Arguments::set_heap_size() {
if (!FLAG_IS_DEFAULT(DefaultMaxRAMFraction)) {
// Deprecated flag
FLAG_SET_CMDLINE(uintx, MaxRAMFraction, DefaultMaxRAMFraction);
}
const julong phys_mem =
FLAG_IS_DEFAULT(MaxRAM) ? MIN2(os::physical_memory(), (julong)MaxRAM)
: (julong)MaxRAM;
// If the maximum heap size has not been set with -Xmx,
// then set it as fraction of the size of physical memory,
// respecting the maximum and minimum sizes of the heap.
if (FLAG_IS_DEFAULT(MaxHeapSize)) {
julong reasonable_max = phys_mem / MaxRAMFraction;
if (phys_mem <= MaxHeapSize * MinRAMFraction) {
// Small physical memory, so use a minimum fraction of it for the heap
reasonable_max = phys_mem / MinRAMFraction;
}
.
.
.
.
}
}
其中读取内存的os::physical_memory()读取也是physical memory,而这在Docker中运行可能引发一系列的错误比如被OOMKiller给杀掉(参考).
可见当我们使用一些比较老的JDK8版本时,如果我们没有显式指定一些参数可能会遇到一些稀奇古怪的问题,我在JDK-8146115中发现此对Docker支付的增强已经在JDK10中实现了,使用-XX:+UseContainerSupport可以开启容器支持,而且这一增强已经被backport到了JDK8的一些新版本中(JDK8u131之后的版本).
我下载了新版本的OpenJDK8,翻阅源码发现Oracle果然做了相应的处理.
原先os::active_processor_count()变成了:
// Determine the active processor count from one of
// three different sources:
//
// 1. User option -XX:ActiveProcessorCount
// 2. kernel os calls (sched_getaffinity or sysconf(_SC_NPROCESSORS_ONLN)
// 3. extracted from cgroup cpu subsystem (shares and quotas)
//
// Option 1, if specified, will always override.
// If the cgroup subsystem is active and configured, we
// will return the min of the cgroup and option 2 results.
// This is required since tools, such as numactl, that
// alter cpu affinity do not update cgroup subsystem
// cpuset configuration files.
int os::active_processor_count() {
// User has overridden the number of active processors
if (ActiveProcessorCount > 0) {
if (PrintActiveCpus) {
tty->print_cr("active_processor_count: "
"active processor count set by user : %d",
ActiveProcessorCount);
}
return ActiveProcessorCount;
}
int active_cpus;
if (OSContainer::is_containerized()) {
active_cpus = OSContainer::active_processor_count();
if (PrintActiveCpus) {
tty->print_cr("active_processor_count: determined by OSContainer: %d",
active_cpus);
}
} else {
active_cpus = os::Linux::active_processor_count();
}
return active_cpus;
}
可以清晰的看到,如果有-XX:ActiveProcessorCount参数则使用参数,如果没有就会去OSContainer::is_containerized()判断是否容器化:
inline bool OSContainer::is_containerized() {
assert(_is_initialized, "OSContainer not initialized");
return _is_containerized;
}
而_is_containerized是由Threads::create_vm调用OSContainer::init()时检查虚拟机是否运行在容器中得来的(具体方法太长了):
/* init
*
* Initialize the container support and determine if
* we are running under cgroup control.
*/
void OSContainer::init() {
int mountid;
int parentid;
int major;
int minor;
FILE *mntinfo = NULL;
FILE *cgroup = NULL;
char buf[MAXPATHLEN+1];
char tmproot[MAXPATHLEN+1];
char tmpmount[MAXPATHLEN+1];
char tmpbase[MAXPATHLEN+1];
char *p;
jlong mem_limit;
assert(!_is_initialized, "Initializing OSContainer more than once");
_is_initialized = true;
_is_containerized = false;
_unlimited_memory = (LONG_MAX / os::vm_page_size()) * os::vm_page_size();
if (PrintContainerInfo) {
tty->print_cr("OSContainer::init: Initializing Container Support");
}
if (!UseContainerSupport) {
if (PrintContainerInfo) {
tty->print_cr("Container Support not enabled");
}
return;
}
...........
_is_containerized = true;
}
方法就是对一些地方做了检查,如UseContainerSupport参数是否开启、/proc/self/mountinfo、/proc/self/cgroup是否可读等等,如果判断JVM运行在容器中,那么就会调用OSContainer::active_processor_count()获取容器限制的CPU数目:
/* active_processor_count
*
* Calculate an appropriate number of active processors for the
* VM to use based on these three inputs.
*
* cpu affinity
* cgroup cpu quota & cpu period
* cgroup cpu shares
*
* Algorithm:
*
* Determine the number of available CPUs from sched_getaffinity
*
* If user specified a quota (quota != -1), calculate the number of
* required CPUs by dividing quota by period.
*
* If shares are in effect (shares != -1), calculate the number
* of CPUs required for the shares by dividing the share value
* by PER_CPU_SHARES.
*
* All results of division are rounded up to the next whole number.
*
* If neither shares or quotas have been specified, return the
* number of active processors in the system.
*
* If both shares and quotas have been specified, the results are
* based on the flag PreferContainerQuotaForCPUCount. If true,
* return the quota value. If false return the smallest value
* between shares or quotas.
*
* If shares and/or quotas have been specified, the resulting number
* returned will never exceed the number of active processors.
*
* return:
* number of CPUs
*/
int OSContainer::active_processor_count() {
int quota_count = 0, share_count = 0;
int cpu_count, limit_count;
int result;
cpu_count = limit_count = os::Linux::active_processor_count();
int quota = cpu_quota();
int period = cpu_period();
int share = cpu_shares();
...........
}
通过注释发现,此时的计算是通过cgroup cpu quota & cpu period、cgroup cpu shares得来的,而Docker可以通过–cpu-period、–cpu-quota等来进行设置。
同理,对于Memory的处理,如果不标明-Xmx,JVM可以开启-XX:+UnlockExperimentalVMOptions、 -XX:+UseCGroupMemoryLimitForHeap这两个参数,来使得JVM使用Linux cgroup的配置确定最大Java堆大小。
Arguments::set_heap_size()方法:
void Arguments::set_heap_size() {
if (!FLAG_IS_DEFAULT(DefaultMaxRAMFraction)) {
// Deprecated flag
FLAG_SET_CMDLINE(uintx, MaxRAMFraction, DefaultMaxRAMFraction);
}
julong phys_mem =
FLAG_IS_DEFAULT(MaxRAM) ? MIN2(os::physical_memory(), (julong)MaxRAM)
: (julong)MaxRAM;
// Experimental support for CGroup memory limits
if (UseCGroupMemoryLimitForHeap) {
// This is a rough indicator that a CGroup limit may be in force
// for this process
const char* lim_file = "/sys/fs/cgroup/memory/memory.limit_in_bytes";
FILE *fp = fopen(lim_file, "r");
if (fp != NULL) {
julong cgroup_max = 0;
int ret = fscanf(fp, JULONG_FORMAT, &cgroup_max);
if (ret == 1 && cgroup_max > 0) {
// If unlimited, cgroup_max will be a very large, but unspecified
// value, so use initial phys_mem as a limit
if (PrintGCDetails && Verbose) {
// Cannot use gclog_or_tty yet.
tty->print_cr("Setting phys_mem to the min of cgroup limit ("
JULONG_FORMAT "MB) and initial phys_mem ("
JULONG_FORMAT "MB)", cgroup_max/M, phys_mem/M);
}
phys_mem = MIN2(cgroup_max, phys_mem);
} else {
warning("Unable to read/parse cgroup memory limit from %s: %s",
lim_file, errno != 0 ? strerror(errno) : "unknown error");
}
fclose(fp);
} else {
warning("Unable to open cgroup memory limit file %s (%s)", lim_file, strerror(errno));
}
}
....................
}
JVM会通过使用cgroup文件系统中的memory_limit()值初始化os::physical_memory()中的值,不过我有注意到注释上有Experimental support的字样,估计不太成熟哈哈还只是实验性质的支持。
这么看Java在Docker中运行小坑与限制还不少呢,不知道哪里设置的不好就会出现一些莫名其妙的问题,我们最好还是根据Docker的配置来显式设置JVM的参数以避免大部分问题。如果还是有问题,可以考虑下升级较高版本的JDK8u,如果成本高不想升级请参考方案来外部加载一些库进行拦截修改。