在 Docker 里跑 Java,趟坑总结

Java是一种可以撰写跨平台应用软件的面向对象的程序设计语言。Java 技术具有卓越的通用性、高效性、平台移植性和安全性,广泛应用于PC、数据中心、游戏控制台、科学超级计算机、移动电话和互联网,同时拥有全球最大的开发者专业社群。

给你java学习路线:html-css-js-jq-javase-数据库-jsp-servlet-Struts2-hibernate-mybatis-spring4-springmvc-ssh-ssm

背景:众所周知,当我们执行没有任何调优参数(如“java-jar mypplication-fat.jar”)的 Java 应用程序时,JVM 会自动调整几个参数,以便在执行环境中具有最佳性能。

但是许多开发者发现,如果让 JVM ergonomics (即JVM人体工程学,用于自动选择和行为调整)对垃圾收集器、堆大小和运行编译器使用默认设置值,运行在 Linux 容器(docker,rkt,runC,lxcfs 等)中的 Java 进程会与我们的预期表现严重不符。

本篇文章采用简单的方法来向开发人员展示在 Linux 容器中打包 Java 应用程序时应该知道什么。

懒人超精简阅读版:

a.JVM 做不了内存限制,一旦超出资源限制,容器就会出错

b.即使你多给些内存资源,也没什么卵用,只会错上加错

c.解决方案:用 Dockfile 中的环境变量来定义 JVM 的额外参数

d.更进一步:使用由 Fabric8 社区提供的基础 Docker 镜像来定义 Java 应用程序,将始终根据容器调整堆大小

详细全文:

我们往往把容器当虚拟机,让它定义一些虚拟 CPU 和虚拟内存。其实容器更像是一种隔离机制:它可以让一个进程中的资源(CPU,内存,文件系统,网络等)与另一个进程中的资源完全隔离。Linux 内核中的 cgroups 功能用于实现这种隔离。

然而,一些从执行环境收集信息的应用程序已经在 cgroups 存在之前就被执行了。“top”,“free”,“ps”,甚至 JVM 等工具都没有针对在容器内执行高度受限的 Linux 进程进行优化。

1.存在的问题

为了演示,我用“docker-machine create -d virtualbox –virtualbox-memory ‘1024’ docker1024”在1GB RAM 的虚拟机中创建了 docker daemon。接下来,在一个虚拟内存为100MB 的容器里面跑三个不同的Linux distribution,执行 “free -h”命令,结果是:它们都显示了995MB 的总内存。

小编推荐一个学Java的学习裙【 六五零,五五四,六零七 】,无论你是大牛还是小白,是想转行还是想入行都可以来了解一起进步一起学习!裙内有开发工具,很多干货和技术资料分享!

即使在 Kubernetes / OpenShift 集群中,结果也类似。

