作者 | Grzegorz Kocur

原文:https://blog.softwaremill.com/docker-support-in-new-java-8-finally-fd595df0ca54

出品 | CSDN,译者 | 苏本如,责编 | 屠敏

Java 8 过去一直与 Docker 无法很好地兼容,现在可让开发者们奔走相告的是,这个问题已经解决了。

请注意:本文中我使用的是遵循GNU GPL v2 许可授权的OpenJDK官方Docker镜像。这里描述的对Docker的支持在Oracle Java SE 开发工具包(JDK)版本8的更新191中被引入。Oracle在2019年4月修改了Java 8更新的许可政策,自Java SE 8更新211后的商业使用不再免费。

你是否曾经经历过在Docker中运行基于JVM的应用程序时出现“随机”故障?或者一些奇怪的死机?两者都有可能是由于Java 8(它仍然被广泛使用)中的糟糕的Docker支持引起。

Docker使用控制组(cgroups)来限制对资源的使用。在容器中运行应用程序时限制其对内存和CPU的使用绝对是一个好主意,它可以防止应用程序占用全部可用的内存和/或CPU,因而导致在同一系统上运行的其他容器无法响应。限制资源的使用可以提高应用程序的可靠性和稳定性。它还为硬件容量的规划提供了依据。在像诸如Kubernetes或DC/OS这样的编排系统上运行容器时,这一点尤为重要。

一、问题

JVM可以“看到”系统上所有可用的内存和CPU内核,并保持与这些资源的一致。在默认情况下,JVM会将max heap size(最大堆大小)设置为系统内存的1/4,并将一些线程池个数(比如说垃圾回收(GC))设置为与物理CPU内核的数量一致。我们一起来看看下面的例子。

我们将运行一个简单的应用程序,它将消耗尽可能多的内存(示例可以在这个站点上找到):


import java.util.Vector;
public class MemoryEater
{
  public static void main(String[] args)
  {
    Vector v = new Vector();
    while (true)
    {
      byte b[] = new byte[1048576];
      v.add(b);
      Runtime rt = Runtime.getRuntime();
      System.out.println( "free memory: " + rt.freeMemory() );
    }
  }
}

我们在内存为64GB的系统上运行它,让我们来检查一下默认的最大堆大小:


$ docker run -ti -m 512M openjdk:8u181-jdk
root@eca214e0fcd4:/# java -XX:+PrintFlagsFinal -version | grep MaxHeap
    uintx MaxHeapFreeRatio                          = 100                                 {manageable}
    uintx MaxHeapSize                              := 16819159040                         {product}
openjdk version "1.8.0_181"
OpenJDK Runtime Environment (build 1.8.0_181-8u181-b13-2~deb9u1-b13)
OpenJDK 64-Bit Server VM (build 25.181-b13, mixed mode)

如前所述,它应该是系统物理内存的1/4 (16GB)。如果我们使用Docker cgroups限制内存,会发生什么呢?让我们检查一下:


$ docker run -ti -m 512M openjdk:8u181-jdk
root@eca214e0fcd4:/# javac MemoryEater.java
Note: MemoryEater.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
root@eca214e0fcd4:/# java MemoryEater
free memory: 1003980048
free memory: 1003980048
free memory: 1003980048
[...]
free memory: 803562640
free memory: 802514048
free memory: 801465456
free memory: 800416864
Killed
root@eca214e0fcd4:/#

这个JVM进程被终止了。因为它是一个子进程,所以容器本身幸存下来,但是通常当Java是容器内的唯一进程(用PID 1)时,容器也会崩溃。46张PPT弄懂JVM、GC算法和性能调优,这个分享给你。

让我们深入研究一下系统日志中有什么:


