• Java 服务 Docker 容器化最佳实践


    一、概述

    当我们在容器中运行 Java 应用程序时,可能希望对其进行调整以充分利用资源。

    在本教程中,我们将了解如何在运行 Java 进程的容器中设置 JVM 参数。本文将重点关注常见的 -Xmx 和-Xms 标志

    另外,我们还将研究使用某些 Java 版本运行的程序容器化的常见问题,以及如何在常见的容器化 Java 应用程序时设置自定义标志。

    2. Java 容器中的默认堆设置

    过去,JVM 不知道分配给容器的内存和 CPU

    Java 10 引入了一个新设置:+UseContainerSupport(默认启用)来修复 这个问题,并在 8u191 中将修复反向移植到 Java 8 。

    现在 JVM 可以根据分配给容器的内存计算其内存。

    2.1 自动内存计算

    当不设置-Xmx-Xmx参数时,JVM 会根据系统规范来调整堆大小。

    看看堆大小:

    $ java -XX:+PrintFlagsFinal -version | grep -Ei "maxheapsize|maxram"
    
    • 1

    输出结果如下:

    openjdk version "15" 2020-09-15
    OpenJDK Runtime Environment AdoptOpenJDK (build 15+36)
    OpenJDK 64-Bit Server VM AdoptOpenJDK (build 15+36, mixed mode, sharing)
       size_t MaxHeapSize      = 4253024256      {product} {ergonomic}
     uint64_t MaxRAM           = 137438953472 {pd product} {default}
        uintx MaxRAMFraction   = 4               {product} {default}
       double MaxRAMPercentage = 25.000000       {product} {default}
       size_t SoftMaxHeapSize  = 4253024256   {manageable} {ergonomic}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在这里,我们看到 JVM 将其堆大小设置为可用 RAM 的大约 25%。在这个例子中,它在一个 16GB 的系统上分配了 4GB。

    为了测试目的,创建一个文件,名为PrintXmxXms.java,内容是 以 MB 为单位打印堆大小,代码内容如下:

    import java.lang.management.ManagementFactory;
    import java.lang.management.MemoryMXBean;
    
    public class PrintXmxXms {
        public static void main(String[] args) {
            int mb = 1024 * 1024;
            MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
            long xmx = memoryBean.getHeapMemoryUsage().getMax() / mb;
            long xms = memoryBean.getHeapMemoryUsage().getInit() / mb;
            System.out.println("Initial Memory (xms) : " + xms + "mb");
            System.out.println("Max Memory (xmx) : " + xmx + "mb");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    假设已经安装了 JDK,可以编译程序并运行:

    $ javac ./PrintXmxXms.java
    $ java -cp . PrintXmxXms
    
    • 1
    • 2

    在 16Gb RAM 的主机上,输出结果为:

    INFO: Initial Memory (xms) : 254mb
    INFO: Max Memory (xmx) : 4056mb
    
    • 1
    • 2

    下面,在容器中尝试一下。

    2.2. JDK 8u191 之前的版本

    在包含 PrintXmxXms.java 文件的文件夹中添加以下 Dockerfile

    FROM openjdk:8u92-jdk-alpine
    COPY *.java /src/
    RUN mkdir /app \
        && ls /src \
        && javac /src/PrintXmxXms.java -d /app
    CMD ["sh", "-c", \
         "java -version \
          && java -cp /app PrintXmxXms"]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    这里使用的容器使用旧版本的 Java 8,它早于更新版本中可用的容器支持。构建镜像:

    $ sudo docker build -t oldjava .
    
    • 1

    Dockerfile 中的 CMD 行是运行容器时默认执行的进程。由于没有提供-Xmx-XmsJVM 标志,内存设置将是默认设置。

    运行容器:

    $ sudo docker run --rm -ti oldjava
    openjdk version "1.8.0_92-internal"
    OpenJDK Runtime Environment (build 1.8.0_92-...)
    OpenJDK 64-Bit Server VM (build 25.92-b14, mixed mode)
    Initial Memory (xms) : 198mb
    Max Memory (xmx) : 2814mb
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    现在使用--memory=1g命令行标志将容器内存限制为 1GB:

    $ sudo docker run --rm -ti --memory=1g oldjava
    openjdk version "1.8.0_92-internal"
    OpenJDK Runtime Environment (build 1.8.0_92-...)
    OpenJDK 64-Bit Server VM (build 25.92-b14, mixed mode)
    Initial Memory (xms) : 198mb
    Max Memory (xmx) : 2814mb
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    输出完全相同。这证明旧的 JVM 没有遵守容器内存限制。

    2.3. JDK 8u130 之后的版本

    使用相同的测试程序,更改 Dockerfile 的第一行来使用 JVM 8 的新版本:

    FROM openjdk:8-jdk-alpine
    
    • 1

    然后再次做测试:

    $ sudo docker build -t newjava .
    $ sudo docker run --rm -ti newjava
    openjdk version "1.8.0_212"
    OpenJDK Runtime Environment (IcedTea 3.12.0) (Alpine 8.212.04-r0)
    OpenJDK 64-Bit Server VM (build 25.212-b04, mixed mode)
    Initial Memory (xms) : 198mb
    Max Memory (xmx) : 2814mb
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    如上输出,使用整个 docker 主机内存来计算 JVM 堆大小。但是,如果为容器分配 1GB 的 RAM:

    $ sudo docker run --rm -ti --memory=1g newjava
    openjdk version "1.8.0_212"
    OpenJDK Runtime Environment (IcedTea 3.12.0) (Alpine 8.212.04-r0)
    OpenJDK 64-Bit Server VM (build 25.212-b04, mixed mode)
    Initial Memory (xms) : 16mb
    Max Memory (xmx) : 247mb
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这一次,JVM 根据容器可用的 1GB RAM 计算堆大小。

    现在我们了解了 JVM 如何计算其默认值以及为什么需要一个最新的 JVM 来获得正确的默认值。

    3. 常用的基础镜像中内存设置

    3.1 OpenJDK

    与其直接在容器命令上硬编码 JVM 标志,不如使用环境变量。例如在Dockerfile 中使用 JAVA_OPTS 变量,可以在启动容器时对其进行修改:

    FROM openjdk:8u92-jdk-alpine
    COPY *.java /src/
    RUN mkdir /app \
        && ls /src \
        && javac /src/PrintXmxXms.java -d /app
    ENV JAVA_OPTS=""
    CMD ["sh", "-c", \
         "java -version \
          && java $JAVA_OPTS -cp /app PrintXmxXms"]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    构建镜像:

    $ sudo docker build -t openjdk-java .
    
    • 1

    通过指定JAVA_OPTS环境变量在运行时选择内存设置:

    $ sudo docker run --rm -ti -e JAVA_OPTS="-Xms50M -Xmx50M" openjdk-java
    openjdk version "1.8.0_92-internal"
    OpenJDK Runtime Environment (build 1.8.0_92-internal-alpine-r1-b14)
    OpenJDK 64-Bit Server VM (build 25.92-b14, mixed mode)
    Initial Memory (xms) : 50mb
    Max Memory (xmx) : 48mb
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    注意:-Xmx 参数和 JVM 报告的 Max memory 之间存在细微差别。这是因为 Xmx 设置了内存分配池的最大大小,其中包括堆、垃圾收集器的幸存者空间和其他池。

    3.2 Tomcat 9

    Tomcat 9 容器有自己的启动脚本,因此要设置 JVM 参数,需要使用这些脚本。

    bin/catalina.sh 脚本要求在环境变量 CATALINA_OPTS 中设置内存参数。

    首先需要 创建一个 war 包 部署到 Tomcat。

    然后,我们使用下面的Dockerfile 对其进行容器化,并在其中声明CATALINA_OPTS环境变量:

    FROM tomcat:9.0
    COPY ./target/*.war /usr/local/tomcat/webapps/ROOT.war
    ENV CATALINA_OPTS="-Xms1G -Xmx1G"
    
    • 1
    • 2
    • 3

    然后构建容器镜像并运行它:

    $ sudo docker build -t tomcat .
    $ sudo docker run --name tomcat -d -p 8080:8080 \
      -e CATALINA_OPTS="-Xms512M -Xmx512M" tomcat
    
    • 1
    • 2
    • 3

    注意:运行时,将新值传递给 CATALINA_OPTS。如果不提供这个值,会使用 Dockerfile 的第 3 行给出的默认值。

    可以检查应用的运行时参数并验证选项-Xmx-Xms是否存在:

    $ sudo docker exec -ti tomcat jps -lv
    1 org.apache.catalina.startup.Bootstrap <other options...> -Xms512M -Xmx512M
    
    • 1
    • 2

    4. 使用构建插件

    Maven 和 Gradle 提供的插件允许我们在没有Dockerfile的情况下创建容器镜像。生成的镜像通常可以在运行时通过环境变量进行参数化。

    下面看几个例子。

    4.1 使用 Spring Boot

    从 Spring Boot 2.3 开始,Spring Boot MavenGradle 插件可以在没有 Dockerfile 的情况下高效构建容器。

    使用 Maven 时,将它们添加到 spring-boot-maven-plugin 中的 块中:

    
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
      <groupId>com.baeldung.dockergroupId>
      <artifactId>heapsizing-demoartifactId>
      <version>0.0.1-SNAPSHOTversion>
      
      <build>
        <plugins>
          <plugin>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-maven-pluginartifactId>
            <configuration>
              <image>
                <name>heapsizing-demoname>
              image>
       
            configuration>
          plugin>
        plugins>
      build>
    project>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    要构建项目,运行下面的命令:

    $ ./mvnw clean spring-boot:build-image
    
    • 1

    这将产生一个名为 : 的镜像。在这个例子中产生的镜像名为: demo-app:0.0.1-SNAPSHOT。Spring Boot 底层使用 Cloud Native Buildpacks 作为容器化技术。

    该插件对 JVM 的内存设置进行硬编码。但是,我们仍然可以通过设置环境变量JAVA_OPTSJAVA_TOOL_OPTIONS 来覆盖:

    $ sudo docker run --rm -ti -p 8080:8080 \
      -e JAVA_TOOL_OPTIONS="-Xms20M -Xmx20M" \
      --memory=1024M heapsizing-demo:0.0.1-SNAPSHOT
    
    • 1
    • 2
    • 3

    输出将与此类似:

    Setting Active Processor Count to 8
    Calculated JVM Memory Configuration: [...]
    [...]
    Picked up JAVA_TOOL_OPTIONS: -Xms20M -Xmx20M
    [...]
    
    • 1
    • 2
    • 3
    • 4
    • 5

    4.2 使用谷歌 JIB

    就像 Spring Boot maven 插件一样,Google JIB 无需 Dockerfile 即可高效创建 Docker 镜像。Maven 和 Gradle 插件以类似的方式配置。Google JIB 还使用环境变量 JAVA_TOOL_OPTIONS 作为 JVM 参数的覆盖机制。

    我们可以在任何能够生成可执行 jar 文件的 Java 框架中使用 Google JIB Maven 插件。例如,可以在 Spring Boot 应用程序中使用它来代替spring-boot-maven插件来生成容器镜像:

    
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    
        
    
        <build>
            <plugins>
                
                <plugin>
                    <groupId>com.google.cloud.toolsgroupId>
                    <artifactId>jib-maven-pluginartifactId>
                    <version>2.7.1version>
                    <configuration>
                        <to>
                            <image>heapsizing-demo-jibimage>
                        to>
                    configuration>
                plugin>
            plugins>
        build>
    project>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    镜像是使用 mvn jib:dockerBuild 命令构建的:

    $ mvn clean install && mvn jib:dockerBuild
    
    • 1

    尝试运行:

    $ sudo docker run --rm -ti -p 8080:8080 \
    -e JAVA_TOOL_OPTIONS="-Xms50M -Xmx50M" heapsizing-demo-jib
    Picked up JAVA_TOOL_OPTIONS: -Xms50M -Xmx50M
    [...]
    2021-01-25 17:46:44.070  INFO 1 --- [           main] c.baeldung.docker.XmxXmsDemoApplication  : Started XmxXmsDemoApplication in 1.666 seconds (JVM running for 2.104)
    2021-01-25 17:46:44.075  INFO 1 --- [           main] c.baeldung.docker.XmxXmsDemoApplication  : Initial Memory (xms) : 50mb
    2021-01-25 17:46:44.075  INFO 1 --- [           main] c.baeldung.docker.XmxXmsDemoApplication  : Max Memory (xmx) : 50mb
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    5. 结论

    在本文中,我们介绍了需要使用最新的 JVM 来获取在容器中默认内存设置。

    然后,研究了在自定义容器映像中设置 -Xms-Xmx 的最佳实践, 以及如何使用现有 Java 应用程序容器在其中设置 JVM 选项。

    最后,我们看到了如何利用构建工具来管理 Java 应用程序的容器化。

    上面用到的 示例源代码 可在 GitHub 上获得。

    交流

    请关注微信公众号【进击云原生】,扫码关注,了解更多咨询,更有免费资源供您学习

  • 相关阅读:
    音频信号分析与实践
    【摘要】Cpp核心指南
    5-网络架构和Netty系列-Bootstrap和Channel源码分析
    计算机网络课程实训:局域网方案设计与实现(基于ensp)
    深度学习自学笔记十三:unet网络详解和环境配置
    如何使用yum 安装php7.2
    CSAPP 之 CacheLab 详解
    MyBatis概述
    【顶顶通呼叫中心中间件(mod_cti 基于 FreeSWITCH)-拨号方案和路由配置】
    数据结构之链表
  • 原文地址:https://blog.csdn.net/ll837448792/article/details/126078785