我在一个15GB 内存的集群中跑一个 Kubernetes Pod ,并将 Pod 的内存限制为512M (通过“kubectl run mycentos –image=centos -it –limits=’memory=512Mi'”命令实现),最后显示的总内存却是14GB

如果想知道为什么会发生这种情况

docker switches(-m,-memory和-memory-swap)和kubernetes switch(–limits)在进程超过限制的情况下,会指示 Linux 内核杀死该进程;但 JVM 是完全不知道限制,所以在进程超过限制的时候,糟糕的事情就发生了!

为了模拟在超过指定的内存限制后被杀死的进程,我们可以通过“docker run -it –name mywildfly -m=50m jboss/wildfly” 命令在50MB 内存限制的容器中跑WildFly应用 server,用 “dockerstats” 命令来检查容器限制。

但是在几秒钟之后,Wildfly 的容器执行将被中断并显示:*** JBossAS process (55) received KILL signal ***

“docker inspect mywildfly -f ‘{{json.State}}'” 命令显示由于 OOM(内存不足),该容器已被杀死。注意容器 “state” 中的OOMKilled = true。

2.JAVA的应用程序是如何被影响的?

在docker daemon里用 Dockerfile 中定义的参数-XX:+ PrintFlagsFinal和-XX:+ PrintGCDetails起一个 java 应用。

其中 machine:1GB RAM 容器内存:限制为150M (对于这个Spring Boot应用,似乎够用)

这些参数允许我们读取初始JVM人机工程学参数,并了解有关垃圾收集(GC)执行的详细信息。

动手试一下:

我已经在“/ api / memory /”上准备了一个端点,它使用 String 对象加载 JVM 内存来模拟消耗大量内存的操作。我们来调用一次:

此端点将回复“分配超过80%(219.8 MiB)的最大允许 JVM 内存大小(241.7 MiB)”

在这里我们可以提至少两个问题:

为什么JVM最大允许内存241.7 MiB?

如果这个容器将内存限制为150MB,那为什么它允许Java分配近220MB?

首先,我们需要回顾一下 JVM 人机工程学页面上关于“最大堆大小”的内容:是物理内存的1/4。由于 JVM 不知道它在一个容器内执行,所以允许最大堆大小将接近260MB。鉴于我们在容器初始化期间添加了-XX:+ PrintFlagsFinal标志,我们可以检查这个值:

其次,我们需要了解,当我们在 docker 命令行中使用参数“-m 150M”时,docker daemon将在RAM中限制150M ,在 Swap 中限制为150M。因此,该过程可以分配300M。这就解释了为什么我们的进程没有被杀死。

docker 命令行中的内存限制(-memory)和swap(-memory-swap)之间的更多组合可以在这里

3.提供更多内存是否靠谱?

不了解问题的开发者往往认为环境不能为执行 JVM 提供足够的内存。所以通常的解决办法是提供更多内存,这实际上会使事情变得更糟。

我们假设将 daemon 从1GB 更改为8GB (使用“docker-machinecreate -d virtualbox –virtualbox-memory ‘8192’ docker8192”创建),并将容器内存从150M 更改为800M :

请注意这次, “curl http://`docker-machine ipdocker8192`:8080/api/memory” 命令甚至没有执行完,因为在8GB 环境中计算的 JVM 的MaxHeapSize 为2092957696字节(〜2GB)。检查 “docker logs mycontainer|grep -i MaxHeapSize”

小编推荐一个学Java的学习裙【 六五零,五五四,六零七 】,无论你是大牛还是小白,是想转行还是想入行都可以来了解一起进步一起学习!裙内有开发工具,很多干货和技术资料分享!

该应用将尝试分配超过1.6GB 的内存,这超出了此容器的限制(RAM 中的800MB + Swap中的800MB),并且该进程将被杀掉。

很显然,用增加内存且让 JVM 自定义参数的方式在容器里跑Java,不是什么好主意 在容器内部运行 Java 应用程序时,我们应该根据应用程序需求和容器限制设置最大堆大小(-Xmx参数)。

4.解决方案

Dockerfile 的一个细微变化允许用户指定一个环境变量来定义 JVM 的额外参数。 检查以下行:

现在我们可以使用 JAVA_OPTIONS 环境变量来通知 JVM 堆的大小。对于这个应用程序,300M 就够了。稍后可以检查日志并获取314572800字节(300MBi)的值

对于docker,您可以使用“-e”switch指定环境变量。

在Kubernetes中,您可以使用switch “-env = [key = value]”设置环境变量:

再进一步

如果可以根据容器限制自动计算堆的值,该怎么做?

使用由Fabric8社区提供的基础Docker镜像,就可以搞定。这个镜像 fabric8 / java-jboss-openjdk8-jdk 使用一个脚本来计算容器限制,并使用50%的可用内存作为上限。 请注意,这个50%的内存比可以被复写。 您还可以使用此镜像来启用/禁用调试,诊断等。

下面一起看看 Dockerfile 是如何作用于这个 Spring Boot 应用程序:

搞定!现在,无论容器内存限制是多少,我们的 Java 应用程序将始终根据容器调整堆大小,而不是根据 daemon 调整堆大小。

小编推荐一个学Java的学习裙【 六五零,五五四,六零七 】,无论你是大牛还是小白,是想转行还是想入行都可以来了解一起进步一起学习!裙内有开发工具,很多干货和技术资料分享!

5.结论

直到现在,Java JVM依然没有提供什么支持,让大家可以理解它在容器内是如何运行的,而且它有一些资源是内存和CPU限制的。 因此,您不能让JVM人体工程学本身决定最大堆大小。

解决此问题的一种方法是使用能够理解它在受限容器内运行的Fabric8 Base镜像

在JVM中有一个实验支持,已经包含在JDK9中以支持容器(即Docker)环境中的cgroup内存限制。可以参考

原文评论:更好的方法是以 exec 表单定义您的 CMD 指令,这将确保 java 是PID 1进程 - 这对于允许 Java 在容器停止时正常关闭至关重要。

Exec表单不支持环境变量替换,但您可以通过设置JAVA_TOOL_OPTIONS环境变量来传递其他命令行标志

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容