Apr 18 16:18:46 dcos-agent-1 kernel: java invoked oom-killer: gfp_mask=0xd0, order=0, oom_score_adj=0
Apr 18 16:18:46 dcos-agent-1 kernel: java cpuset=eca214e0fcd4b245eecb2a80c05e9d7f8688fc36979c510d2fb9afab2ce55712 mems_allowed=0
Apr 18 16:18:46 dcos-agent-1 kernel: CPU: 6 PID: 4142 Comm: java Tainted: G               ------------ T 3.10.0-693.17.1.el7.x86_64 #1
Apr 18 16:18:46 dcos-agent-1 kernel: Hardware name: Supermicro Super Server/X10SRi-F, BIOS 2.0 12/17/2015
Apr 18 16:18:46 dcos-agent-1 kernel: Call Trace:
Apr 18 16:18:46 dcos-agent-1 kernel: [<ffffffff816a6071>] dump_stack+0x19/0x1b
Apr 18 16:18:46 dcos-agent-1 kernel: [<ffffffff816a1466>] dump_header+0x90/0x229
Apr 18 16:18:46 dcos-agent-1 kernel: [<ffffffff81187dc6>] ? find_lock_task_mm+0x56/0xc0
Apr 18 16:18:46 dcos-agent-1 kernel: [<ffffffff811f36a8>] ? try_get_mem_cgroup_from_mm+0x28/0x60
Apr 18 16:18:46 dcos-agent-1 kernel: [<ffffffff81188274>] oom_kill_process+0x254/0x3d0
Apr 18 16:18:46 dcos-agent-1 kernel: [<ffffffff812ba2fc>] ? selinux_capable+0x1c/0x40
Apr 18 16:18:46 dcos-agent-1 kernel: [<ffffffff811f73c6>] mem_cgroup_oom_synchronize+0x546/0x570
Apr 18 16:18:46 dcos-agent-1 kernel: [<ffffffff811f6840>] ? mem_cgroup_charge_common+0xc0/0xc0
Apr 18 16:18:46 dcos-agent-1 kernel: [<ffffffff81188b04>] pagefault_out_of_memory+0x14/0x90
Apr 18 16:18:46 dcos-agent-1 kernel: [<ffffffff8169f82e>] mm_fault_error+0x68/0x12b
Apr 18 16:18:46 dcos-agent-1 kernel: [<ffffffff816b3a21>] __do_page_fault+0x391/0x450
Apr 18 16:18:46 dcos-agent-1 kernel: [<ffffffff816b3b15>] do_page_fault+0x35/0x90
Apr 18 16:18:46 dcos-agent-1 kernel: [<ffffffff816af8f8>] page_fault+0x28/0x30
Apr 18 16:18:46 dcos-agent-1 kernel: Task in /docker/eca214e0fcd4b245eecb2a80c05e9d7f8688fc36979c510d2fb9afab2ce55712 killed as a result of limit of /docker/eca214e0fc
d4b245eecb2a80c05e9d7f8688fc36979c510d2fb9afab2ce55712
Apr 18 16:18:46 dcos-agent-1 kernel: memory: usage 524180kB, limit 524288kB, failcnt 314788
Apr 18 16:18:46 dcos-agent-1 kernel: memory+swap: usage 1048576kB, limit 1048576kB, failcnt 6
Apr 18 16:18:46 dcos-agent-1 kernel: kmem: usage 0kB, limit 9007199254740988kB, failcnt 0
Apr 18 16:18:46 dcos-agent-1 kernel: Memory cgroup stats for /docker/eca214e0fcd4b245eecb2a80c05e9d7f8688fc36979c510d2fb9afab2ce55712: cache:28KB rss:524152KB rss_huge
:0KB mapped_file:0KB swap:524396KB inactive_anon:262176KB active_anon:261976KB inactive_file:8KB active_file:4KB unevictable:0KB
Apr 18 16:18:46 dcos-agent-1 kernel: [ pid ]   uid  tgid total_vm      rss nr_ptes swapents oom_score_adj name
Apr 18 16:18:46 dcos-agent-1 kernel: [ 1400]     0  1400     4985      418      14      139             0 bash
Apr 18 16:18:46 dcos-agent-1 kernel: [ 4141]     0  4141  4956003   126966     606   137837             0 java
Apr 18 16:18:46 dcos-agent-1 kernel: Memory cgroup out of memory: Kill process 4162 (java) score 1012 or sacrifice child
Apr 18 16:18:46 dcos-agent-1 kernel: Killed process 4141 (java) total-vm:19824012kB, anon-rss:495748kB, file-rss:12116kB, shmem-rss:0kB

像这样的故障很难调试,因为应用程序日志中没有任何内容。在像AWS ECS这样的管理系统上,它可能尤其困难。

CPU怎么样?让我们运行一个显示可用处理器数量的小程序,来再一次看看会发生什么:


