V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
Tenxcloud10
V2EX  ›  推广

在 Docker 里跑 Java ,趟坑总结

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

    背景:众所周知,当我们执行没有任何调优参数(如“ 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
    写得很好,感谢分享!
    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   ·   实用小工具   ·   3115 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 22ms · UTC 00:12 · PVG 08:12 · LAX 16:12 · JFK 19:12
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.