在 Docker 里跑 Java ,趟坑总结 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
Tenxcloud10
V2EX    推广

在 Docker 里跑 Java ,趟坑总结

  •  6
     
  •   Tenxcloud10 2017-04-07 18:09:31 +08:00 7409 次点击
    这是一个创建于 3108 天前的主题,其中的信息可能已经有所发展或是发生改变。

    背景:众所周知,当我们执行没有任何调优参数(如“ 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 的总内存。

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

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

    如果想知道为什么会发生这种情况,建议您阅读博客“ Memoryinside 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 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 )执行的详细信息。

    动手试一下:

    $ 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

    此端点将回复“分配超过 80 %( 219.8 MiB )的最大允许 JVM 内存大小( 241.7 MiB )” 在这里我们可以提至少两个问题: 为什么 JVM 最大允许内存 241.7 MiB ? 如果这个容器将内存限制为 150MB ,那为什么它允许 Java 分配近 220MB ? 首先,我们需要回顾一下 JVM 人机工程学页面上关于“最大堆大小”的内容:是物理内存的 1/4 。由于 JVM 不知道它在一个容器内执行,所以允许最大堆大小将接近 260MB 。鉴于我们在容器初始化期间添加了-XX :+ PrintFlagsFinal 标志,我们可以检查这个值:

    $ docker logs mycontainer150|grep -i MaxHeapSize uintx MaxHeapSize := 262144000 {product}

    其次,我们需要了解,当我们在 docker 命令行中使用参数“-m 150M ”时, docker daemon 将在 RAM 中限制 150M ,在 Swap 中限制为 150M 。因此,该过程可以分配 300M 。这就解释了为什么我们的进程没有被杀死。 docker 命令行中的内存限制(-memory )和 swap (-memory-swap )之间的更多组合可以在这里( https://docs.docker.com/engine/reference/run/#example-run-htop-inside-a-container)找到。

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

    不了解问题的开发者往往认为环境不能为执行 JVM 提供足够的内存。所以通常的解决办法是提供更多内存,这实际上会使事情变得更糟。 我们假设将 daemon 从 1GB 更改为 8GB (使用“ docker-machinecreate -d virtualbox virtualbox-memory ‘ 8192 ’ docker8192 ”创建),并将容器内存从 150M 更改为 800M :

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

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

    该应用将尝试分配超过 1.6GB 的内存,这超出了此容器的限制( RAM 中的 800MB + Swap 中的 800MB ),并且该进程将被杀掉。 很显然,用增加内存且让 JVM 自定义参数的方式在容器里跑 Java ,不是什么好主意。 在容器内部运行 Java 应用程序时,我们应该根据应用程序需求和容器限制设置最大堆大小(-Xmx 参数)。

    4.解决方案

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

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

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

    $ 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 中,您可以使用 switch “-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}

    再进一步

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

    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/

    下面一起看看 Dockerfile 是如何作用于这个 Spring Boot 应用程序: 搞定!现在,无论容器内存限制是多少,我们的 Java 应用程序将始终根据容器调整堆大小,而不是根据 daemon 调整堆大小。

    5.结论

    直到现在, Java JVM 依然没有提供什么支持,让大家可以理解它在容器内是如何运行的,而且它有一些资源是内存和 CPU 限制的。 因此,您不能让 JVM 人体工程学本身决定最大堆大小。 解决此问题的一种方法是使用能够理解它在受限容器内运行的 Fabric8 Base 镜像。 在 JVM 中有一个实验支持,已经包含在 JDK9 中以支持容器(即 Docker )环境中的 cgroup 内存限制。可以参考: http://hg.openjdk.java.net/jdk9/jdk9/hotspot/rev/5f1d1df0ea49 原文评论:更好的方法是以 exec 表单定义您的 CMD 指令,这将确保 java 是 PID 1 进程 - 这对于允许 Java 在容器停止时正常关闭至关重要。 Exec 表单不支持环境变量替换,但您可以通过设置 JAVA_TOOL_OPTIONS 环境变量来传递其他命令行标志(请参阅 http://bit.ly/2mTIDUt

    11 条回复    2017-04-10 11:21:27 +08:00
    nikymaco
        1
    nikymaco  
       2017-04-07 18:15:45 +08:00
    写得很好,感谢分享!/div>
    Tenxcloud10
        2
    Tenxcloud10  
    OP
       2017-04-07 18:16:39 +08:00
    @nikymaco #1 :)
    momocraft
        3
    momocraft  
       2017-04-07 18:24:21 +08:00
    相关讨论: https://github.com/docker/docker/issues/15020
    这个 issue 我订了十年了,好气呀
    Tenxcloud10
        4
    Tenxcloud10  
    OP
       2017-04-07 18:26:25 +08:00
    @momocraft #3 哈哈,坚持不易,且行且珍惜
    sagaxu
        5
    sagaxu  
       2017-04-07 18:59:21 +08:00
    JVM 在 docker 里跑的时候 xmx 参数不管用?
    gamexg
        6
    gamexg  
       2017-04-07 19:12:55 +08:00
    翻译?
    之前看过一篇英文的。
    ryd994
        7
    ryd994  
       2017-04-07 21:46:35 +08:00 via Android
    机翻太硬,很多地方还要翻回英文才能理解
    mritd
        8
    mritd  
       2017-04-07 22:07:27 +08:00 via iPhone
    给力
    silverfox
        9
    silverfox  
       2017-04-08 10:53:31 +08:00
    简单的说,现在 JVM 在 Docker 中运行时, MaxHeapSize 是根据 Host 的内存,而不是 Container Memory Limit 来自动设置。

    当前版本 (OpenJDK 8u121),最简单的办法是
    $ java -XX:MaxRAM=`cat /sys/fs/cgroup/memory/memory.limit_in_bytes` -jar some.jar

    晚些时候发布的 OpenJDK 8u131 ,以及未来的 OpenJDK 9 ,可以使用
    $ java -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -jar some.jar
    Tenxcloud10
        10
    Tenxcloud10  
    OP
       2017-04-10 09:42:39 +08:00
    @gamexg #6 好眼力
    androidlive
        11
    androidlive  
       2017-04-10 11:21:27 +08:00
    学习了
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     2920 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 25ms UTC 12:53 PVG 20:53 LAX 05:53 JFK 08:53
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86