public class AvailableProcessors {
public static void main(String[] args) {
// check the number of processors available
      System.out.println(""+Runtime.getRuntime().availableProcessors());
   }
}

我们在一个CPU数量设置为1的Docker容器中运行这个小程序:


$ docker run -ti --cpus 1 openjdk:8u181-jdk
root@82080104994c:/# javac AvailableProcessors.java
root@82080104994c:/# java AvailableProcessors
12

不好!这个系统上实际有12个CPU。因此,即使将可用处理器的数量限制为1个,JVM也将尝试使用12个。例如,垃圾回收(GC)线程数量是基于以下公式设置的:

在具有n个硬件线程并且n大于8的计算机上,并行回收器使用一个固定的分数来设定垃圾回收器的线程数。当n大于8时,这个分数约为5/8。当n小于8时,垃圾回收器的线程数为n。

在案例中:


root@82080104994c:/# java -XX:+PrintFlagsFinal -version | grep ParallelGCThreads
    uintx ParallelGCThreads                         = 10                                  {product}

二、解决方案

好的,我们现在知道这个问题的存在了。那么有解决办案吗?幸运的是 - 有!

新的Java版本(10及以上)已经内置了Docker的支持功能。但有时升级并不能解决问题,比如说,如果应用程序与新的JVM不兼容就不行。推荐阅读:Docker 教程,详细到令人发指。

好消息是:对Docker的支持还被向后移植到Java 8。让我们运行下面人命令来检查标记为8u212的最新openjdk 镜像。我们将内存限制为1G,并使用1个CPU:


docker run -ti --cpus 1 -m 1G openjdk:8u212-jdk

内存:


root@843e552c2e49:/# java -XX:+PrintFlagsFinal -version | grep MaxHeap
    uintx MaxHeapFreeRatio                          = 70                                  {manageable}
    uintx MaxHeapSize                              := 268435456                           {product}
openjdk version "1.8.0_212"
OpenJDK Runtime Environment (build 1.8.0_212-8u212-b01-1~deb9u1-b01)
OpenJDK 64-Bit Server VM (build 25.212-b01, mixed mode)
root@843e552c2e49:/#

它是256M, 正好是已分配内存的1/4。

CPU:


root@16f12923f731:/# java AvailableProcessors
1

正如我们想要的那样。

此外,还有一些新设置:


-XX:InitialRAMPercentage
-XX:MaxRAMPercentage
-XX:MinRAMPercentage

这些设置允许微调 heap size(堆大小)。这些设置的含义在StackOverflow的这个优秀答案中已经得到了解释。请注意,它们设置的是百分比,而不是固定值。多亏了它,更改Docker内存设置不会破坏任何东西。

如果出于某种原因不需要新JVM的特性,可以使用-xx:-useContainerSupport关闭它。

三、结论

为基于JVM的应用程序设置正确的heap size(堆大小)是非常重要的。使用最新的Java 8版本,你可以依赖安全(但是非常保守)的默认设置。而不需要在Docker入口点中使用任何变通办法,也不需要再将Xmx设置为固定值。

祝大家使用 JVM愉快! 

关注Java技术栈微信公众号,在后台回复关键字:Java,可以获取更多栈长整理的Java技术干货。

最近干货分享

厉害了,Dubbo 正式毕业!

46张PPT弄懂JVM、GC算法和性能调优!

Intellij IDEA 解决撸码最头大的问题。

微服务配置中心对比,哪个更牛逼?

分享一份Java架构师学习资料

点击「阅读原文」一起搞技术,爽~

