docker jvm_Docker和JVM

docker jvm

尽管Docker是2016年的事情,但今天仍然有意义。 它是最流行的Orchestration平台Kubernetes基础 ,已成为云部署 的首选 解决方案

Docker是用于容器化应用程序/(微)服务的事实上的标准解决方案 。 如果您运行Java应用程序,则需要注意一些陷阱和技巧。 如果您不这样做,该帖子仍然可以为您提供帮助。

为什么我应该将Java放在容器中?

这是一个很好的问题。 Java是否不是用“ 编写一次,就可以在任何地方运行 ”的口号构建的? 即使正确,该语句也只包含Java二进制文件。

您的字节码(即Jar文件)将在JVM的每个可能版本上正常运行。 但是数据库驱动程序呢? 文件系统访问? 联网? 可用熵? 您依赖的第三方应用程序? 所有这些事情在不同的操作系统上都不同。

通常,您的应用程序将需要Java二进制文件与任何第三方依赖项之间良好平衡 。 如果您的客户端曾经支持过Java应用程序,那么您将理解我的意思。

虚拟机首先出现

在使用容器之前,通用解决方案是使用虚拟机 。 使用您选择的操作系统创建一个新的空白虚拟计算机,安装所有第三方依赖项,复制Java二进制文件,拍摄快照并最终发货。

使用虚拟机,您可以确定所交付的产品可以按照预期的方式运行,并且每次都可以始终如一地运行。 没有空间解决环境配置问题。

它们还提供强大的封装 。 如果您在云中运行应用程序,则每个虚拟机都将被高度隔离。 在相同硬件上运行的VM之间损坏的空间非常有限。

但是,总有一个,但是,它们很重 。 如果您在应用程序中发现错误,并且不得不更改一行代码,则必须重新编译您的Jar文件,重新安装VM并交付整个产品。 一行代码变成了 几个GB文件 ,可以上传到云或在您的客户端下载。 操作系统文件很重 ,可能比您的Java二进制文件重得多,即使它们没有真正改变,您也必须每次都发送它们。

救援容器

如上所述,VM具有自己的OS副本,而容器被制成较小的容器,并且包含您要运送的容器:

Docker JVM

有了一个容器,操作系统(确切地说就是共享的内核,您可以选择从不同发行版(例如Ubuntu,Debian,Alpine等…)构建映像)由引擎(例如Docker)提供,而您不需要不必随应用程序一起提供。

使用Docker,您可以运送在layers中构建的图像 。 构建映像的说明放入Dockerfile中

从概念上讲,Dockerfile可能类似于:

  1. 从空白的Ubuntu发行版开始
  2. 安装Java
  3. 安装依赖项A
  4. 安装依赖项B
  5. 复制jar文件

(下面提供了实际的Dockerfile示例)

Dockerfile中的每条指令都会创建一个不可变的 。 这是一个聪明而伟大的优化。 如果保留将复制Java二进制文件结尾的指令(并且应该这样做),则只需在进行代码更改时更改最后一层。 这意味着,如果您要为最终用户交付更改,则只需上传最后一层 ,即缓存先前未更改的层; 最终用户只需从图片底部下载更改的图层即可。 对于Docker,更改一行代码意味着仅上传/下载几个MB (而如果这是VM,则更改将以GB而不是MB为单位)。

警告

请注意,容器不能提供与VM相同级别的封装。 Docker容器只是在主机上运行的进程。 有Linux内核功能(即名称空间控制组 )可以帮助降低Docker容器的访问级别,但是它远没有VM隔离那么灵活 。 这可能对您的业务来说不是问题,但您需要注意。

Java + Docker =❤️???

在研究如何在Docker容器中打包Jar文件之前,我们需要涵盖一些重要的限制。 Java 1.0于1996年发布,而Linux容器于2008年左右诞生。 由此可见,JVM不可能容纳Linux容器。

Docker的主要功能之一是能够限制容器的内存和CPU使用率 。 这是在云中运行许多Docker容器在经济上引起人们关注的主要原因之一。 诸如Kubernetes(k8s)之类的编排解决方案将尝试在多个节点上 有效地 “打包” 容器 。 这里的“ pack”是指内存和CPU( 请参阅 k8s如何玩俄罗斯方块 )。 如果您为Docker容器提供合理的内存和CPU边界,则K8将能够有效地将它们布置在多个节点上

不幸的是,这正是Java运行不足的地方。 让我们用一个例子来理解这个问题。

假设您有一个具有32GB内存的节点,并且您想运行一个限制为1GB的Java应用程序。 请记住,Docker容器只不过 主机 上的荣耀进程 。 如果不提供-Xmx参数,则JVM将使用其默认配置:

    1. JVM将检查总可用内存。 因为JVM不知道Linux容器(特别是限制内存的控制组),所以它认为它在主机上运行并且可以访问全部 32GB的可用内存。
  1. 默认情况下,JVM将使用MaxMemory / 4,在这种情况下为8GB (32GB / 4)。
  2. 随着堆大小的增加并超过1GB,该容器将被Docker杀死

