很多人都使用容器来包装他们的 Spring Boot 应用程序,然而创建容器并不是一件很容易的事情。这篇文章将指导你构建运行 Spring Boot 应用程序的Docker镜像。
对于我们开发者来说,容器不是一个好理解的概念 – 它会强迫我们了解并考虑非常低级的问题 – 但是有些时候我们需要创建或使用容器,因此我们理解下构建块的内容是有必要的。
在这篇文章中,我会向你展示创建容器的一些知识,你可以针对你的应用程序作出适当地选择。
Docker 是一个具有“社交”方面的 Linux 容器管理工具,它允许用户发布容器镜像,以及使用其他人发布的镜像。Docker 镜像是一个运行容器化进程的“食谱”,接下来,我将构建一个简单的 Spring Boot 应用程序的镜像。
在此之前,你需要安装一下 Docker。(Docker)如果你使用的系统是 Windows,你需要安装 Docker Desktop。(Docker Desktop)
创建 Gradle 工程
首先,我们使用 IDEA 创建一个 Gradle 工程。然后,在 build.gradle
文件中加入以下内容:
1 | buildscript { |
在 Gradle 构建文件中,我们使用了 Spring Boot Gradle 插件,它可以提供很多便利的特性:
- 它可以收集环境变量中的所有的 jar,然后将它们构建成一个简单的,可运行的“über-jar”。(über 是一个德文单词,可理解为总)这使得执行和传输服务变得更加方便。
- 它会搜索
public static void main()
方法,并将它标记为一个可运行的类。 - 它提供了一个内置的依赖解决工具,可以设置 Spring Boot 依赖的版本号。你可以用你想要的版本进行覆盖,但是它默认为 Spring Boot 选择的版本。
创建 Spring Boot 应用
我们在工程中创建一个 Application
类,作为 Spring Boot 的启动类,同时也作为一个 Controller 类。
1 | @SpringBootApplication |
现在我们可以运行一下 Spring Boot 程序,可以直接在 IDEA 中运行,也可以使用以下命令运行 ./gradlew build && java -jar build/libs/spring-boot-docker-0.1.0.jar
。(Windows:gradlew build && java -jar build/libs/spring-boot-docker-0.1.0.jar
)
运行成功后,我们访问 localhost:8080,可以看到页面会显示“Hello Docker World”。
制作 Docker 镜像
我们在上面的 Gradle 构建文件中有一个任务 bootJar
,此任务会在 build/libs
下生成可运行的 jar 包 spring-boot-docker-0.1.1.jar
。接下来我们使用此 jar 文件制作一个 Docker 镜像。
首先,我们编写 Dockerfile。
1 | FROM openjdk:8-jdk-alpine |
这个 Dockerfile 非常简单,但是你需要运行一个没有多余装饰的 Spring Boot 项目:仅仅使用 Java 和 一个 JAR 文件。项目的 JAR 文件作为 “app.jar” 被添加进容器,然后在 ENTRYPOINT
中执行。
然后我们运行 docker 命令制作 docker 镜像: docker build --build-arg JAR_FILE=build/libs/*.jar -t myorg/myapp .
如果运行失败,并且日志显示如下:
1 | ...... This error may also indicate that the docker daemon is not running. |
说明我们的机器没有启动 Docker,需要把 Docker 启动后再运行,在 Windows 上运行的是 Docker Desktop。
制作镜像成功后,我们可以运行命令查看当前我们的 Docker 中的镜像:docker images
,显示如下:
1 | $ docker images |
可以看到,我们创建的镜像 myorg/myapp
已经成功了,然后我们运行此镜像:docker run -p 8080:8080 myorg/myapp
,显示如下:
1 | $ docker run -p 8080:8080 myorg/myapp |
这里,我们的 Docker 镜像就制作成功了,可以访问 localhost:8080 看一下结果是否正常。
使用命令查看 docker 镜像运行是否正常: docker ps
1 | $ docker ps |
可以看到我们的 docker 正在正常运行,并且已经运行了 8 分钟。
到目前为止,我们的 docker 配置非常简单,生成的镜像也非常低效。Docker 镜像只有一个单独的文件系统层,里面包含了一整个 jar,我们每次更改代码的时候,都会直接更改到这层,这会导致这层占用空间非常大。我们可以改进一下,把这个 JAR 包分成多层。
更小的镜像
我们前面采用的基础镜像是 openjdk:8-jdk-alpine
,alpine
镜像比 Dockerhub 提供的标准的 openjdk
镜像更小。 目前还没有针对 Java 11 的官方 alpine 镜像。你还可以在基础镜像中通过使用 jre
标签代替 jdk
来节省大约 20 MB 的空间。虽然并不是所有的应用使用 JRE 就能工作(相对于 JDK),但是大部分应用都可以。实际上,由于对 JDK 特性滥用的风险存在(比如编译),一些组织会强制执行对于每个 APP 必须遵守的规则。
最后,对于镜像构建有一个非常重要的问题:我们的目标不总是要构建尽可能小的镜像。较小的镜像上传和下载速度比较快,这固然好,但是这也有个前提:它们中的任何层都没有被缓存。镜像注册非常复杂,你可以通过尝试巧妙地使用图像构造来轻松地失去这些功能的好处。如果你使用公共基础层,你根本不需要担心镜像的总大小,并且随着注册和平台的发展,镜像可能会变得更小。话说回来,尝试优化我们应用的镜像的层仍然是非常重要以及有效的,但是我们的目标应该始终是将最快速变化的东西放在最高层,并与其他程序共享尽可能多的大型较低层。
优化的 Dockerfile
由于 Spring Boot fat jar 的打包方式,它自然就有多层这个概念。我们解压 jar 包后可以发现,它里面早已分成了外部和内部依赖。要在 docker 构建中执行此操作,我们需要先解压 jar 包。例如:
1 | $ mkdir build/dependency |
对应的 Dockerfile 为:
1 | FROM openjdk:8-jdk-alpine |
现在我们的 docker 镜像有三层了,后面的两层包含所有的应用程序资源。如果应用的依赖不做更改,那么第一层(来自 `BOOT-INF/lib)将不会变动,因此构建将会非常快,只要基础层已经被缓存过,容器在运行时的启动也是如此。
我们使用了硬编码指定应用的启动类
com.tryking.docker.Application
。在这里我们还可以将 Spring Boot fatJarLauncher
复制进镜像,然后使用它来启动应用,这样就不需要指定 main 类了,但是它可能会拖慢速度。
微调
如果我们想要让应用启动速度尽可能快,有一些微调我们可以用到。下面是一些方法:
- 使用
spring-context-indexer
。对于小型程序来说它可能增加不了太多,但是苍蝇再小也是肉。 - 如果可以的话,尽量不要使用执行器 actuators。
- 使用 Spring Boot 2.1 以及 Spring 5.1。
- 使用
spring.config.location
(命令行参数或系统属性) 代替 Spring Boot 默认的配置文件地址。 - 关闭 JMX - 在容器中你可能不需要它。命令:
spring.jmx.enabled=false
- 使用
-noverify
运行 JVM。还要注意:-XX:TieredStopAtLevel=1
(这个虽然会节约启动时的时间,但是后面会导致 JIT 的速度减慢) - 对于 Java 8, 使用容器内存提示:
-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap
。在 Java 11 中,这些已经被默认设置了。
我们的应用程序可能运行的时候不需要完整的 CPU,但是它需要多个 CPU 才能尽可能快地启动(至少 2 个,4 个更好)。如果我们不介意启动速度较慢,可以将 CPU 降到 4 个以下。如果我们被迫以少于 4 个 CPU 启动,我们可以设置 Dspring.backgroundpreinitializer.ignore = true
,因为它会阻止 Spring Boot 创建一个它可能无法使用的新线程。(这个适用于 Spring Boot 2.1.0 及以上版本)
Docker 构建插件
如果我们在构建中不想直接调用 docker
命令,有很多 Gradle 插件可以帮我们做这些,下面介绍一些。
Palantir Gradle Plugin
Palantir Gradle Plugin
插件和 Dockerfile 一起工作,它也可以为我们生成一个 Dockerfile,然后它会运行 docker
,就像我们自己在命令行中运行一样。
首先,我们需要在 build.gralde
中引入:
1 | buildscript { |
最后,我们需要应用此插件然后调用它的任务:
1 | apply plugin: 'com.palantir.docker' |
在此示例中,我们选择在解压 Spring Boot 的 fat jar 到一个 build 目录下的特定位置,这里是 docker 构建的根目录。然后上面的多层(multi-layer,不是 multi-stage) Dockerfile 就会工作了。
Jib Gradle Plugin
参照 Jib
持续集成
自动化现在是(或者应该是)每个应用程序的一部分。人们用来进行自动化的工具往往非常擅长从源代码中调用构建系统。因此,如果有一个 Docker 镜像,并且构建代理中的环境与开发人员的环境一致,这对于我们来说就足够了。对 docker 注册表进行身份验证对我们来说可能是最大的挑战,但是所有的自动化工具中都有一些功能可以帮助我们解决这个问题。
但是,有时我们最好将容器的创建完全留给自动化层,这样可以保证我们的代码不需要被污染。容器创建是一个棘手的问题,我们开发人员往往并不关心它。如果我们的代码更加整洁,那么不同的工具将更有可能做到“做正确的事”,比如应用安全修复,优化缓存等。自动化有很多选择,如今他们都会带一些与容器化相关的功能。接下来我们看一下 Jenkins
。
Jenkins
Jenkins 是一个非常流行的自动化服务。它有很多特性,但是最接近其他自动化示例的是它的 pipeline
功能。下面是一个 Jenkinsfile
,它会使用 maven 构建一个 Spring Boot 工程,然后使用 Dockerfile
构建一个镜像并把它们推送到仓库中。
1 | node { |
结语
本文提供了很多用于为 Spring Boot 应用构建容器镜像的选项。所有的内容都是有效的选择,现在由你自己决定需要哪个。你的第一个问题应该是“我真的需要建立一个容器镜像吗?”如果答案是确定的,那你的选择可能要尽可能考虑效率和可缓存性等。
标题:使用 Docker 构建你的 Spring Boot 程序
作者:末日没有进行曲
链接:link
时间:2019/08/16
声明:本博客所有文章均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。