(转)在Docker中运行Java:为了防止失败,你需要知道这些

转自:https://mp.weixin.qq.com/s?__biz=MzA5OTAyNzQ2OA==&mid=2649693848&idx=1&sn=4e9ef7e2a9d41b39985899b6ad146298&chksm=889321fbbfe4a8ed58d09e6bcf2f9c2603859c331489c0a8a56b8050e601438415b1398fc1f6&mpshare=1&scene=1&srcid=0419cxIPfJTpcCFP1FDn9cSs&key=7bae48d5a88e60c5ec76efea9c2ee66bec96c6478a3e8f6ce4cfe8531486f2f75c26ece7cd8aff63cdca6913da9eea809fe3561f22dc0bcbab552a20cf3c66bceb7faee2afc241d1742d38d396573d90&ascene=0&uin=MTc0NzU1NQ%3D%3D&devicetype=iMac+MacBookPro11%2C3+OSX+OSX+10.12.2+build(16C68)&version=12010210&nettype=WIFI&fontScale=100&pass_ticket=68WphVpsnWDAsG%2BTtici8KYsWYIyYtTAh%2FPGk04mUCw%3D


很多开发者会(或者应该)知道,当我们为运行在Linux容器(Docker、rkt、runC、lxcfs等)中的Java程序去设置JVM的GC、堆大小和运行时编译器的参数时并没有得到预想的效果。当我们通过“java -jar mypplication-fat.jar”的方式而不设置任何参数来运行一个Java应用时,JVM会根据自身的许多参数进行调整,以便在执行环境中获得最优的性能。

本篇博客将通过简单的方式向开发人员展示在将Java应用运行在Linux容器内时需要了解的内容。

我们倾向于认为容器可以像虚拟机一样可以完整的定义虚拟机的CPU个数和虚拟机的内存。容器更像是一个进程级别的资源(CPU、内存、文件系统、网络等)隔离。这种隔离是依赖于Linux内核中提供的一个cgroups的功能。

然而,一些可以从运行时环境中收集信息的应用程序在cgroups功能出现之前已经存在。在容器中执行命令 ‘top‘、‘free‘、‘ps’,也包括没有经过优化的JVM是一个会受到高限制的Linux进程。让我们来验证一下。

问题

为了展示遇到的问题,我使用命令“docker-machine create -d virtualbox –virtualbox-memory ‘1024’ docker1024”在虚拟机中创建了一个具有1GB内存的Docker守护进程,接下来在3个Linux容器中执行命令“free -h”,使其只有100MB的内存和Swap。结果显示所有的容器总内存是995MB。

即使是在 Kubernetes/OpenShift集群中,结果也是类似的。我在一个内存是15G的集群中也执行了命令使得Kubernetes Pod有511MB的内存限制(命令:“kubectl run mycentos –image=centos -it –limits=’memory=512Mi’”),总内存显示为14GB。

