竹笋

首页 » 问答 » 环境 » SpringBootDocker认证
TUhjnbcbe - 2022/10/12 19:33:00

许多人使用容器来包装他们的SpringBoot应用程序,而构建容器并不是一件简单的事情。这是针对SpringBoot应用程序开发人员的指南,容器对于开发人员来说并不总是一个好的抽象。它们迫使你去了解和思考低层次的问题。但是,有时可能会要求您创建或使用容器,因此了解构建块是值得的。在本指南中,我们旨在向您展示如果您面临需要创建自己的容器的前景,您可以做出的一些选择。

我们假设您知道如何创建和构建基本的SpringBoot应用程序。如果没有,请转到入门指南之一——例如,关于构建REST服务的指南。从那里复制代码并练习本指南中包含的一些想法。

还有一个关于Docker的入门指南,这也是一个很好的起点,但它没有涵盖我们在此处介绍的选择范围或详细介绍它们。

一个基本的Dockerfile

SpringBoot应用程序很容易转换为可执行的JAR文件。所有的入门指南都是这样做的,你从SpringInitializr下载的每个应用程序都有一个构建步骤来创建一个可执行的JAR。使用Maven,你运行./mvnwinstall,使用Gradle,你运行./gradlewbuild。运行该JAR的基本Dockerfile将如下所示,位于项目的顶层:

Dockerfile

FROMopenjdk:8-jdk-alpineVOLUME/tmpARGJAR_FILECOPY${JAR_FILE}app.jarENTRYPOINT["java","-jar","/app.jar"]复制

JAR_FILE您可以作为命令的一部分传入docker(Maven和Gradle不同)。对于Maven,以下命令有效:

