百度360必应搜狗淘宝本站头条
当前位置:网站首页 > IT技术 > 正文

Java 服务 Docker 容器化最佳实践

wptr33 2024-12-19 16:51 26 浏览

一、概述

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

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

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

二、Java 容器中的默认堆设置

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

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

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

1. 自动内存计算

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

看看堆大小:

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

输出结果如下:

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}

我们看到 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");
    }
}

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

$ javac ./PrintXmxXms.java
$ java -cp . PrintXmxXms

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

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

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

  1. 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"]

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

$ sudo docker build -t oldjava .

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

现在使用--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

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

  1. JDK 8u130 之后的版本
    使用相同的测试程序,更改 Dockerfile 的第一行来使用 JVM 8 的新版本:
FROM openjdk:8-jdk-alpine

然后再次做测试:

$ 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

如上输出,使用整个 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

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

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

三、常用的基础镜像中内存设置

  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"]

构建镜像:

$ sudo docker build -t openjdk-java .

通过指定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

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

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"

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

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

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

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

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

四、使用构建插件

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

下面看几个例子。

  1. 使用 Spring Boot
    从 Spring Boot 2.3 开始,Spring Boot Maven 和 Gradle 插件可以在没有 Dockerfile 的情况下高效构建容器。

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

<?xml version="1.0" encoding="UTF-8"?>
<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.docker</groupId>
  <artifactId>heapsizing-demo</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <!-- dependencies... -->
  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <configuration>
          <image>
            <name>heapsizing-demo</name>
          </image>
   <!--
    for more options, check:
    https://docs.spring.io/spring-boot/docs/2.4.2/maven-plugin/reference/htmlsingle/#build-image
   -->
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

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

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

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

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

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

输出将与此类似:

Setting Active Processor Count to 8
Calculated JVM Memory Configuration: [...]
[...]
Picked up JAVA_TOOL_OPTIONS: -Xms20M -Xmx20M
[...]
  1. 使用谷歌 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插件来生成容器镜像:

<?xml version="1.0" encoding="UTF-8"?>
<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">

    <!-- dependencies, ... -->

    <build>
        <plugins>
            <!-- [ other plugins ] -->
            <plugin>
                <groupId>com.google.cloud.tools</groupId>
                <artifactId>jib-maven-plugin</artifactId>
                <version>2.7.1</version>
                <configuration>
                    <to>
                        <image>heapsizing-demo-jib</image>
                    </to>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

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

$ mvn clean install && mvn jib:dockerBuild

尝试运行:

$ 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

五、结论

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

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

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

相关推荐

MySQL进阶五之自动读写分离mysql-proxy

自动读写分离目前,大量现网用户的业务场景中存在读多写少、业务负载无法预测等情况,在有大量读请求的应用场景下,单个实例可能无法承受读取压力,甚至会对业务产生影响。为了实现读取能力的弹性扩展,分担数据库压...

Postgres vs MySQL_vs2022连接mysql数据库

...

3分钟短文 | Laravel SQL筛选两个日期之间的记录,怎么写?

引言今天说一个细分的需求,在模型中,或者使用laravel提供的EloquentORM功能,构造查询语句时,返回位于两个指定的日期之间的条目。应该怎么写?本文通过几个例子,为大家梳理一下。学习时...

一文由浅入深带你完全掌握MySQL的锁机制原理与应用

本文将跟大家聊聊InnoDB的锁。本文比较长,包括一条SQL是如何加锁的,一些加锁规则、如何分析和解决死锁问题等内容,建议耐心读完,肯定对大家有帮助的。为什么需要加锁呢?...

验证Mysql中联合索引的最左匹配原则

后端面试中一定是必问mysql的,在以往的面试中好几个面试官都反馈我Mysql基础不行,今天来着重复习一下自己的弱点知识。在Mysql调优中索引优化又是非常重要的方法,不管公司的大小只要后端项目中用到...

MySQL索引解析(联合索引/最左前缀/覆盖索引/索引下推)

目录1.索引基础...

你会看 MySQL 的执行计划(EXPLAIN)吗?

SQL执行太慢怎么办?我们通常会使用EXPLAIN命令来查看SQL的执行计划,然后根据执行计划找出问题所在并进行优化。用法简介...

MySQL 从入门到精通(四)之索引结构

索引概述索引(index),是帮助MySQL高效获取数据的数据结构(有序),在数据之外,数据库系统还维护者满足特定查询算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构...

mysql总结——面试中最常问到的知识点

mysql作为开源数据库中的榜一大哥,一直是面试官们考察的重中之重。今天,我们来总结一下mysql的知识点,供大家复习参照,看完这些知识点,再加上一些边角细节,基本上能够应付大多mysql相关面试了(...

mysql总结——面试中最常问到的知识点(2)

首先我们回顾一下上篇内容,主要复习了索引,事务,锁,以及SQL优化的工具。本篇文章接着写后面的内容。性能优化索引优化,SQL中索引的相关优化主要有以下几个方面:最好是全匹配。如果是联合索引的话,遵循最...

MySQL基础全知全解!超详细无废话!轻松上手~

本期内容提醒:全篇2300+字,篇幅较长,可搭配饭菜一同“食”用,全篇无废话(除了这句),干货满满,可收藏供后期反复观看。注:MySQL中语法不区分大小写,本篇中...

深入剖析 MySQL 中的锁机制原理_mysql 锁详解

在互联网软件开发领域,MySQL作为一款广泛应用的关系型数据库管理系统,其锁机制在保障数据一致性和实现并发控制方面扮演着举足轻重的角色。对于互联网软件开发人员而言,深入理解MySQL的锁机制原理...

Java 与 MySQL 性能优化:MySQL分区表设计与性能优化全解析

引言在数据库管理领域,随着数据量的不断增长,如何高效地管理和操作数据成为了一个关键问题。MySQL分区表作为一种有效的数据管理技术,能够将大型表划分为多个更小、更易管理的分区,从而提升数据库的性能和可...

MySQL基础篇:DQL数据查询操作_mysql 查

一、基础查询DQL基础查询语法SELECT字段列表FROM表名列表WHERE条件列表GROUPBY分组字段列表HAVING分组后条件列表ORDERBY排序字段列表LIMIT...

MySql:索引的基本使用_mysql索引的使用和原理

一、索引基础概念1.什么是索引?索引是数据库表的特殊数据结构(通常是B+树),用于...