Docker上的Java早期采用者度过了一段愉快的时光,试图了解为什么他们的JVM一直崩溃而没有任何错误消息。 要了解发生了什么,您需要检查失效的Docker容器,在这种情况下,您会看到一条消息,提示“ OOM杀死 ”(内存不足)。

当然,一个显而易见的解决方案是使用Xmx参数修复JVM的堆大小,但这意味着您需要两次控制内存,一次在Docker中,一次在JVM中。 每次您要进行更改时,都必须进行两次。 不理想。

Java 8u131Java 9发行了此问题的第一个解决方法。 我说解决方法是因为您必须使用心爱的-XX:+ UnlockExperimentalVMOptions参数。 如果您在金融服务业工作,我相信您会喜欢向客户或老板解释这是明智的选择。

然后,您必须使用-XX:+ UseCGroupMemoryLimitForHeap ,这将告诉JVM检查控制组内存限制以设置最大堆大小。

最后,您必须使用-XX:MaxRAMFraction来确定可为JVM分配的最大内存部分。 不幸的是,该参数是自然数。 例如,将Docker内存限制设置为1GB,您将拥有以下内容:

  • -XX:MaxRAMFraction = 1最大堆大小将为1GB。 这不是很好,因为您无法为JVM提供100%的允许内存。 该容器上可能还有其他组件在运行
  • -XX:MaxRAMFraction = 2最大堆大小将为500MB。 更好,但是现在看来我们正在浪费大量内存。
  • -XX:MaxRAMFraction = 3最大堆大小将为250MB。 您只需支付1GB的内存,您的Java应用程序就可以使用250MB。 这有点荒谬
  • -XX:MaxRAMFraction = 4甚至不要去那里。

基本上,用于控制“最大可用RAM”的JVM标志设置为分数而不是百分比,这使得难以设置有效利用可用(允许)RAM的值。

我们专注于内存,但同样适用于CPU。 您需要使用类似的参数

-Djava.util.concurrent.ForkJoinPool.common.parallelism = 2

控制应用程序中不同线程池的大小。 2表示两个线程(最大值将限制为主机上可用的超线程数)。

综上所述,使用Java 8u131Java 9时,您将获得以下内容:

-XX:+UnlockExperimentalVMOptions
-XX:+UseCGroupMemoryLimitForHeap
-XX:MaxRAMFraction=2
-Djava.util.concurrent.ForkJoinPool.common.parallelism=2

Docker JVM

幸运的是, Java 10得以解救。 首先,您不必使用吓人的实验性功能标志。 如果在Linux容器中运行Java应用程序,则JVM将自动检测控制组内存限制。 否则,您只需要添加-XX:-UseContainerSupport即可

然后,您可以使用-XX:InitialRAMPercentage-XX:MaxRAMPercentage-XX:MinRAMPercentage来控制内存。 例如与

  • Docker内存限制:1GB
  • -XX:InitialRAMPercentage = 50
  • -XX:MaxRAMPercentage = 70

您的JVM将以500MB(50%)的堆大小开始,并将增长到700MB(70%),在容器中,最大可用内存为1GB。

Docker JVM

Java2的Docker

有多种方法可以将Java应用程序转换为Docker映像。

您可以使用Maven插件( fabric8Spotify )或Graddle插件。 但是,也许最简单,更具语义的方法是自己编写一个Dockerfile。 这种方法还允许您利用JDK的JLINK在JDK9介绍。 使用jlink可以构建仅包含应用程序所需模块的自定义JDK二进制文件。

让我们看一个例子:

FROM adoptopenjdk/openjdk11 AS jdkBuilder
RUN $JAVA_HOME/bin/jlink \
--module-path /opt/jdk/jmods \
--verbose \
--**add**-modules java.base \
--output /opt/jdk-minimal \
--compress 2 \
--no-header-files

