很多人都使用容器来包装他们的 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 许可协议,转载请注明出处。