想要知道为什么是这样的结果,可以去阅读此篇博客文章 “Memory inside Linux containers – Or why don’t free and top work in a Linux container?”(https://fabiokung.com/2014/03/13/memory-inside-linux-containers/)

我们需要知道Docker参数(-m、–memory和–memory-swap)和Kubernetes参数(–limits)会让Linux内核在一个进程的内存超出限制时将其Kill掉,但是JVM根本不清楚这个限制的存在,当超过这个限制时,不好的事情发生了!

为了模拟当一个进程超出内存限制时会被杀死的场景,我们可以通过命令“docker run -it –name mywildfly -m=50m jboss/wildfly”在一个容器中运行WildFly Application Server并且为其限制内存大小为50MB。在这个容器运行期间,我们可以执行命令“docker stats”来查看容器的限制。

但是过了几秒之后,容器Wildfly将会被中断并且输出信息:*** JBossAS process (55) received KILL signal ***

通过命令 “docker inspect mywildfly -f ‘{{json .State}}‘”可以查看容器被杀死的原因是发生了OOM(内存不足)。容器中的“state”被记录为OOMKilled=true 。

这将怎样影响Java应用

在Docker宿主机中创建一个具有1GB内存的虚拟机(在之前使用命令已经创建完毕 “docker-machine create -d virtualbox –virtualbox-memory ‘1024’ docker1024”) ,并且限制一个容器的内存为150M,看起来已经足够运行这个在 Dockerfile中设置过参数-XX: PrintFlagsFinal 和 -XX: PrintGCDetails的Spring Boot application了。这些参数使得我们可以读取JVM的初始化参数并且获得 Garbage Collection(GC)的运行详细情况。

尝试一下:

$ docker run -it --rm --name mycontainer150 -p 8080:8080 -m 150M rafabene/java-container:openjdk

我也提供了一个访问接口“/api/memory/”来使用String对象加载JVM内存,模拟大量的消耗内存,可以调用试试:

$ curl http://`docker-machine ip docker1024`:8080/api/memory

这个接口将会返回下面的信息 “Allocated more than 80% (219.8 MiB) of the max allowed JVM memory size (241.7 MiB)”。

在这里我们至少有2个问题:

  • 为什么JVM会允许241.7MiB的最大内容?
  • 如果容器已经限制了内存为150MB,为什么允许Java分配内存到220MB?

首先,我们应该重新了解在JVM ergonomic page中所描述的 “maximum heap size”的定义,它将会使用1/4的物理内存。JVM并不知道它运行在一个容器中,所以它将被允许使用260MB的最大堆大小。通过添加容器初始化时的参数-XX: PrintFlagsFinal,我们可以检查这个参数的值。

$ docker logs mycontainer150|grep -i MaxHeapSize

uintx MaxHeapSize := 262144000 {product}

其次,我们应该理解当在docker命令行中设置了 “-m 150M”参数时,Docker守护进程会限制RAM为150M并且Swap为150M。从结果上看,一个进程可以分配300M的内存,解释了为什么我们的进程没有收到任何从Kernel中发出的退出信号。

更多的关于Docker命令中内存限制 (–memory)和Swap (–memory-swap)的差别可以参考这里。

更多的内存是解决方案吗?

开发者如果不理解问题可能会认为运行环境中没有为JVM提供足够的内存。通常的解决对策就是为运行环境提供更多的内存,但是实际上,这是一个错误的认识。

假如我们将Docker Machine的内存从1GB提高到8GB(使用命令 “docker-machine create -d virtualbox –virtualbox-memory ‘8192’ docker8192”),并且创建的容器从150M到800M:

$ docker run -it --name mycontainer -p 8080:8080 -m 800M rafabene/java-container:openjdk

此时使用命令 “curl http://X51X:8080/api/memory” 还不能返回结果,因为在一个拥有8GB内存的JVM环境中经过计算的MaxHeapSize大小是2092957696(~ 2GB)。可以使用命令“docker logs mycontainer|grep -i MaxHeapSize”查看。

应用将会尝试分配超过1.6GB的内存,当超过了容器的限制(800MB的RAM 800MB的Swap),进程将会被Kill掉。

很明显当在容器中运行程序时,通过增加内存和设置JVM的参数不是一个好的方式。当在一个容器中运行Java应用时,我们应该基于应用的需要和容器的限制来设置最大堆大小(参数:-Xmx)。

解决方案是什么?

在Dockerfile中稍作修改,为JVM指定扩展的环境变量。修改内容如下:

CMD java -XX:+PrintFlagsFinal -XX:+PrintGCDetails $JAVA_OPTIONS -jar java-container.jar

现在我们可以使用JAVA_OPTIONS的环境变量来设置JVM Heap的大小。300MB看起来对应用足够了。稍后你可以查看日志,看到Heap的值是 314572800 bytes(300MBi)。

Docker下,可以使用“-e”的参数来设置环境变量进行切换。

$ docker run -d --name mycontainer8g -p 8080:8080 -m 800M -e JAVA_OPTIONS=‘-Xmx300m‘ rafabene/java-container:openjdk-env

$ docker logs mycontainer8g|grep -i MaxHeapSize

uintx    MaxHeapSize := 314572800       {product}

在Kubernetes中,可以使用“–env=[key=value]”来设置环境变量进行切换:

$ kubectl run mycontainer --image=rafabene/java-container:openjdk-env --limits=‘memory=800Mi‘ --env="JAVA_OPTIONS=‘-Xmx300m‘"

$ kubectl get pods

NAME                          READY  STATUS    RESTARTS AGE

mycontainer-2141389741-b1u0o  1/1    Running   0        6s

$ kubectl logs mycontainer-2141389741-b1u0o|grep MaxHeapSize

uintx     MaxHeapSize := 314572800     {product}

还能再改进吗?

有什么办法可以根据容器的限制来自动计算Heap的值?

事实上如果你的基础Docker镜像使用的是由Fabric8提供的,那么就可以实现。镜像fabric8/java-jboss-openjdk8-jdk使用了脚本来计算容器的内存限制,并且使用50%的内存作为上限。也就是有50%的内存可以写入。你也可以使用这个镜像来开/关调试、诊断或者其他更多的事情。让我们看一下一个Spring Boot应用的 Dockerfile :

FROM fabric8/java-jboss-openjdk8-jdk:1.2.3

ENV JAVA_APP_JAR java-container.jar

ENV AB_OFF true

EXPOSE 8080

ADD target/$JAVA_APP_JAR /deployments/

就这样!现在,不管容器的内存限制如何,我们的Java应用将在容器中自动的调节Heap大小,而不是再根据宿主机来设置。

总结到目前为止,Java JVM还不能意识到其是运行在一个容器中 — 某些资源在内存和CPU的使用上会受到限制。因此,你不能让JVM自己来设置其认为的最优的最大Heap值。

一个解决对策是使用Fabric8作为基础镜像,它可以意识到应用程序运行在一个受限制的容器中,并且在你没有做任何事情的情况下,可以自动的调整最大Heap的值。

在JDK9中已经开始进行尝试在容器(i.e. Docker)环境中为JVM提供cgroup功能的内存限制。相关信息可以查看:http://hg.openjdk.java.net/jdk9/jdk9/hotspot/rev/5f1d1df0ea49

3 天烧脑式Kubernetes训练营

本次培训内容包括:Kubernetes概述和架构、部署和核心机制分析、进阶篇——Kubernetes工作原理和代码分析等,点击下面图片即可查看具体培训内容。

点击阅读原文链接可直接报名。

时间: 2024-10-06 00:43:02

(转)在Docker中运行Java:为了防止失败,你需要知道这些的相关文章

docker 运行jenkins及vue项目与springboot项目(五.jenkins打包springboot服务且在docker中运行)

docker 运行jenkins及vue项目与springboot项目: 一.安装docker 二.docker运行jenkins为自动打包运行做准备 三.jenkins的使用及自动打包vue项目 四.docker运行nginx 五.jenkins打包springboot服务且在docker中运行 1.立即构建 输入任务名称:service_blog 选择maven项目(先要在插件管理中安装maven插件) 2.配置 源码管理 选择git(先要在插件管理中安装git插件) 输入git地址.秘钥及

docker中运行ASP.NET Core Web API

在docker中运行ASP.NET Core Web API应用程序 本文是一篇指导快速演练的文章,将介绍在docker中运行一个ASP.NET Core Web API应用程序的基本步骤,在介绍的过程中,也会对docker的使用进行一些简单的描述.对于.NET Core以及docker的基本概念,网上已经有很多文章对其进行介绍了,因此本文不会再详细讲解这些内容.对.NET Core和docker不了解的朋友,建议首先查阅与这些技术相关的文档,然后再阅读本文. 先决条件 要完成本文所介绍的演练任

在docker中运行ASP.NET Core Web API应用程序

本文是一篇指导快速演练的文章,将介绍在docker中运行一个ASP.NET Core Web API应用程序的基本步骤,在介绍的过程中,也会对docker的使用进行一些简单的描述.对于.NET Core以及docker的基本概念,网上已经有很多文章对其进行介绍了,因此本文不会再详细讲解这些内容.对.NET Core和docker不了解的朋友,建议首先查阅与这些技术相关的文档,然后再阅读本文. 先决条件 要完成本文所介绍的演练任务,需要准备以下环境: Visual Studio 2015,或者Vi

NET Core Web 应用部署到 Docker 中运行

NET Core Web 应用部署到 Docker 中运行 环境介绍 : 虚拟机:VirtualBox 5.1.6 系 统:Ubuntu 16.04.1 LTS 系统准备完成后可以使用 sudo apt-get udpate 和 sudo apt-get upgrade 对系统进行更新,然后打开https://www.microsoft.com/net/core#ubuntu 这个网址,在ubuntu系统中安装 .net core 运行环境.直接复制/粘贴给出的命令就可以了. 安装 Docker

在Docker中运行纸壳CMS并配置使用MySql

纸壳CMS是基于ASP.Net Core开发的可视化内容管理系统,可以跨平台部署,可以在容器中运行.接下来看看如何在docker中运行纸壳CMS. GitHub:https://github.com/SeriaWei/ZKEACMS.Core 方式一 直接运行以下命令即可在docker中运行纸壳CMS,运行成功以后,使用{IP}:5000来访问: docker run -d -p 5000:80 zkeasoft/zkeacms 注意:使用这种方式运行,数据库使用的是SQLite. 方式二 接下

在 docker中 运行 mono /jexus server 并部署asp.net mvc站点

http://linuxdot.net/bbsfile-3988 1.  安装 docker:      // docker 1.7 新版 安装非常容易,理论上说,在主流的任意linux发行版上都可以直接安装.    wget -qO- https://get.docker.com/ | sh 2. 重启你的宿主机,可选.    reboot 3.拉取已经内建了mono 和jexus 服务器的 docker镜像, 此镜像基于 官方 ubuntu:14.04.2 版本构建,dockerfile 在

二、在Docker中运行web应用

启动一个简单的web 应用 使用社区提供的模板,启动一个简单的web应用,熟悉下各种Docker命令的使用: [plain] view plain copy # docker run -d -P training/webapp python app.py Unable to find image 'training/webapp' locally Pulling repository training/webapp 31fa814ba25a: Download complete 511136ea

在Docker中运行web应用

启动一个简单的web 应用 使用社区提供的模板,启动一个简单的web应用,熟悉下各种Docker命令的使用: [plain] view plain copy # docker run -d -P training/webapp python app.py Unable to find image 'training/webapp' locally Pulling repository training/webapp 31fa814ba25a: Download complete 511136ea

在docker中运行mariadb程序

安装docker 获取mariadb镜像 docker pull mariadb 首先在https://hub.docker.com查找mariadb官方镜像,相关参数设置https://hub.docker.com/_/mariadb/有详细说明 运行镜像 docker run -p 3306:3306 --name mariaone -v /usr/local/mysql/my.cnf:/etc/mysql/my.cnf -e MYSQL_ROOT_PASSWORD=mypassword -