这里的容器是一个代称包含cgroup,docker等。
java在容器环境的问题
一个在物理机上跑的很好的java程序,如果用的jdk8u131之前的版本,加上容器限制后,可能会出现奇奇怪怪的问题,有的会启动失败,有的线程数远远超过限制的核数,cpu飙高。究其原因,本质是jvm的默认配置所致。
- 与cpu相关的默认配置。如gc线程数,jit线程数,forkjoin并发度等。
- 与内存相关的默认配置。如heap,direct memory等。
下面主要围绕cpu,内存展开。
java环境现状
jdk 8已经发布8u333。已经可以做到物理机跑的java程序,加上容器限制后,相当于跑在了对应资源的物理机上的效果。主要起作用的参数是
-XX:+UseContainerSupport
这个参数默认是打开的。这里就不介绍一些过渡的一些容器参数了。下面我们来解析一下这个参数是如何起到作用的。
UseContainerSupport的实现
识别容器限制
optResult = determineType("/proc/self/mountinfo", "/proc/cgroups", "/proc/self/cgroup");
代码主要围绕着3个文件来进行读取。就是上面方法中的3个参数。我们后面根据代码来看看这3个文件的作用。
了解cgroup的同学,了解资源的限制是写在了文件路径下的。所以代码的核心的就是去找到文件路径。填充如下的class数据数据.
public class CgroupInfo {
private final String name;
private final int hierarchyId;
private final boolean enabled;
private String mountPoint;
private String mountRoot;
private String cgroupPath;
}
第一步从 /proc/cgroups从获取子系统的信息。
jdk默认识别的子系统有6种,他们是作为常量写在java文件中的。
private static final String CPU_CTRL = "cpu";
private static final String CPUACCT_CTRL = "cpuacct";
private static final String CPUSET_CTRL = "cpuset";
private static final String BLKIO_CTRL = "blkio";
private static final String MEMORY_CTRL = "memory";
private static final String PIDS_CTRL = "pids";
第二步是从/proc/self/mountinfo获取mountpoint,mountroot。
这里jdk直接匹配了正则。
private static final Pattern MOUNTINFO_PATTERN = Pattern.compile(
"^[^\\s]+\\s+[^\\s]+\\s+[^\\s]+\\s+" + // (1), (2), (3)
"([^\\s]+)\\s+([^\\s]+)\\s+" + // (4), (5) - group 1, 2: root, mount point
"[^-]+-\\s+" + // (6), (7), (8)
"([^\\s]+)\\s+" + // (9) - group 3: filesystem type
".*$"); // (10), (11)
这里的信息,我们可以通过自己cat或者注释来看。
/*
* From https://www.kernel.org/doc/Documentation/filesystems/proc.txt
*
* 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue
* (1)(2)(3) (4) (5) (6) (7) (8) (9) (10) (11)
*
* (1) mount ID: unique identifier of the mount (may be reused after umount)
* (2) parent ID: ID of parent (or of self for the top of the mount tree)
* (3) major:minor: value of st_dev for files on filesystem
* (4) root: root of the mount within the filesystem
* (5) mount point: mount point relative to the process's root
* (6) mount options: per mount options
* (7) optional fields: zero or more fields of the form "tag[:value]"
* (8) separator: marks the end of the optional fields
* (9) filesystem type: name of filesystem of the form "type[.subtype]"
* (10) mount source: filesystem specific information or "none"
* (11) super options: per super block options
*/
在获取到mountpoint,mountroot之后,我们还差最后一个cgroupPath。
这个数据就是从/proc/self/cgroup中获取。
/proc/self/cgroup的数据如下
/*
* Sets the path to the cgroup controller for cgroups v1 based on a line
* in /proc/self/cgroup file (represented as the 'tokens' array).
*
* Note that multiple controllers might be joined at a single path.
*
* Example:
*
* 7:cpu,cpuacct:/system.slice/docker-74ad896fb40bbefe0f181069e4417505fffa19052098f27edf7133f31423bc0b.scope
*
* => tokens = [ "7", "cpu,cpuacct", "/system.slice/docker-74ad896fb40bbefe0f181069e4417505fffa19052098f27edf7133f31423bc0b.scope" ]
*/
通过切割字符串,我们获取到了cgroupPath。
通过以上的3个操作,我们最终构造出了完整的cgroup路径,后面的部分就是根据路径读取配置的文件名即可。
cpu配置相关影响
目前cpu的设置,主要是3种使用方式,第一种是设置quota,第二种是设置share比率,第三种是设置cpu具体的核。以下就围绕着三种情况进行展开。
quota模式
if (quota > -1 && period > 0) {
quota_count = ceilf((float)quota / (float)period);
log_trace(os, container)("CPU Quota count based on quota/period: %d", quota_count);
}
在设置了quota和period。jdk就用quota/period作为cpu的核数。这里的处理是虚拟的核数。因为period的周期是可以调整的,是否真的用到了对应倍数的物理机核,这里其实是不确定的。
share模型
在share模式下,只是设置了竞争cpu的比率。竞争强跑满了,share才有限制,如果都跑不满,限制是不存在的。在这种情况下,就需要用户做一些自己系统的预判。第一个设置是UseContainerCpuShares,开启这个参数后,就会读取cpu.shares的配置值,然后和1024(默认值做倍数关系)
if (share > -1) {
share_count = ceilf((float)share / (float)PER_CPU_SHARES);
log_trace(os, container)("CPU Share count based on shares: %d", share_count);
}
这种就是模拟大家都跑满的场景。并且希望设置核数比率的时候,是按照默认值的倍数来进行设置。否者jdk拿到的核数是不对的。这里只是jdk自己的限制,cgroup并没有这么要求。
如果不开启UseContainerCpuShares,share就会用系统的核数。这里模拟的就是程序竞争不激烈的情况。
cpu_count = limit_count = os::Linux::active_processor_count();
直接限制物理核
通过cgroup设置cpuset.cpus,可以固定使用具体某几个核。这种设置在系统函数中就会返回被限制后的。
cpu_count = limit_count = os::Linux::active_processor_count();
混合模式
以上三种模式,是可以混合在一起设置的。我们来继续看看,当出现混合设置的时候,如何去做cpu个数的计算。
先处理share和quota
这里的share是开启了UseContainerCpuShares的情况,没开启的时候拿到的核数和设置了cpuset.cpus是同一个逻辑。同时引入了一个新的参数PreferContainerQuotaForCPUCount,这个参数开启了就会直接使用quota值。否则会根据share和quota谁的比较小来做判断。
if (PreferContainerQuotaForCPUCount) {
limit_count = quota_count;
} else {
limit_count = MIN2(quota_count, share_count);
}
最后需要和cpuset.cpus的结果在做对比。这里注意的一点是,share不开启UseContainerCpuShares之后获取物理核的方法和cpuset.cpus是同一个,所以接下来的对比是包含了2中情况的,但是代码是同一处。
第一种情况是没有cpuset.cpus,那么对比的就是和物理机的核数。
第二种情况是有cpuset.cpus,对比是就是cpuset.cpus的值,并且cpuset.cpus的值一定比物理机核数小,同时也就保证了怎么都不会超过物理真实核数。
result = MIN2(cpu_count, limit_count);
依旧最终选择最小。这里也就发现了一个点,最大值不能超过物理机核数,现在看第一个quota模式,如果period只有正常cpu的一半,然后想用光所有cpu,就必须把quota扩大一倍。其实这样的比率设置和跑满没有区别。这里建议peroid的设置还是和cpu的时钟保值一致最好。这样的比率就接近于物理机的真是核数了。
内存配置相关影响
设置了cgroup主要影响heap的默认值。direct memory默认是xmx-s0的大小,他是被间接影响的。
void Arguments::set_heap_size() {
julong phys_mem;
…………
if (override_coop_limit) {
if (FLAG_IS_DEFAULT(MaxRAM)) {
phys_mem = os::physical_memory();
FLAG_SET_ERGO(MaxRAM, (uint64_t)phys_mem);
} else {
phys_mem = (julong)MaxRAM;
}
} else {
phys_mem = FLAG_IS_DEFAULT(MaxRAM) ? MIN2(os::physical_memory(), (julong)MaxRAM)
: (julong)MaxRAM;
}
…………
julong reasonable_max = (julong)((phys_mem * MaxRAMPercentage) / 100);
const julong reasonable_min = (julong)((phys_mem * MinRAMPercentage) / 100);
if (reasonable_min < MaxHeapSize) {
// Small physical memory, so use a minimum fraction of it for the heap
reasonable_max = reasonable_min;
} else {
// Not-small physical memory, so require a heap at least
// as large as MaxHeapSize
reasonable_max = MAX2(reasonable_max, (julong)MaxHeapSize);
}
…………
从上面的代码,我们可以看出xmx默认是按照phys_mem的比率来的,因为MaxHeapSize默认64m,所以我们接触到的场景的批示else分支的reasonable_max,MaxRAMPercentage根据其他默认值计算是25.最终是phys_mem的四分之一。
phys_mem的获取从os::physical_memory()来
julong os::physical_memory() {
jlong phys_mem = 0;
if (OSContainer::is_containerized()) {
jlong mem_limit;
if ((mem_limit = OSContainer::memory_limit_in_bytes()) > 0) {
log_trace(os)("total container memory: " JLONG_FORMAT, mem_limit);
return mem_limit;
}
log_debug(os, container)("container memory limit %s: " JLONG_FORMAT ", using host value",
mem_limit == OSCONTAINER_ERROR ? "failed" : "unlimited", mem_limit);
}
phys_mem = Linux::physical_memory();
log_trace(os)("total system memory: " JLONG_FORMAT, phys_mem);
return phys_mem;
}
如果是容器的环境下,返回的是memory_limit_in_bytes的设置。
现有的不足
容器问题并没有完全解决,目前还有如下已知问题。
- jmx getProcessCpuTime指标在使用share mode的时候,运行一段时间会返回-1。
目前这个bug已经在19,17,11上修复。但是jdk8的还没修复。pr还没有被review
https://github.com/openjdk/jdk8u-dev/pull/105 - 当在容器环境下,jvm启动不检测xmx是否有效。jvm进程设置了xmx,但是内存默认是懒加载,所以不会里面占用那么多,但容器给不了那么多的资源,最后会在运行时被kill掉。如果改成提前加载的模式 -XX:+AlwaysPreTouch,在启动时就知道heap的内存是否足够。相关讨论在https://github.com/openjdk/jdk/pull/8256
- 目前没有容器配置的jmx指标暴露。目前还在社区讨论https://github.com/openjdk/jdk/pull/9372。不过能合并进去的概率不大,建议使用第三方的https://github.com/xpbob/containerJmx。支持jdk11+.
网友评论