内容简介:即使Docker是2016年的大事情,它今天仍然很热!它是最受欢迎的Orchestration平台Kubernetes的基础,它已成为云部署的首选解决方案。Docker是容器应用/微服务的事实标准解决方案。如果你运行Java应用程序,你需要知道一些陷阱和技巧。
即使 Docker 是2016年的大事情,它今天仍然很热!它是最受欢迎的Orchestration平台Kubernetes的基础,它已成为云部署的首选解决方案。
Docker是容器应用/微服务的事实标准解决方案。如果你运行 Java 应用程序,你需要知道一些陷阱和技巧。
为什么我要将JAVA放在容器中呢?
这是一个很好的问题。Java不是用“ 一次编写,随处运行 ”的口号构建的吗?尽管如此,该语句的意思Java应用仅包含Java二进制文件。
您的字节码(Jar文件)将可能在每个的JVM版本上正常运行。然而,数据库驱动程序呢?文件系统访问?联网?可用熵?您依赖的第三方应用程序?所有这些因素都会因操作系统而异。
通常,您的应用程序需要在Java二进制文件和任何第三方依赖项之间取得良好的平衡。如果您曾经支持过客户安装的Java应用程序,那么您就会知道我的意思。
首先是虚拟机
在容器之前,通用解决方案是使用虚拟机。使用您选择的操作系统创建一个新的空白虚拟计算机,安装所有第三方依赖项,复制Java二进制文件,获得快照并最终递交。
使用VM,您可以确定所发送的内容与其运行方式完全相同,并且每次都一致。环境配置问题没有空间。
它们还提供强大的封装。如果您在云中运行应用程序,则每个虚拟机都将被隔离。在同一硬件上运行的VM之间的损坏空间非常有限。
但是,总有一个但是,却很重!如果您在应用程序中发现了一个错误并且必须更改一行代码,则必须重新编译Jar文件,重新安装VM并运送整个代码。一行代码变成 几个GB文件,可以上传到云端或下载到客户端。操作系统文件很重,可能比Java二进制文件重得多,并且每次都必须发送它们,即使它们没有真正改变。
Docker来救援
如上所述,VM拥有自己的操作系统副本,而容器则更小,并且只包含您要发送的内容。
使用容器,操作系统(确切地说,它是正在共享的内核,您可以选择从不同的发行版(如Ubuntu,Debian,Alpine等)构建图像)由引擎(例如Docker)提供,并且您不需要将其和你的应用一起交付。
使用Docker,您可以交付以层为单位构建的图像。构建镜像的说明放在Dockerfile中。
从概念上讲,Dockerfile可能是这样的:
- 从空白的Ubuntu发行版开始
- 安装Java
- 安装依赖项A.
- 安装依赖关系B.
- 复制jar文件
Dockerfile中的每条指令都会创建一个不可变 层。这很聪明,也是一个很好的优化。只有当你修改改变最后一层代码时,则只需上传最后一层,之前未更改的图层将被缓存; 最终用户只需从镜像底部下载更改的图层。使用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)这样的业务流程解决方案将尝试在多个节点上有效地 “包装” 容器。这里的“包装”是指打包内存和CPU。如果为Docker容器提供内存和CPU的合理界限,K8将能够在多个节点上有效地安排它们。
不幸的是,这正是Java缺乏的地方。让我们用一个例子来理解这个问题。
想象一下,你有一个32GB内存的节点,你想使用Docker运行一个限制为1GB的Java应用程序。如果未提供-Xmx参数,则JVM将使用其默认配置:
- JVM将检查总可用内存。因为JVM不知道Linux容器(特别是限制内存的控制组),所以它认为它在主机上运行并且可以访问完整的 32GB可用内存。
- 默认情况下,JVM将使用MaxMemory / 4,在这种情况下为8GB(32GB / 4)。
- 随着堆大小的增长并超过1GB,容器将被Docker杀死。
早期的Docker Java采用者有一段时间试图理解为什么他们的JVM在没有任何错误消息的情况下崩溃。要了解发生了什么,你需要检查被杀死的Docker容器,在这种情况下,你会看到一条消息说“OOM被杀 ”(OutOf Memory)。
当然,一个明显的解决方案是使用Xmx参数修复JVM的堆大小,但这意味着您需要控制内存两次,一次在Docker中,一次在JVM中。每当你想要做出改变时,你必须做两次。不理想。
此问题的第一个解决方法是使用Java 8u131和Java 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 8u131和Java 9,你会有类似的配置:
-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XX:MaxRAMFraction=2 -Djava.util.concurrent.ForkJoinPool.common.parallelism=2
幸运的是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。
Java2Docker
将Java应用程序转换为Docker镜像的方法有很多种。
您可以使用Maven插件( fabric8 或 Spotify )或 Graddle 插件。但也许最简单和更语义的方法是自己编写Dockerfile。这种方法还允许您利用在JDK9引入的JLINK。使用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<font><i>/*.jar /opt/ CMD $JAVA_HOME/bin/java $JAVA_OPTS -jar /opt/*.jar </i></font>
让我们一行一行地解释它:
FROM adoptopenjdk/openjdk11 AS jdkBuilder
我们从包含完整JDK 11的现有Docker镜像开始。这里我们使用 AdoptOpenJDK 提供的构建,但您可以使用任何其他分发(例如新发布的 AWS Corretto )。AS jdkBuilderinstruction是一个特殊的指令,告诉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的Docker镜像开始,并安装了最小的依赖项(slim标签)。这个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的文件,这个指令缓存将为你节省大量的时间。
好消息是多阶段使这种方法过时了!您可以创建第一个“Throw-away”阶段,在这个阶段创建任意数量的图层。然后,您将创建一个新的“final”阶段,在该阶段中,您只从第一个Throw-away阶段复制所需的文件。
第一阶段的许多层将被完全忽略!
COPY target/*.jar /opt/
现在我们安装了Java,我们需要复制你的应用程序。上面这行将从目标目录复制任何jar文件并将它们放在opt文件夹中
CMD $JAVA_HOME/bin/java $JAVA_OPTS -jar /opt/*.jar
最后,这告诉Docker在容器运行时执行哪个命令。这里我们只运行java并允许在运行时传递JAVA_OPTS变量。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Programming in Haskell
Graham Hutton / Cambridge University Press / 2007-1-18 / GBP 34.99
Haskell is one of the leading languages for teaching functional programming, enabling students to write simpler and cleaner code, and to learn how to structure and reason about programs. This introduc......一起来看看 《Programming in Haskell》 这本书的介绍吧!
UNIX 时间戳转换
UNIX 时间戳转换
HSV CMYK 转换工具
HSV CMYK互换工具