dockerbuild--build-argJAR_FILE=target/*.jar-tmyorg/myapp.复制

对于Gradle,以下命令有效:

dockerbuild--build-argJAR_FILE=build/libs/*.jar-tmyorg/myapp.复制

一旦你选择了一个构建系统,你就不需要ARG.您可以对JAR位置进行硬编码。对于Maven,如下所示:

Dockerfile

FROMopenjdk:8-jdk-alpineVOLUME/tmpCOPYtarget/*.jarapp.jarENTRYPOINT["java","-jar","/app.jar"]复制

然后我们可以使用以下命令构建镜像:

dockerbuild-tmyorg/myapp.复制

然后我们可以通过运行以下命令来运行它:

dockerrun-p:myorg/myapp复制

输出类似于以下示例输出:

._________/\\/________(_)______\\\\(()\___

_

_

_\/_`

\\\\\\/___)

_)

(_

))))

____

.__

_

_

_

_\__,

////=========

_

==============

___/=/_/_/_/::SpringBoot::(v2.0.2.RELEASE)Nov06,:45:16PMorg.springframework.boot.StartupInfoLoggerlogStartingINFO:StartingApplicationv0.1.0onbcdc9b87withPID1(/app.jarstartedbyrootin/)Nov06,:45:16PMorg.springframework.boot.SpringApplicationlogStartupProfileInfo...复制

如果你想在镜像内部四处寻找,你可以通过运行以下命令在其中打开一个shell(注意基础镜像没有bash):

dockerrun-ti--entrypoint/bin/shmyorg/myapp复制

输出类似于以下示例输出:

/#lsapp.jardevhomemediaprocrunsrvtmpvarbinetclibmntrootsbinsysusr/#

我们在示例中使用的alpine基础容器没有bash,所以这是一个ashshell。它具有一些但不是全部的特性bash。

如果你有一个正在运行的容器并且你想查看它,你可以通过运行dockerexec:

dockerrun--namemyapp-ti--entrypoint/bin/shmyorg/myappdockerexec-timyapp/bin/sh/#复制

传递给命令myapp的位置在哪里。如果您没有使用,docker会分配一个助记名称,您可以从.您还可以使用容器的SHA标识符而不是名称。SHA标识符在输出中也可见。--namedockerrun--namedockerpsdockerps

入口点

使用Dockerfile的exec形式ENTRYPOINT,以便没有外壳包装Java进程。优点是java进程响应KILL发送到容器的信号。实际上,这意味着(例如)如果您dockerrun在本地使用图像,则可以使用CTRL-C.如果命令行有点长,您可以COPY在运行之前将其提取到shell脚本中并放入映像中。以下示例显示了如何执行此操作:

Dockerfile

FROMopenjdk:8-jdk-alpineVOLUME/tmpCOPYrun.sh.COPYtarget/*.jarapp.jarENTRYPOINT["run.sh"]复制

请记住使用execjava…启动java进程(以便它可以处理KILL信号):

run.sh

#!/bin/shexecjava-jar/app.jar复制

入口点的另一个有趣方面是您是否可以在运行时将环境变量注入Java进程。例如,假设您想要在运行时添加Java命令行选项。您可以尝试这样做:

Dockerfile

FROMopenjdk:8-jdk-alpineVOLUME/tmpARGJAR_FILE=target/*.jarCOPY${JAR_FILE}app.jarENTRYPOINT["java","${JAVA_OPTS}","-jar","/app.jar"]复制

然后您可以尝试以下命令:

dockerbuild-tmyorg/myapp.dockerrun-p:-eJAVA_OPTS=-Dserver.port=myorg/myapp复制

这失败了,因为${}替换需要一个外壳。exec表单不使用shell来启动进程,因此不应用选项。您可以通过将入口点移动到脚本(如run.sh前面显示的示例)或在入口点显式创建shell来解决此问题。以下示例显示了如何在入口点中创建shell:

Dockerfile

FROMopenjdk:8-jdk-alpineVOLUME/tmpARGJAR_FILE=target/*.jarCOPY${JAR_FILE}app.jarENTRYPOINT["sh","-c","java${JAVA_OPTS}-jar/app.jar"]复制

然后,您可以通过运行以下命令来启动此应用程序:

dockerrun-p:-e"JAVA_OPTS=-Ddebug-Xmxm"myorg/myapp复制

该命令产生类似于以下的输出:

._________/\\/________(_)______\\\\(()\___

_

_

_\/_`

\\\\\\/___)

_)

(_

))))

____

.__

_

_

_

_\__,

////=========

_

==============

___/=/_/_/_/::SpringBoot::(v2.2.0.RELEASE)...-10-:12:12.DEBUG1---[main]ConditionEvaluationReportLoggingListener:============================CONDITIONSEVALUATIONREPORT============================...复制

(前面的输出显示了SpringBootDEBUG生成的完整输出的一部分。)-Ddebug

将anENTRYPOINT与显式shell一起使用(如前面的示例所做的那样)意味着您可以将环境变量传递给Java命令。但是,到目前为止,您还不能为SpringBoot应用程序提供命令行参数。以下命令不会在端口上运行应用程序:

dockerrun-p:myorg/myapp--server.port=复制

该命令产生以下输出,将端口显示为而不是:

._________/\\/________(_)______\\\\(()\___

_

_

_\/_`

\\\\\\/___)

_)

(_

))))

____

.__

_

_

_

_\__,

////=========

_

==============

___/=/_/_/_/::SpringBoot::(v2.2.0.RELEASE)...-10-:20:19.INFO1---[main]o.s.b.web.embedded.netty.NettyWebServer:Nettystartedonport(s):复制

它不起作用,因为docker命令(该--server.port=部分)被传递到入口点(sh),而不是它启动的Java进程。要解决此问题,您需要将命令行从以下添加CMD到ENTRYPOINT:

Dockerfile

FROMopenjdk:8-jdk-alpineVOLUME/tmpARGJAR_FILE=target/*.jarCOPY${JAR_FILE}app.jarENTRYPOINT["sh","-c","java${JAVA_OPTS}-jar/app.jar${0}${

}"]复制

然后您可以运行相同的命令并将端口设置为:

$dockerrun-p:myorg/myapp--server.port=复制

如以下输出示例所示,端口确实设置为:

._________/\\/________(_)______\\\\(()\___

_

_

_\/_`

\\\\\\/___)

_)

(_

))))

____

.__

_

_

_

_\__,

////=========

_

==============

___/=/_/_/_/::SpringBoot::(v2.2.0.RELEASE)...-10-:30:19.INFO1---[main]o.s.b.web.embedded.netty.NettyWebServer:Nettystartedonport(s):复制

注意${0}“命令”(在这种情况下是第一个程序参数)和${

}“命令参数”(程序参数的其余部分)的使用。如果您使用脚本作为入口点,那么您不需要${0}(/app/run.sh在前面的示例中)。以下列表显示了脚本文件中的正确命令:

run.sh

#!/bin/shexecjava${JAVA_OPTS}-jar/app.jar${

}复制

docker配置到现在都非常简单,生成的镜像效率不是很高。docker镜像有一个文件系统层,其中包含fatJAR,我们对应用程序代码所做的每一次更改都会更改该层,这可能是10MB或更多(对于某些应用程序甚至高达50MB)。我们可以通过将JAR拆分为多个层来改进这一点。

较小的图像

请注意,前面示例中的基本映像是openjdk:8-jdk-alpine.这些alpine图像小于Dockerhubopenjdk的标准库图像。您还可以通过使用标签而不是.并非所有应用程序都使用JRE(与JDK相对),但大多数应用程序都可以。一些组织强制执行一个规则,即每个应用程序都必须使用JRE,因为存在滥用某些JDK功能(例如编译)的风险。jrejdk

另一个可以让您获得更小的映像的技巧是使用JLink,它与OpenJDK11捆绑在一起。JLink允许您从完整JDK中的模块子集构建自定义JRE分发,因此您不需要JRE或JDK基础图像。原则上,这将使您获得比使用openjdk官方docker图像更小的总图像大小。在实践中,您(还)不能将alpine基础镜像与JDK11一起使用,因此您对基础镜像的选择是有限的,并且可能会导致最终镜像的大小更大。此外,您自己的基本映像中的自定义JRE不能在其他应用程序之间共享,因为它们需要不同的自定义。因此,您的所有应用程序可能都有较小的图像,但它们仍然需要更长的时间才能启动,因为它们没有从缓存JRE层中受益。

最后一点突出了图像构建者的一个非常重要的问题:目标不一定总是尽可能地构建最小的图像。较小的图像通常是一个好主意,因为它们需要更少的时间来上传和下载,但前提是它们中的所有图层都没有被缓存。如今,图像注册非常复杂,您很容易通过尝试巧妙地构建图像而失去这些功能的好处。如果您使用通用基础层,图像的总大小就不再那么重要了,而且随着注册中心和平台的发展,它可能变得更不重要。话虽如此,尝试优化应用程序映像中的层仍然很重要且有用。然而,

更好的Dockerfile

由于JAR本身的打包方式,SpringBootfatJAR自然有“层”。如果我们先解包,它已经分为外部依赖和内部依赖。要在docker构建中一步完成此操作,我们需要先解压缩JAR。以下命令(坚持使用Maven,但Gradle版本非常相似)解压缩SpringBootfatJAR:

mkdirtarget/dependency(cdtarget/dependency;jar-xf../*.jar)dockerbuild-tmyorg/myapp.复制

然后我们可以使用下面的Dockerfile

Dockerfile

FROMopenjdk:8-jdk-alpineVOLUME/tmpARGDEPENDENCY=target/dependencyCOPY${DEPENDENCY}/BOOT-INF/lib/app/libCOPY${DEPENDENCY}/META-INF/app/META-INFCOPY${DEPENDENCY}/BOOT-INF/classes/appENTRYPOINT["java","-cp","app:app/lib/*","hello.Application"]复制

现在有三层,所有应用程序资源都在后面两层。如果应用程序依赖没有改变,第一层(fromBOOT-INF/lib)不需要改变,所以构建更快,并且容器在运行时的启动也更快,只要基础层已经被缓存。

我们使用了一个硬编码的主应用程序类:hello.Application.这对于您的应用程序可能有所不同。如果你愿意,你可以用另一个参数化它ARG。您还可以将SpringBootfat复制JarLauncher到映像中并使用它来运行应用程序。它可以工作,您不需要指定主类,但启动时会慢一些。

SpringBoot层索引

从SpringBoot2.3.0开始,使用SpringBootMaven或Gradle插件构建的JAR文件在JAR文件中包含层信息。该层信息根据应用程序构建之间更改的可能性来分离应用程序的各个部分。这可以用来使Docker镜像层更加高效。

层信息可用于将JAR内容提取到每个层的目录中:

mkdirtarget/extractedjava-Djarmode=layertools-jartarget/*.jarextract--destinationtarget/extracteddockerbuild-tmyorg/myapp.复制

然后我们可以使用以下内容Dockerfile:

Dockerfile

FROMopenjdk:8-jdk-alpineVOLUME/tmpARGEXTRACTED=/workspace/app/target/extractedCOPY${EXTRACTED}/dependencies/./COPY${EXTRACTED}/spring-boot-loader/./COPY${EXTRACTED}/snapshot-dependencies/./COPY${EXTRACTED}/application/./ENTRYPOINT["java","org.springframework.boot.loader.JarLauncher"]

SpringBootfatJarLauncher是从JAR中提取到镜像中的,因此它可以用于启动应用程序,而无需对主应用程序类进行硬编码。

有关使用分层功能的更多信息,请参阅SpringBoot文档。

调整

如果您想尽快启动您的应用程序(大多数人都这样做),您可能会考虑一些调整:

使用spring-context-indexer(链接到文档)。它不会为小型应用程序增加太多,但每一点都有帮助。

如果您负担得起,请不要使用执行器。

使用SpringBoot2.1(或更高版本)和Spring5.1(或更高版本)。

使用(通过命令行参数、系统属性或其他方法)修复SpringBoot配置文件的位置。spring.config.location

通过设置来关闭JMX(您可能不需要在容器中使用它)spring.jmx.enabled=false。

使用-noverify.还要考虑-XX:TieredStopAtLevel=1(这会在以后减慢JIT但会缩短启动时间)。

使用Java8的容器内存提示:-XX:+UnlockExperimentalVMOptions-XX:+UseCGroupMemoryLimitForHeap.在Java11中,默认情况下这是自动的。

您的应用程序在运行时可能不需要完整的CPU,但它确实需要多个CPU才能尽快启动(至少两个,四个更好)。如果您不介意启动速度较慢,则可以将CPU限制在四个以下。如果您被迫从少于四个CPU开始,设置可能会有所帮助-Dspring.backgroundpreinitializer.ignore=true,因为它可以防止SpringBoot创建一个它可能无法使用的新线程(这适用于SpringBoot2.1.0及更高版本)。

多阶段构建

ABetterDockerfile中Dockerfile所示的假设假设胖JAR已经在命令行上构建。您还可以通过使用多阶段构建并将结果从一个图像复制到另一个图像来在docker中执行该步骤。以下示例通过使用Maven来实现:

Dockerfile

FROMopenjdk:8-jdk-alpineasbuildWORKDIR/workspace/appCOPYmvnw.COPY.mvn.mvnCOPYpom.xml.COPYsrcsrcRUN./mvnwinstall-DskipTestsRUNmkdir-ptarget/dependency(cdtarget/dependency;jar-xf../*.jar)FROMopenjdk:8-jdk-alpineVOLUME/tmpARGDEPENDENCY=/workspace/app/target/dependencyCOPY--from=build${DEPENDENCY}/BOOT-INF/lib/app/libCOPY--from=build${DEPENDENCY}/META-INF/app/META-INFCOPY--from=build${DEPENDENCY}/BOOT-INF/classes/appENTRYPOINT["java","-cp","app:app/lib/*","hello.Application"]复制

第一个图像标记为build,它用于运行Maven、构建胖JAR并解压缩它。解包也可以由Maven或Gradle完成(这是入门指南中采用的方法)。没有太大区别,只是必须编辑构建配置并添加插件。

请注意,源代码已分为四层。后面的层包含构建配置和应用程序的源代码,前面的层包含构建系统本身(Maven包装器)。这是一个小的优化,也意味着我们不必将target目录复制到docker镜像,即使是用于构建的临时镜像。

RUN每个源代码更改的构建都很慢,因为必须在第一部分重新创建Maven缓存。但是你有一个完全独立的构建,只要他们有docker,任何人都可以运行它来运行你的应用程序。这在某些环境中可能非常有用——例如,您需要与不了解Java的人共享您的代码。

实验功能

Docker18.06带有一些“实验性”特性,包括缓存构建依赖项的方法。要打开它们,您需要在守护进程(dockerd)中有一个标志,并在运行客户端时需要一个环境变量。然后你可以添加一个“神奇”的第一行到你的Dockerfile:

Dockerfile

#syntax=docker/dockerfile:experimental复制

然后该RUN指令接受一个新标志:--mount.以下清单显示了一个完整示例:

Dockerfile

#syntax=docker/dockerfile:experimentalFROMopenjdk:8-jdk-alpineasbuildWORKDIR/workspace/appCOPYmvnw.COPY.mvn.mvnCOPYpom.xml.COPYsrcsrcRUN--mount=type=cache,target=/root/.m2./mvnwinstall-DskipTestsRUNmkdir-ptarget/dependency(cdtarget/dependency;jar-xf../*.jar)FROMopenjdk:8-jdk-alpineVOLUME/tmpARGDEPENDENCY=/workspace/app/target/dependencyCOPY--from=build${DEPENDENCY}/BOOT-INF/lib/app/libCOPY--from=build${DEPENDENCY}/META-INF/app/META-INFCOPY--from=build${DEPENDENCY}/BOOT-INF/classes/appENTRYPOINT["java","-cp","app:app/lib/*","hello.Application"]复制

然后你可以运行它:

DOCKER_BUILDKIT=1dockerbuild-tmyorg/myapp.复制

以下清单显示了示例输出:

...=/bin/sh-c./mvnwinstall-DskipTests5.7s=exportingtoimage0.0s==exportinglayers0.0s==writingimagesha:3defa...==namingtodocker.io/myorg/myapp复制

使用实验性功能,您会在控制台上获得不同的输出,但您可以看到,如果缓存是热的,现在Maven构建只需几秒钟而不是几分钟。

这个Dockerfile配置的Gradle版本非常相似:

Dockerfile

#syntax=docker/dockerfile:experimentalFROMopenjdk:8-jdk-alpineASbuildWORKDIR/workspace/appCOPY./workspace/appRUN--mount=type=cache,target=/root/.gradle./gradlewcleanbuildRUNmkdir-pbuild/dependency(cdbuild/dependency;jar-xf../libs/*.jar)FROMopenjdk:8-jdk-alpineVOLUME/tmpARGDEPENDENCY=/workspace/app/build/dependencyCOPY--from=build${DEPENDENCY}/BOOT-INF/lib/app/libCOPY--from=build${DEPENDENCY}/META-INF/app/META-INFCOPY--from=build${DEPENDENCY}/BOOT-INF/classes/appENTRYPOINT["java","-cp","app:app/lib/*","hello.Application"]

虽然这些功能处于实验阶段,但打开和关闭buildkit的选项取决于docker您使用的版本。检查您拥有的版本的文档(前面显示的示例对于docker18.0.6是正确的)。

安全方面

就像在经典VM部署中一样,进程不应以root权限运行。相反,映像应包含运行应用程序的非root用户。

在aDockerfile中,您可以通过添加另一个添加(系统)用户和组并将其设置为当前用户(而不是默认的root)的层来实现此目的:

Dockerfile

FROMopenjdk:8-jdk-alpineRUNaddgroup-Sdemoadduser-Sdemo-GdemoUSERdemo...复制

如果有人设法突破您的应用程序并在容器内运行系统命令,这种预防措施会限制他们的能力(遵循最小权限原则)。

一些进一步的Dockerfile命令只能以root身份运行,因此您可能必须将USER命令进一步向下移动(例如,如果您计划在容器中安装更多包,它只能以root身份运行)。

对于其他方法,不使用aDockerfile可能更适合。例如,在后面描述的buildpack方法中,大多数实现默认使用非root用户。

另一个考虑因素是大多数应用程序在运行时可能不需要完整的JDK,因此一旦我们进行了多阶段构建,我们就可以安全地切换到JRE基础映像。因此,在前面显示的多阶段构建中,我们可以将其用于最终的可运行映像:

Dockerfile

FROMopenjdk:8-jre-alpine...复制

如前所述,这也节省了映像中的一些空间,这些空间将被运行时不需要的工具占用。

#java##程序员##spring认证##java源码#

文末备注:

Spring中国教育管理中心

SpringBootDocker来源:Spring中国教育管理中心

1
查看完整版本: SpringBootDocker认证