Java 8 终于支持 Docker !的更多相关文章

  1. Java 8 终于支持 Docker!

    Java 8曾经与Docker无法很好地兼容性,现在问题已消失. 请注意:我在本文中使用采用GNU GPL v2许可证的OpenJDK官方docker映像.在Oracle Java SE中,这里描述的 ...

  2. JAVA项目如何通过Docker实现Jenkins持续部署

    原文地址:http://blog.51cto.com/dadonggg/1957691 本篇实操性的案例讲解——JAVA项目如何通过Docker实现持续部署(只需简单四步), 即:开发通过git pu ...

  3. Java程序运行在Docker等容器环境有哪些新问题

    基本回答 一.  对于Java来说,Docker毕竟是一个较新的环境,其内存.CPU等资源限制是通过ControlGroup实现的.早期的JDK版本并不能识别这些限制,进而会导致一些基础问题. 1.如 ...

  4. C++builder XE10 终于支持类内变量初始化了

    Win32终于支持类内变量初始化了,C++11 用bcc32C编译器  llvm CLang.还支持Unicode 中文汉字 变量名. 用经典的bcc32编译还是不支持! class TPerson ...

  5. Eclipse 安装对 Java 8 的支持

    Java 8 正式版今天已经发布了(详情),但最常用的 Java 开发工具 Eclipse 还没有正式发布对 Java 8 的支持.不过目前可以通过更新 JDT 来支持 Java 8.步骤如下: 菜单 ...

  6. 微软Azure开始支持Docker技术

    前一段时间还在与微软的技术人员讨论媒体转换服务的效率问题,如果应用 Docker将会有质的提高,没想到国外的Azure已经开始支持了,相信国内Azure支持也不远了.微软正在努力确保Azure成为开发 ...

  7. Azure终于支持大容量虚拟机了-最高32核,448G内存

    Azure终于支持大容量虚拟机了-最高32核,448G内存 最近微软Azure虚拟机旗下的大容量G系列虚拟机通用版本正式上线.G系列虚拟机方案提供公有云领域最大的内存容量.最强处理能力以及空间可观的本 ...

  8. PyCharm 2017.2.3 版本在2017年9月7日发布,支持 Docker Compose

    PyCharm是由JetBrains打造的一款Python IDE.PyCharm具备用于一般IDE的功能,比如, 调试.语法高亮.Project管理.代码跳转.智能提示.自动完成.单元测试.版本控制 ...

  9. Atitit. 软件---多媒体区---- jmf 2.1.1 Java Media Framework 支持的格式

    Atitit. 软件---多媒体区---- jmf 2.1.1 Java Media Framework 支持的格式 JMF,全名为Java Media Framework,它可以在java appl ...

随机推荐

  1. Android组件内核之间组件间通信方案(四)下篇

    阿里P7Android高级架构进阶视频免费学习请点击:https://space.bilibili.com/474380680本篇文章将继续从以下两个内容来介绍通信方案: [ViewModel 与 V ...

  2. Oracle查看有锁进程,删除锁

    查看锁表进程SQL语句1: select sess.sid,     sess.serial#,     lo.oracle_username,     lo.os_user_name,     ao ...

  3. 归并排序(Merge_Sort)

    基本思想 建立在归并操作上的一种有效的排序算法.该算法是采用分治法(Divide and Conquer)的一个非常典型的应用. 算法原理 归并操作指的是将两个已经排序的序列合并成一个序列的操作,归并 ...

  4. Java中File类创建文件

    只需要调用该类的一个方法createNewFile(),但是在实际操作中需要注意一些事项,如判断文件是否存在,以及如何向新建文件中写入数据等. import java.io.*; public cla ...

  5. Centos 文件权限修改

    1.查看权限 # ls -l dirPath 2.修改权限,root权限执行(-R 子目录的权限都会改变) # chmod -R dirPath

  6. JavaScript常用技巧之数组操作

    1.获取最后数组中最后一个元素 . arr.slice(-1).pop() . arr[arr.length - 1] 2.过滤重复元素 arr.filter(function(v, i) { ret ...

  7. 1、Spring MVC的web.xml配置详解(转)

    版权声明:本文为博主原创文章,转载请注明出处http://blog.csdn.net/u010796790 1.spring 框架解决字符串编码问题:过滤器 CharacterEncodingFilt ...

  8. Ubuntu更换阿里云数据源

    sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak sudo vim /etc/apt/sources.list 将里面的内容全部删除修改成 ...

  9. JavaScript 中 reduce去重方法

    过去有很长一段时间,我一直很难理解 reduce() 这个方法的具体用法,平时也很少用到它.事实上,如果你能真正了解它的话,其实在很多地方我们都可以用得上,那么今天我们就来简单聊聊 JS 中 redu ...

  10. json数据返回数字,页面显示文字处理

    var obj = { 1:'你好1', 2:'你好2', 3:'你好3' } var e = obj[1]; e; //'你好1'