FROM debian:9-slim
COPY --from=jdkBuilder /opt/jdk-minimal /opt/jdk-minimal
ENV JAVA_HOME=/opt/jdk-minimal
COPY target/*.jar /opt/
CMD $JAVA_HOME/bin/java $JAVA_OPTS -jar /opt/*.jar

让我们逐行浏览

FROM adoptopenjdk/openjdk11 AS jdkBuilder

我们从包含完整JDK 11的现有Docker映像开始。 在这里,我们使用了AdoptOpenJDK提供的构建,但是您可以使用任何其他发行版(例如,新发布的AWS Corretto )。 AS jdkBuilder指令是一条特殊指令,告诉Docker我们要启动一个称为jdkBuilder的“阶段”。 以后将很有用。

RUN $JAVA_HOME/bin/jlink \
--module-path /opt/jdk/jmods \
--verbose \
--add-modules java.base \
--output /opt/jdk-minimal \
--compress 2 \
--no-header-files

我们运行jlink来构建我们的自定义JDK二进制文件。 在此示例中,我们仅使用java.base模块,这可能不足以运行您的应用程序。 如果仍在编写旧的类路径类型的应用程序,则必须手动添加应用程序所需的所有模块。 例如,对于我的一个Spring应用程序,我使用以下模块:

--add-modules java.base,java.logging,java.xml,
java.xml.bind,java.sql,jdk.unsupported,
java.naming,java.desktop,java.management,
java.security.jgss,java.security.sasl,
jdk.crypto.cryptoki,jdk.crypto.ec,
java.instrument,jdk.management.agent,
jdk.localedata

如果您正在使用模块编写Java应用程序,则可以让jlink 推断需要哪些模块 。 为此,您需要将模块添加到module-path参数中(在MacOS / Linux中,路径列表以“:”分隔,在Windows中以“;”分隔)。 但是,因为此过程发生在Docker映像中,所以您需要使用COPY命令将其移过来。 然后,您只需要在-add-modules命令中添加自己的模块,所需的模块将自动添加。 因此,它将类似于:

FROM adoptopenjdk/openjdk11 AS jdkBuilder
COPY path/to/module-info.class /opt/myModules
RUN $JAVA_HOME/bin/jlink \
--module-path /opt/jdk/jmods:/opt/myModules \
--verbose \
--**add**-modules my-module \
--output /opt/jdk-minimal \
--compress 2 \
--no-header-files
FROM debian:9-slim

因为我们使用了另一个FROM关键字,所以Docker会舍弃目前为止所做的一切并开始创建新的映像 。 在这里,我们从安装了Debian 9并已安装最低限度依赖项 (slim标签)的Docker映像开始。 这个Debian映像甚至没有Java,因此我们接下来将安装它。

COPY --from=jdkBuilder /opt/jdk-minimal /opt/jdk-minimal
ENV JAVA_HOME=/opt/jdk-minimal

这是阶段名称变得重要的地方。 我们可以告诉Docker 从更早的阶段复制特定的文件夹 ,在这种情况下,是从jdkBuilder阶段复制 。 这很有趣,因为在第一阶段,我们可以下载很多最终不需要的中间库。

在这种情况下,我们从完整的JDK 11发行版开始,该发行版的重量为200 + MB,但是我们只需要复制我们的自定义JDK二进制文件(通常为〜50 / 60MB); 取决于您必须导入的JDK模块。 然后,我们将JAVA_HOME环境变量设置为指向我们新建的JDK二进制文件。

这项技术称为Docker多阶段构建 ,确实非常有用。 它有效地利用了创建的图层,并有助于制作更薄的docker映像 。 如果查看了典型的Dockerfile,您可能会看到类似以下的说明:

rm -rf /var/lib/apt/lists/* \
apt-get clean && apt-get update && apt-get upgrade -y \
apt-get install -y --no-install-recommends curl ca-certificates \
rm -rf /var/lib/apt/lists/* \
...

这种在单个Dockerfile指令中将尽可能多的命令分组的技术对于最小化映像中的层数很有用 。 大量的层会影响运行时的性能。 但是,这种方法也有一个缺点。 每条指令只有一层意味着创建大量缓存的检查点。

如果您在指令15中在Dockerfile中犯了一个错误,则Docker不必重新运行先前的14,只需从缓存中恢复它们即可。 如果您的步骤之一是下载具有此指令高速缓存的400MB文件,则可以节省大量时间。

好消息是多阶段使这种方法过时了 ! 您可以创建第一个“扔掉”阶段,在其中可以创建任意多个图层 。 然后,创建一个新的“最终”阶段,在该阶段中,您仅从第一个废弃阶段复制所需的文件。

从第一阶段开始的许多层次将被完全忽略!

COPY target/*.jar /opt/

现在我们已经安装了Java,我们需要复制您的应用程序。 这将复制目标目录中的所有jar文件并将其放在opt文件夹中

CMD $JAVA_HOME/bin/java $JAVA_OPTS -jar /opt/*.jar

最后,这告诉Docker在容器运行时执行哪个命令。 在这里,我们仅运行Java并允许在运行时传递JAVA_OPTS变量。

请注意,最后保留复制您的应用程序的指令非常重要。 这样,如果您需要进行代码更改, 则只会使最后一层无效,而无需重做前面的步骤。

结论

就是这样,伙计们,我们经历了在Docker上运行Java的陷阱,以及如何编写一个通用的Dockerfile,该文件只能满足您的应用程序的需求!

如果您想了解如何在云中运行Docker容器,请参阅我的演讲Kubernetes的Cloud Ready JVM

谢谢Mani Sarkar( @ theNeomatrix369 )帮助我写了这篇文章!

资源资源

翻译自: https://www.javacodegeeks.com/2018/12/docker-jvm.html

docker jvm

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值