美文网首页
java与容器的小火花

java与容器的小火花

作者: xpbob | 来源:发表于2022-08-28 15:24 被阅读0次

    这里的容器是一个代称包含cgroup,docker等。

    java在容器环境的问题

    一个在物理机上跑的很好的java程序,如果用的jdk8u131之前的版本,加上容器限制后,可能会出现奇奇怪怪的问题,有的会启动失败,有的线程数远远超过限制的核数,cpu飙高。究其原因,本质是jvm的默认配置所致。

    1. 与cpu相关的默认配置。如gc线程数,jit线程数,forkjoin并发度等。
    2. 与内存相关的默认配置。如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的设置。

    现有的不足

    容器问题并没有完全解决,目前还有如下已知问题。

    1. jmx getProcessCpuTime指标在使用share mode的时候,运行一段时间会返回-1。
      目前这个bug已经在19,17,11上修复。但是jdk8的还没修复。pr还没有被review
      https://github.com/openjdk/jdk8u-dev/pull/105
    2. 当在容器环境下,jvm启动不检测xmx是否有效。jvm进程设置了xmx,但是内存默认是懒加载,所以不会里面占用那么多,但容器给不了那么多的资源,最后会在运行时被kill掉。如果改成提前加载的模式 -XX:+AlwaysPreTouch,在启动时就知道heap的内存是否足够。相关讨论在https://github.com/openjdk/jdk/pull/8256
    3. 目前没有容器配置的jmx指标暴露。目前还在社区讨论https://github.com/openjdk/jdk/pull/9372。不过能合并进去的概率不大,建议使用第三方的https://github.com/xpbob/containerJmx。支持jdk11+.

    相关文章

      网友评论

          本文标题:java与容器的小火花

          本文链接:https://www.haomeiwen.com/subject/dofcnrtx.html