转载自:https://mp.weixin.qq.com/s/d2PFISYUy6X6ZAOGu0-Kig

1. 概述

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

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

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

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

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

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

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

2.1 自动内存计算

当不设置-Xmx-Xms参数时,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

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

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

这里使用的容器使用旧版本的 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 没有遵守容器内存限制。

2.3. 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 来获得正确的默认值。

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

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

构建镜像:

$ 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 设置了内存分配池的最大大小,其中包括堆、垃圾收集器的幸存者空间和其他池。

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"

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

$ 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

4. 使用构建插件

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

下面看几个例子。

4.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
[...]

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插件来生成容器镜像:

<?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

5. 结论

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

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

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

Java 服务 Docker 容器化最佳实践的更多相关文章

  1. docker容器化python服务部署(supervisor-gunicorn-flask)

    docker容器化python服务部署(supervisor-gunicorn-flask) 本文系作者原创,转载请注明出处: https://www.cnblogs.com/further-furt ...

  2. 通过 Azure Pipelines 实现持续集成之docker容器化及自动化部署

    通过 Azure Pipelines 实现持续集成之docker容器化及自动化部署 Intro Azure DevOps Pipeline 现在对于公开的项目完全免费,这对于开源项目来讲无疑是个巨大的 ...

  3. 利用 ELK 搭建 Docker 容器化应用日志中心

    利用 ELK 搭建 Docker 容器化应用日志中心 概述 应用一旦容器化以后,需要考虑的就是如何采集位于 Docker 容器中的应用程序的打印日志供运维分析.典型的比如SpringBoot应用的日志 ...

  4. [ci]jenkins-slave-ssh docker容器化-用户名密码

    jenkins-slave-ssh docker容器化 架构 参考:https://www.youtube.com/watch?v=OxrBCt1JLuQ https://github.com/Dav ...

  5. Docker容器化技术(下)

    Docker容器化技术(下) 一.Dockerfile基础命令 1.1.FROM - 基于基准镜像 FROM centos #制作基准镜像(基于centos) FROM scratch #不依赖任何基 ...

  6. Docker容器化技术(上)

    目录 Docker容器化技术 一.介绍 二.Docker的发展 三.Docker安装 四.阿里云Docker镜像加速 五.Docker的基本概念 六.命令 七.Docker宿主机与容器通信 八.容器内 ...

  7. Docker 使用杂记 - 最佳实践尝试 - 实战

    目录 Docker 使用杂记 - 最佳实践尝试 - 实战 Docker简介 项目背景 内在原因 外在原因 基础镜像 需求 镜像维护者 工作文件夹 文件 ADD COPY 宗卷 命令 入口点 Docke ...

  8. 《转载》Java异常处理的10个最佳实践

    本文转载自 ImportNew - 挖坑的张师傅 异常处理在编写健壮的 Java 应用中扮演着非常重要的角色.异常处理并不是功能性需求,它需要优雅地处理任何错误情况,比如资源不可用.非法的输入.nul ...

  9. Java异常处理的10个最佳实践

    本文作者: ImportNew - 挖坑的张师傅 未经许可,禁止转载! 异常处理在编写健壮的 Java 应用中扮演着非常重要的角色.异常处理并不是功能性需求,它需要优雅地处理任何错误情况,比如资源不可 ...

随机推荐

  1. 《Ranked List Loss for Deep Metric Learning》CVPR 2019

    Motivation: 深度度量学习的目标是学习一个嵌入空间来从数据点中捕捉语义信息.现有的成对或者三元组方法随着模型迭代过程会出现大量的平凡组导致收敛缓慢.针对这个问题,一些基于排序结构的损失取得了 ...

  2. Git 中的回退操作:reset 和 revert

    Git 中回退有 reset 和 revert,这两个的区别就是是否保留更改记录 假设当前的提交情况是:A <- B <- C <- D <- HEAD,如下图: 当前是 D, ...

  3. Pytorch Tensor 维度的扩充和压缩

    维度扩展 x.unsqueeze(n) 在 n 号位置添加一个维度 例子: import torch x = torch.rand(3,2) x1 = x.unsqueeze(0) # 在第一维的位置 ...

  4. Template -「整体二分」

    写的简单.主要是留给自己做复习资料. 「BZOJ1901」Dynamic Rankings. 给定一个含有 \(n\) 个数的序列 \(a_1,a_2 \dots a_n\),需要支持两种操作: Q ...

  5. 3.Android高仿网易云音乐-首页复杂发现界面布局和功能/RecyclerView复杂布局

    0.效果图 效果图依次为发现界面顶部,包含首页轮播图,水平滚动的按钮,推荐歌单:然后是发现界面推荐单曲,点击单曲就是直接进入播放界面:最后是全局播放控制条上点击播放列表按钮显示的播放列表弹窗. 1.整 ...

  6. NoSQL,关系型数据库,行列数据库对比、类比

    数据库对比.类比 关系型数据库.NoSQL 关系型数据库 表与表之间有关系.表存储一些格式化的数据结构,每个元组字段的组成都一样,这样便于表之间的联结操作.不过也限制了其性能瓶颈. 更支持SQL,支持 ...

  7. 2506-nginx的配置-域名分发与负载均衡(只有配置无原理)

    nginx的安装 Windows7:官网下载,是一个压缩包,运行解压缩后的exe文件即启动了nginx,需注意的是,Windows(win7)的80端口默认被微软的IIS占用,需改成别的端口,例如80 ...

  8. cache2go-源码阅读

    简介 cache2go 是非常简短的 go 开源项目了,很适合作为第一个读源码项目. 如果你有一定的 go 开发经验,读起来会感觉到比较容易. 如果你刚刚接触 go 语音,基础知识还不完全了解,希望阅 ...

  9. 技术分享 | innodb_buffer_pool_size为什么无法调低至1GB以内

    前言 innodb_buffer_pool_size可以调大,却不能调小至1GB以内,这是为什么? MySQL 版本:5.7.30 测试环境有台 MySQL 服务器反应很慢,检查系统后发现内存使用量已 ...

  10. 活动回顾丨ALC Beijing 首场 Meetup:《开源到底有多难?》

    8月16日,ALC Beijing 的首次线下沙龙活动 -- <开源到底有多难?>在微软大厦如期举行.本次沙龙主要是分享开源开发经验.探讨如何让开源项目更加茁壮成长,以及分享 ASF 管理 ...